jsxpression 0.1.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.
Files changed (165) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +368 -0
  3. package/lib/analyze/analysis-error.d.ts.map +1 -0
  4. package/lib/analyze/analysis-report.d.ts +33 -0
  5. package/lib/analyze/analysis-report.d.ts.map +1 -0
  6. package/lib/analyze/analysis-report.js +194 -0
  7. package/lib/analyze/analysis-report.js.map +1 -0
  8. package/lib/analyze/analyze.d.ts.map +1 -0
  9. package/lib/analyze/analyze.test.d.ts.map +1 -0
  10. package/lib/analyze/analyze.test.js +765 -0
  11. package/lib/analyze/analyze.test.js.map +1 -0
  12. package/lib/analyze/data/analyze.d.ts.map +1 -0
  13. package/lib/analyze/data/data-access.d.ts.map +1 -0
  14. package/lib/analyze/data/index.d.ts.map +1 -0
  15. package/lib/analyze/data/method-calls.d.ts.map +1 -0
  16. package/lib/analyze/data/utils.d.ts.map +1 -0
  17. package/lib/analyze/index.d.ts.map +1 -0
  18. package/lib/analyze/jsx/analyze.d.ts.map +1 -0
  19. package/lib/analyze/jsx/element-attributes.d.ts.map +1 -0
  20. package/lib/analyze/jsx/element-attributes.js +89 -0
  21. package/lib/analyze/jsx/element-attributes.js.map +1 -0
  22. package/lib/analyze/jsx/element-children.d.ts.map +1 -0
  23. package/lib/analyze/jsx/element-tags.d.ts.map +1 -0
  24. package/lib/analyze/jsx/index.d.ts.map +1 -0
  25. package/lib/analyze/security/analyze.d.ts.map +1 -0
  26. package/lib/analyze/security/call-expressions.d.ts.map +1 -0
  27. package/lib/analyze/security/declarations.d.ts.map +1 -0
  28. package/lib/analyze/security/expressions.d.ts.map +1 -0
  29. package/lib/analyze/security/identifiers.d.ts.map +1 -0
  30. package/lib/analyze/security/index.d.ts.map +1 -0
  31. package/lib/analyze/security/member-access.d.ts.map +1 -0
  32. package/lib/builtins.d.ts +50 -0
  33. package/lib/builtins.d.ts.map +1 -0
  34. package/lib/builtins.js +1065 -0
  35. package/lib/builtins.js.map +1 -0
  36. package/lib/codegen/builtin-types.d.ts +11 -0
  37. package/lib/codegen/builtin-types.d.ts.map +1 -0
  38. package/lib/codegen/builtin-types.js +151 -0
  39. package/lib/codegen/builtin-types.js.map +1 -0
  40. package/lib/codegen/builtins/builtin-types.d.ts +2 -0
  41. package/lib/codegen/builtins/builtin-types.d.ts.map +1 -0
  42. package/lib/codegen/builtins/builtin-types.js +143 -0
  43. package/lib/codegen/builtins/builtin-types.js.map +1 -0
  44. package/lib/codegen/builtins/index.d.ts +2 -0
  45. package/lib/codegen/builtins/index.d.ts.map +1 -0
  46. package/lib/codegen/builtins/index.js +6 -0
  47. package/lib/codegen/builtins/index.js.map +1 -0
  48. package/lib/codegen/declare-types.d.ts +20 -0
  49. package/lib/codegen/declare-types.d.ts.map +1 -0
  50. package/lib/codegen/declare-types.js +299 -0
  51. package/lib/codegen/declare-types.js.map +1 -0
  52. package/lib/codegen/generate-typescript-definitions.d.ts +31 -0
  53. package/lib/codegen/generate-typescript-definitions.d.ts.map +1 -0
  54. package/lib/codegen/generate-typescript-definitions.js +37 -0
  55. package/lib/codegen/generate-typescript-definitions.js.map +1 -0
  56. package/lib/codegen/generate-typescript-definitions.test.d.ts +2 -0
  57. package/lib/codegen/generate-typescript-definitions.test.d.ts.map +1 -0
  58. package/lib/codegen/generate-typescript-definitions.test.js +587 -0
  59. package/lib/codegen/generate-typescript-definitions.test.js.map +1 -0
  60. package/lib/codegen/index.d.ts +2 -0
  61. package/lib/codegen/index.d.ts.map +1 -0
  62. package/lib/codegen/index.js +6 -0
  63. package/lib/codegen/index.js.map +1 -0
  64. package/lib/codegen/schema/component-types.d.ts +3 -0
  65. package/lib/codegen/schema/component-types.d.ts.map +1 -0
  66. package/lib/codegen/schema/component-types.js +118 -0
  67. package/lib/codegen/schema/component-types.js.map +1 -0
  68. package/lib/codegen/schema/data-types.d.ts +3 -0
  69. package/lib/codegen/schema/data-types.d.ts.map +1 -0
  70. package/lib/codegen/schema/data-types.js +98 -0
  71. package/lib/codegen/schema/data-types.js.map +1 -0
  72. package/lib/codegen/schema/index.d.ts +3 -0
  73. package/lib/codegen/schema/index.d.ts.map +1 -0
  74. package/lib/codegen/schema/index.js +8 -0
  75. package/lib/codegen/schema/index.js.map +1 -0
  76. package/lib/codegen/schema/utils.d.ts +4 -0
  77. package/lib/codegen/schema/utils.d.ts.map +1 -0
  78. package/lib/codegen/schema/utils.js +103 -0
  79. package/lib/codegen/schema/utils.js.map +1 -0
  80. package/lib/codegen/schema-to-types.d.ts +14 -0
  81. package/lib/codegen/schema-to-types.d.ts.map +1 -0
  82. package/lib/codegen/schema-to-types.js +203 -0
  83. package/lib/codegen/schema-to-types.js.map +1 -0
  84. package/lib/compile/compile.d.ts.map +1 -0
  85. package/lib/compile/compile.js +245 -0
  86. package/lib/compile/compile.js.map +1 -0
  87. package/lib/compile/compile.test.d.ts.map +1 -0
  88. package/lib/compile/compile.test.js +148 -0
  89. package/lib/compile/compile.test.js.map +1 -0
  90. package/lib/evaluate/evaluate.d.ts +26 -0
  91. package/lib/evaluate/evaluate.d.ts.map +1 -0
  92. package/lib/evaluate/evaluate.js +40 -0
  93. package/lib/evaluate/evaluation-error.d.ts +4 -0
  94. package/lib/evaluate/evaluation-error.d.ts.map +1 -0
  95. package/lib/evaluate/evaluation-error.js +6 -0
  96. package/lib/index.d.ts +8 -0
  97. package/lib/index.d.ts.map +1 -0
  98. package/lib/index.js +16 -0
  99. package/lib/index.js.map +1 -0
  100. package/lib/render.d.ts +58 -0
  101. package/lib/render.d.ts.map +1 -0
  102. package/lib/render.js +52 -0
  103. package/lib/render.test.d.ts.map +1 -0
  104. package/lib/schema.d.ts +68 -0
  105. package/lib/schema.d.ts.map +1 -0
  106. package/lib/schema.js +106 -0
  107. package/lib/validate.d.ts +41 -0
  108. package/lib/validate.d.ts.map +1 -0
  109. package/lib/validate.js +42 -0
  110. package/package.json +27 -0
  111. package/src/analyze/analysis-error.ts +26 -0
  112. package/src/analyze/analysis-report.ts +228 -0
  113. package/src/analyze/analyze.test.ts +829 -0
  114. package/src/analyze/analyze.ts +10 -0
  115. package/src/analyze/data/analyze.ts +15 -0
  116. package/src/analyze/data/data-access.ts +349 -0
  117. package/src/analyze/data/index.ts +1 -0
  118. package/src/analyze/data/method-calls.ts +96 -0
  119. package/src/analyze/data/utils.ts +329 -0
  120. package/src/analyze/index.ts +3 -0
  121. package/src/analyze/jsx/analyze.ts +17 -0
  122. package/src/analyze/jsx/element-attributes.ts +143 -0
  123. package/src/analyze/jsx/element-children.ts +97 -0
  124. package/src/analyze/jsx/element-tags.ts +55 -0
  125. package/src/analyze/jsx/index.ts +1 -0
  126. package/src/analyze/security/analyze.ts +21 -0
  127. package/src/analyze/security/call-expressions.ts +41 -0
  128. package/src/analyze/security/declarations.ts +67 -0
  129. package/src/analyze/security/expressions.ts +100 -0
  130. package/src/analyze/security/identifiers.ts +113 -0
  131. package/src/analyze/security/index.ts +1 -0
  132. package/src/analyze/security/member-access.ts +35 -0
  133. package/src/analyze/utils.ts +118 -0
  134. package/src/analyze/validation-context.ts +32 -0
  135. package/src/builtins.ts +1149 -0
  136. package/src/codegen/builtins/builtin-types.ts +174 -0
  137. package/src/codegen/builtins/index.ts +1 -0
  138. package/src/codegen/generate-typescript-definitions.test.ts +676 -0
  139. package/src/codegen/generate-typescript-definitions.ts +35 -0
  140. package/src/codegen/index.ts +1 -0
  141. package/src/codegen/schema/component-types.ts +140 -0
  142. package/src/codegen/schema/data-types.ts +109 -0
  143. package/src/codegen/schema/index.ts +2 -0
  144. package/src/codegen/schema/utils.ts +111 -0
  145. package/src/compile/compilation-error.ts +37 -0
  146. package/src/compile/compile.test.ts +180 -0
  147. package/src/compile/compile.ts +282 -0
  148. package/src/compile/index.ts +2 -0
  149. package/src/evaluate/evaluate.test.ts +316 -0
  150. package/src/evaluate/evaluate.ts +81 -0
  151. package/src/evaluate/evaluation-error.ts +6 -0
  152. package/src/evaluate/index.ts +2 -0
  153. package/src/index.ts +10 -0
  154. package/src/jsx.ts +161 -0
  155. package/src/parse/index.ts +2 -0
  156. package/src/parse/parse-error.ts +14 -0
  157. package/src/parse/parse.test.ts +99 -0
  158. package/src/parse/parse.ts +40 -0
  159. package/src/render.test.ts +425 -0
  160. package/src/render.ts +75 -0
  161. package/src/schema.ts +195 -0
  162. package/src/traverse.test.ts +104 -0
  163. package/src/traverse.ts +78 -0
  164. package/src/types.ts +17 -0
  165. package/src/validate.ts +59 -0
@@ -0,0 +1,282 @@
1
+ import type { Expression, PrivateIdentifier, Program, Super, Identifier, Literal } from "acorn";
2
+
3
+ import {
4
+ isJsxElement,
5
+ isJsxFragment,
6
+ isJsxRoot,
7
+ isJsxText,
8
+ isJsxExpressionContainer,
9
+ isJsxEmptyExpression,
10
+ type JSXRoot,
11
+ type JSXElement,
12
+ type JSXFragment,
13
+ type JSXEmptyExpression,
14
+ type JSXNode,
15
+ type JSXAttribute,
16
+ type JSXSpreadAttribute,
17
+ type JSXNameNode,
18
+ } from "../jsx";
19
+ import { CompilationError } from "./compilation-error";
20
+
21
+ type CompilableExpression = Expression | PrivateIdentifier | Super | JSXElement | JSXFragment | JSXEmptyExpression;
22
+
23
+ export function compile(ast: Program): string {
24
+ const root = getRoot(ast);
25
+ const code = emitJsxRoot(root);
26
+
27
+ return `"use strict";return ${code};`;
28
+ }
29
+
30
+ function getRoot(ast: Program): JSXRoot {
31
+ for (const declaration of ast.body) {
32
+ if (declaration.type === "ExpressionStatement" && isJsxRoot(declaration.expression)) {
33
+ return declaration.expression;
34
+ }
35
+ if (declaration.type === "ReturnStatement" && declaration.argument && isJsxRoot(declaration.argument)) {
36
+ return declaration.argument;
37
+ }
38
+ // Handle bare expressions like {data.user.name} which parse as BlockStatement
39
+ if (declaration.type === "BlockStatement" && declaration.body.length === 1) {
40
+ const innerStatement = declaration.body[0];
41
+ if (innerStatement.type === "ExpressionStatement") {
42
+ // Create a synthetic JSXExpressionContainer for the bare expression
43
+ const syntheticContainer = {
44
+ type: "JSXExpressionContainer" as const,
45
+ expression: innerStatement.expression as CompilableExpression,
46
+ };
47
+ return syntheticContainer as JSXRoot;
48
+ }
49
+ }
50
+ }
51
+
52
+ throw CompilationError.fromNode("No root JSX element/fragment/expression found", ast);
53
+ }
54
+
55
+ function emitJsxRoot(node: JSXRoot): string {
56
+ if (isJsxElement(node)) {
57
+ return emitJsxElement(node);
58
+ }
59
+ if (isJsxFragment(node)) {
60
+ return emitJsxFragment(node);
61
+ }
62
+ // Handle bare expression containers like {data.user.name}
63
+ return emitExpression(node.expression);
64
+ }
65
+
66
+ function emitJsxElement(node: JSXElement): string {
67
+ const name = emitJsxName(node.openingElement.name as JSXNameNode);
68
+ const attributes = emitAttributes(node.openingElement.attributes);
69
+ const children = emitJsxChildren(node.children);
70
+
71
+ if (children.length > 0) {
72
+ return `h(${name}, ${attributes}, ${children.join(", ")})`;
73
+ }
74
+
75
+ return `h(${name}, ${attributes})`;
76
+ }
77
+
78
+ function emitJsxFragment(node: JSXFragment): string {
79
+ const children = emitJsxChildren(node.children);
80
+ return `[${children.join(", ")}]`;
81
+ }
82
+
83
+ function emitJsxName(nameNode: JSXNameNode): string {
84
+ switch (nameNode.type) {
85
+ case "JSXIdentifier":
86
+ return JSON.stringify(nameNode.name);
87
+ case "JSXMemberExpression": {
88
+ const left = emitJsxName(nameNode.object);
89
+ const right = emitJsxName(nameNode.property);
90
+ return JSON.stringify(JSON.parse(left) + "." + JSON.parse(right));
91
+ }
92
+ default:
93
+ throw CompilationError.fromNode(`Unsupported JSX name: ${(nameNode as any).type}`, nameNode);
94
+ }
95
+ }
96
+
97
+ function emitAttributes(attributes: (JSXAttribute | JSXSpreadAttribute)[]): string {
98
+ const parts: string[] = [];
99
+ let currentObj: string[] = [];
100
+
101
+ const flushCurrent = (): void => {
102
+ if (currentObj.length) {
103
+ parts.push(`{ ${currentObj.join(", ")} }`);
104
+ currentObj = [];
105
+ }
106
+ };
107
+
108
+ for (const attribute of attributes) {
109
+ if (attribute.type === "JSXSpreadAttribute") {
110
+ flushCurrent();
111
+ parts.push(`(${emitExpression(attribute.argument as CompilableExpression)})`);
112
+ continue;
113
+ }
114
+
115
+ if (attribute.type !== "JSXAttribute") {
116
+ throw CompilationError.fromNode("Only simple JSX attributes allowed", attribute);
117
+ }
118
+
119
+ const key = attribute.name?.name as string;
120
+
121
+ let value: string;
122
+ if (!attribute.value) {
123
+ value = "true";
124
+ } else if (attribute.value.type === "Literal") {
125
+ value = JSON.stringify(attribute.value.value);
126
+ } else if (attribute.value.type === "JSXExpressionContainer") {
127
+ value = emitExpression(attribute.value.expression as CompilableExpression);
128
+ } else {
129
+ throw CompilationError.fromNode("Unsupported attribute value", attribute.value);
130
+ }
131
+
132
+ currentObj.push(`${key}: ${value}`);
133
+ }
134
+
135
+ flushCurrent();
136
+
137
+ if (parts.length === 0) {
138
+ return "null";
139
+ }
140
+
141
+ if (parts.length === 1) {
142
+ return parts[0];
143
+ }
144
+
145
+ return `Object.assign({}, ${parts.join(", ")})`;
146
+ }
147
+
148
+ function emitJsxChildren(children: JSXNode[]): string[] {
149
+ const result: string[] = [];
150
+
151
+ for (const child of children) {
152
+ if (isJsxText(child)) {
153
+ const text = normalizeJsxText(child.value);
154
+ if (text) {
155
+ result.push(JSON.stringify(text));
156
+ }
157
+ } else if (isJsxExpressionContainer(child)) {
158
+ // Skip empty expressions {} - they don't contribute any content
159
+ if (!isJsxEmptyExpression(child.expression)) {
160
+ result.push(emitExpression(child.expression as CompilableExpression));
161
+ }
162
+ } else if (isJsxElement(child)) {
163
+ result.push(emitJsxElement(child));
164
+ } else if (isJsxFragment(child)) {
165
+ result.push(emitJsxFragment(child));
166
+ }
167
+ }
168
+
169
+ return result;
170
+ }
171
+
172
+ const TRAILING_PLUS_RX = /\+\s*\+\s*$/;
173
+
174
+ function emitExpression(node: CompilableExpression): string {
175
+ switch (node.type) {
176
+ case "Literal":
177
+ return JSON.stringify(node.value);
178
+ case "Identifier":
179
+ return node.name;
180
+ case "TemplateLiteral": {
181
+ const bits: string[] = [];
182
+ node.quasis.forEach((quasi, index) => {
183
+ bits.push(JSON.stringify(quasi.value.cooked ?? ""));
184
+
185
+ if (node.expressions.length > index) {
186
+ bits.push(`+(${emitExpression(node.expressions[index])})+`);
187
+ }
188
+ });
189
+
190
+ return bits.join("").replace(TRAILING_PLUS_RX, "") || '""';
191
+ }
192
+ case "ParenthesizedExpression":
193
+ return `(${emitExpression(node.expression)})`;
194
+ case "UnaryExpression":
195
+ return `${node.operator}${emitExpression(node.argument)}`;
196
+ case "BinaryExpression":
197
+ case "LogicalExpression":
198
+ return `(${emitExpression(node.left)} ${node.operator} ${emitExpression(node.right)})`;
199
+ case "ConditionalExpression":
200
+ return `(${emitExpression(node.test)}?${emitExpression(node.consequent)}:${emitExpression(node.alternate)})`;
201
+ case "ArrayExpression":
202
+ return `[${node.elements
203
+ .map((element) => {
204
+ if (!element) {
205
+ return "undefined";
206
+ }
207
+ if (element.type === "SpreadElement") {
208
+ return `...${emitExpression(element.argument as CompilableExpression)}`;
209
+ }
210
+ return emitExpression(element as CompilableExpression);
211
+ })
212
+ .join(", ")}]`;
213
+ case "ObjectExpression": {
214
+ const keyValuePairs = node.properties
215
+ .map((property) => {
216
+ if (property.type === "SpreadElement") {
217
+ return `...${emitExpression(property.argument as CompilableExpression)}`;
218
+ }
219
+ // analyze() ensures these are simple properties
220
+ const prop = property as any; // analyze() guarantees Property type
221
+ const key = getAstKeyString(prop.key as Identifier | Literal);
222
+ return `${key}: ${emitExpression(prop.value)}`;
223
+ })
224
+ .join(", ");
225
+ return `{ ${keyValuePairs} }`;
226
+ }
227
+ case "MemberExpression": {
228
+ const obj = emitExpression(node.object);
229
+ let prop: string;
230
+ if (node.computed) {
231
+ prop = `[${emitExpression(node.property)}]`;
232
+ } else {
233
+ prop = `.${getAstKeyString(node.property as Identifier | Literal)}`;
234
+ }
235
+ return `${obj}${prop}`;
236
+ }
237
+ case "CallExpression": {
238
+ const callee = emitExpression(node.callee);
239
+ const args = node.arguments
240
+ .map((argument) => {
241
+ if (argument.type === "SpreadElement") {
242
+ return `...${emitExpression(argument.argument as CompilableExpression)}`;
243
+ }
244
+ return emitExpression(argument as CompilableExpression);
245
+ })
246
+ .join(", ");
247
+ return `${callee}(${args})`;
248
+ }
249
+ case "ArrowFunctionExpression": {
250
+ const params = node.params.map((param) => emitExpression(param as CompilableExpression)).join(", ");
251
+ // analyze() ensures this is not a BlockStatement
252
+ const body = emitExpression(node.body as CompilableExpression);
253
+ return `(${params}) => ${body}`;
254
+ }
255
+ case "JSXElement":
256
+ return emitJsxElement(node);
257
+ case "JSXFragment":
258
+ return emitJsxFragment(node);
259
+ case "JSXEmptyExpression":
260
+ return "null";
261
+ default:
262
+ throw CompilationError.fromNode(`Unsupported expression: ${node?.type}`, node);
263
+ }
264
+ }
265
+
266
+ function getAstKeyString(node: Identifier | Literal): string {
267
+ if (node.type === "Identifier") {
268
+ return node.name;
269
+ } else if (node.type === "Literal") {
270
+ return JSON.stringify(node.value);
271
+ } else {
272
+ // This should never happen due to type constraints but
273
+ // if JSX ever changes this could be possible!
274
+ throw CompilationError.fromNode("Unsupported key type", node);
275
+ }
276
+ }
277
+
278
+ const WHITESPACE_COLLAPSER_RX = /\s+/g;
279
+
280
+ function normalizeJsxText(value: string): string {
281
+ return value.replace(WHITESPACE_COLLAPSER_RX, " ").trim();
282
+ }
@@ -0,0 +1,2 @@
1
+ export { compile } from "./compile";
2
+ export { CompilationError } from "./compilation-error";
@@ -0,0 +1,316 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { evaluate, type EvaluateOptions } from "./evaluate";
3
+ import { compile } from "../compile";
4
+ import { parse } from "../parse";
5
+
6
+ import { EvaluationError } from "./evaluation-error";
7
+
8
+ type Node = {
9
+ type: string;
10
+ props: Record<string, unknown>;
11
+ children: Node[];
12
+ };
13
+
14
+ describe("evaluate", () => {
15
+ const compileAndEvaluate = (source: string, options: EvaluateOptions = {}): Node => {
16
+ return evaluate(compile(parse(source)), options);
17
+ };
18
+
19
+ it("should evaluate JSX with props", () => {
20
+ const TestComponent = (props: any, ...children: any[]): Node => ({
21
+ type: "TestComponent",
22
+ props,
23
+ children,
24
+ });
25
+
26
+ const result = compileAndEvaluate("<Test x={data.x} y={data.y}>Hello</Test>", {
27
+ data: { x: 10, y: 20 },
28
+ components: {
29
+ Test: TestComponent,
30
+ },
31
+ });
32
+
33
+ expect(result).toEqual({
34
+ type: "TestComponent",
35
+ props: { x: 10, y: 20 },
36
+ children: ["Hello"],
37
+ });
38
+ });
39
+
40
+ it("should evaluate JSX with Math operations", () => {
41
+ const TestComponent = (props: any): Node => ({
42
+ type: "TestComponent",
43
+ props,
44
+ children: [],
45
+ });
46
+
47
+ const result = compileAndEvaluate("<Test value={Math.floor(data.x * Math.PI)}>Text</Test>", {
48
+ data: { x: 5 },
49
+ components: {
50
+ Test: TestComponent,
51
+ },
52
+ });
53
+
54
+ expect(result.props!.value).toBe(15); // floor(5 * 3.14159...) = 15
55
+ });
56
+
57
+ it("should throw EvaluationError for unknown component", () => {
58
+ const compiled = compile(parse("<UnknownComponent>Hello</UnknownComponent>"));
59
+
60
+ expect(() => evaluate(compiled, { components: {} })).toThrow(EvaluationError);
61
+ expect(() => evaluate(compiled, { components: {} })).toThrow('component "UnknownComponent" is not allowed');
62
+ });
63
+
64
+ it("should deep-freeze props to prevent mutations", () => {
65
+ const TestComponent = (props: any): Node => {
66
+ expect(() => {
67
+ props.nested.value = 999;
68
+ }).toThrow();
69
+ return { type: "Test", props, children: [] };
70
+ };
71
+
72
+ const compiled = compile(parse("<Test>Hello</Test>"));
73
+
74
+ evaluate(compiled, {
75
+ data: { nested: { value: 42 } },
76
+ components: {
77
+ Test: TestComponent,
78
+ },
79
+ });
80
+ });
81
+
82
+ it("should evaluate nested elements", () => {
83
+ const OuterComponent = (props: any, ...children: any[]): Node => ({
84
+ type: "Outer",
85
+ props,
86
+ children,
87
+ });
88
+
89
+ const InnerComponent = (props: any, ...children: any[]): Node => ({
90
+ type: "Inner",
91
+ props,
92
+ children,
93
+ });
94
+
95
+ const result = compileAndEvaluate("<Outer x={data.x}><Inner y={data.y}>Text</Inner></Outer>", {
96
+ data: { x: 10, y: 20 },
97
+ components: {
98
+ Outer: OuterComponent,
99
+ Inner: InnerComponent,
100
+ },
101
+ });
102
+
103
+ expect(result.type).toBe("OuterComponent");
104
+ expect(result.props).toEqual({ x: 10 });
105
+ expect(result.children).toHaveLength(1);
106
+ const innerNode = result.children[0] as Node;
107
+ expect(innerNode.type).toBe("InnerComponent");
108
+ expect(innerNode.props).toEqual({ y: 20 });
109
+ expect(innerNode.children).toEqual(["Text"]);
110
+ });
111
+
112
+ it("should use custom createElement when provided", () => {
113
+ const customCreateElement = (type: any, props: any, ...children: any[]): Node => ({
114
+ type: type.name || "Unknown",
115
+ props: { ...props, custom: true },
116
+ children,
117
+ });
118
+
119
+ const TestComponent = (): Node => ({ type: "Test", props: {}, children: [] });
120
+
121
+ const result = compileAndEvaluate("<Test x={10}>Hello</Test>", {
122
+ components: {
123
+ Test: TestComponent,
124
+ },
125
+ createElement: customCreateElement,
126
+ });
127
+
128
+ expect(result).toEqual({
129
+ type: "TestComponent",
130
+ props: { x: 10, custom: true },
131
+ children: ["Hello"],
132
+ });
133
+ });
134
+
135
+ it("should evaluate conditional expressions with props", () => {
136
+ const TestComponent = (props: any): Node => ({
137
+ type: "TestComponent",
138
+ props,
139
+ children: [],
140
+ });
141
+
142
+ const result = compileAndEvaluate("<Test msg={data.show ? 'visible' : 'hidden'}>Text</Test>", {
143
+ data: { show: true },
144
+ components: {
145
+ Test: TestComponent,
146
+ },
147
+ });
148
+
149
+ expect(result.props!.msg).toBe("visible");
150
+ });
151
+
152
+ it("should evaluate array methods on props", () => {
153
+ const TestComponent = (props: any): Node => ({
154
+ type: "TestComponent",
155
+ props,
156
+ children: [],
157
+ });
158
+
159
+ const result = compileAndEvaluate("<Test values={data.items.filter(x => x > 2).map(x => x * 2)}>Text</Test>", {
160
+ data: { items: [1, 2, 3, 4, 5] },
161
+ components: {
162
+ Test: TestComponent,
163
+ },
164
+ });
165
+
166
+ expect(result.props!.values).toEqual([6, 8, 10]);
167
+ });
168
+
169
+ it("should handle runtime errors gracefully", () => {
170
+ const compiled = compile(parse("<Test value={data.missing.nested}>Text</Test>"));
171
+ const TestComponent = (): Node => ({ type: "Test", props: {}, children: [] });
172
+
173
+ expect(() =>
174
+ evaluate(compiled, {
175
+ data: {},
176
+ components: {
177
+ Test: TestComponent,
178
+ },
179
+ })
180
+ ).toThrow(EvaluationError);
181
+ });
182
+
183
+ it("should evaluate logical operators with props", () => {
184
+ const TestComponent = (props: any): Node => ({
185
+ type: "TestComponent",
186
+ props,
187
+ children: [],
188
+ });
189
+
190
+ const result = compileAndEvaluate("<Test msg={data.name || 'Anonymous'}>Text</Test>", {
191
+ data: { name: "" },
192
+ components: {
193
+ Test: TestComponent,
194
+ },
195
+ });
196
+
197
+ expect(result.props!.msg).toBe("Anonymous");
198
+ });
199
+
200
+ it("should flatten and filter children correctly", () => {
201
+ const TestComponent = (props: any, ...children: any[]): Node => ({
202
+ type: "Test",
203
+ props,
204
+ children,
205
+ });
206
+
207
+ const result = compileAndEvaluate("<Test>{data.show && 'Visible'}{false}{'Always'}</Test>", {
208
+ data: { show: true },
209
+ components: {
210
+ Test: TestComponent,
211
+ },
212
+ });
213
+
214
+ expect(result.children).toEqual(["Visible", "Always"]);
215
+ });
216
+
217
+ it("should handle JSX fragments", () => {
218
+ const TestComponent = (): Node => ({ type: "Test", props: {}, children: [] });
219
+
220
+ const result = compileAndEvaluate("<><Test /><Test /></>", {
221
+ components: { Test: TestComponent },
222
+ });
223
+
224
+ expect(Array.isArray(result)).toBe(true);
225
+ expect(result).toHaveLength(2);
226
+ });
227
+
228
+ it("should handle empty JSX expressions", () => {
229
+ const TestComponent = (props: any, ...children: any[]): Node => ({
230
+ type: "Test",
231
+ props,
232
+ children,
233
+ });
234
+
235
+ const result = compileAndEvaluate("<Test>{}{data.value}{}</Test>", {
236
+ data: { value: "hello" },
237
+ components: { Test: TestComponent },
238
+ });
239
+
240
+ expect(result.children).toEqual(["hello"]);
241
+ });
242
+
243
+ it("should evaluate complex nested expressions", () => {
244
+ const TestComponent = (props: any): Node => ({
245
+ type: "Test",
246
+ props,
247
+ children: [],
248
+ });
249
+
250
+ const result = compileAndEvaluate(
251
+ "<Test values={data.items.filter(x => x > 2).map(x => x * 2).join(', ')}>Text</Test>",
252
+ {
253
+ data: { items: [1, 2, 3, 4, 5] },
254
+ components: { Test: TestComponent },
255
+ }
256
+ );
257
+
258
+ expect(result.props!.values).toBe("6, 8, 10");
259
+ });
260
+
261
+ it("should pass correct parameters to custom createElement", () => {
262
+ const mockCreateElement = vi.fn();
263
+
264
+ compileAndEvaluate("<Test x={10}>Hello</Test>", {
265
+ components: { Test: () => ({ type: "Test", props: {}, children: [] }) },
266
+ createElement: mockCreateElement,
267
+ });
268
+
269
+ expect(mockCreateElement).toHaveBeenCalledWith(expect.any(Function), { x: 10 }, "Hello");
270
+ });
271
+
272
+ it("should handle string literals in expressions", () => {
273
+ const TestComponent = (props: any): Node => ({
274
+ type: "Test",
275
+ props,
276
+ children: [],
277
+ });
278
+
279
+ const result = compileAndEvaluate('<Test msg={"Hello " + data.name}>Text</Test>', {
280
+ data: { name: "World" },
281
+ components: { Test: TestComponent },
282
+ });
283
+
284
+ expect(result.props!.msg).toBe("Hello World");
285
+ });
286
+
287
+ it("should handle numeric operations", () => {
288
+ const TestComponent = (props: any): Node => ({
289
+ type: "Test",
290
+ props,
291
+ children: [],
292
+ });
293
+
294
+ const result = compileAndEvaluate("<Test value={data.x + data.y * 2}>Text</Test>", {
295
+ data: { x: 5, y: 3 },
296
+ components: { Test: TestComponent },
297
+ });
298
+
299
+ expect(result.props!.value).toBe(11); // 5 + (3 * 2)
300
+ });
301
+
302
+ it("should handle data access in children", () => {
303
+ const TestComponent = (props: any, ...children: any[]): Node => ({
304
+ type: "Test",
305
+ props,
306
+ children,
307
+ });
308
+
309
+ const result = compileAndEvaluate("<Test>Hello {data.name}!</Test>", {
310
+ data: { name: "World" },
311
+ components: { Test: TestComponent },
312
+ });
313
+
314
+ expect(result.children).toEqual(["Hello", "World", "!"]);
315
+ });
316
+ });
@@ -0,0 +1,81 @@
1
+ import { EvaluationError } from "./evaluation-error";
2
+
3
+ export type ComponentDict = Record<string, Component>;
4
+
5
+ export type CreateElement<T = any> = (type: Component, props?: PropsDict, ...children: any[]) => T;
6
+
7
+ export type PropsDict = Record<string, unknown>;
8
+
9
+ export type DataDict = Record<string, unknown>;
10
+
11
+ export type Node = {
12
+ type: string;
13
+ props: PropsDict;
14
+ children: Node[];
15
+ };
16
+
17
+ export type H = (type: string, props?: PropsDict, ...children: Node[]) => Node;
18
+
19
+ export type Component = (...args: any[]) => any;
20
+
21
+ /**
22
+ * Configuration options for evaluating JSX expressions.
23
+ *
24
+ * @template T - The return type of createElement function
25
+ */
26
+ export interface EvaluateOptions<T = any> {
27
+ /** Data object available as `data.*` in JSX expressions */
28
+ data?: DataDict;
29
+ /** Map of component names to component functions used in JSX */
30
+ components?: ComponentDict;
31
+ /** Function to create JSX elements (e.g., React.createElement, h) */
32
+ createElement?: CreateElement<T>;
33
+ }
34
+
35
+ export function evaluate<T = any>(source: string, options: EvaluateOptions<T>): T {
36
+ const { data = {}, components = {}, createElement } = options;
37
+
38
+ const h = createH(components, createElement || defaultCreateElement);
39
+
40
+ try {
41
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
42
+ return new Function("h", "data", "Math", source)(h, deepFreezeData(data), Math) as T;
43
+ } catch (error: unknown) {
44
+ // We may have thrown an EvaluationError from createH()
45
+ if (error instanceof EvaluationError) {
46
+ throw error;
47
+ }
48
+
49
+ throw new EvaluationError(error instanceof Error ? error.message : "Evaluation failed");
50
+ }
51
+ }
52
+
53
+ function createH(components: ComponentDict, createElement: CreateElement): H {
54
+ return function h(type, props, ...children) {
55
+ const Component = components[type];
56
+
57
+ if (!Component) {
58
+ throw new EvaluationError(`component "${type}" is not allowed`);
59
+ }
60
+
61
+ return createElement(Component, props, ...children.flat().filter(Boolean));
62
+ };
63
+ }
64
+
65
+ function deepFreezeData(obj: DataDict): DataDict {
66
+ Object.freeze(obj);
67
+ Object.values(obj).filter(isDataDict).forEach(deepFreezeData);
68
+ return obj;
69
+ }
70
+
71
+ function isDataDict(value: any): value is DataDict {
72
+ return typeof value === "object" && value !== null;
73
+ }
74
+
75
+ function defaultCreateElement(Component: Component, props: PropsDict = {}, ...children: any[]): Node {
76
+ return {
77
+ type: Component.name,
78
+ props,
79
+ children,
80
+ };
81
+ }
@@ -0,0 +1,6 @@
1
+ export class EvaluationError extends Error {
2
+ constructor(message: string) {
3
+ super(message);
4
+ this.name = "EvaluationError";
5
+ }
6
+ }
@@ -0,0 +1,2 @@
1
+ export { evaluate, type EvaluateOptions } from "./evaluate";
2
+ export { EvaluationError } from "./evaluation-error";
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export type { Schema, PropertySchema } from "./schema";
2
+
3
+ export { ParseError } from "./parse";
4
+ export { CompilationError } from "./compile";
5
+ export { AnalysisError } from "./analyze";
6
+
7
+ export { render, type RenderOptions } from "./render";
8
+ export { validate, type ValidateOptions, type ValidateResult } from "./validate";
9
+
10
+ export { generateTypeScriptDefinitions } from "./codegen";