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.
- package/LICENSE +21 -0
- package/README.md +368 -0
- package/lib/analyze/analysis-error.d.ts.map +1 -0
- package/lib/analyze/analysis-report.d.ts +33 -0
- package/lib/analyze/analysis-report.d.ts.map +1 -0
- package/lib/analyze/analysis-report.js +194 -0
- package/lib/analyze/analysis-report.js.map +1 -0
- package/lib/analyze/analyze.d.ts.map +1 -0
- package/lib/analyze/analyze.test.d.ts.map +1 -0
- package/lib/analyze/analyze.test.js +765 -0
- package/lib/analyze/analyze.test.js.map +1 -0
- package/lib/analyze/data/analyze.d.ts.map +1 -0
- package/lib/analyze/data/data-access.d.ts.map +1 -0
- package/lib/analyze/data/index.d.ts.map +1 -0
- package/lib/analyze/data/method-calls.d.ts.map +1 -0
- package/lib/analyze/data/utils.d.ts.map +1 -0
- package/lib/analyze/index.d.ts.map +1 -0
- package/lib/analyze/jsx/analyze.d.ts.map +1 -0
- package/lib/analyze/jsx/element-attributes.d.ts.map +1 -0
- package/lib/analyze/jsx/element-attributes.js +89 -0
- package/lib/analyze/jsx/element-attributes.js.map +1 -0
- package/lib/analyze/jsx/element-children.d.ts.map +1 -0
- package/lib/analyze/jsx/element-tags.d.ts.map +1 -0
- package/lib/analyze/jsx/index.d.ts.map +1 -0
- package/lib/analyze/security/analyze.d.ts.map +1 -0
- package/lib/analyze/security/call-expressions.d.ts.map +1 -0
- package/lib/analyze/security/declarations.d.ts.map +1 -0
- package/lib/analyze/security/expressions.d.ts.map +1 -0
- package/lib/analyze/security/identifiers.d.ts.map +1 -0
- package/lib/analyze/security/index.d.ts.map +1 -0
- package/lib/analyze/security/member-access.d.ts.map +1 -0
- package/lib/builtins.d.ts +50 -0
- package/lib/builtins.d.ts.map +1 -0
- package/lib/builtins.js +1065 -0
- package/lib/builtins.js.map +1 -0
- package/lib/codegen/builtin-types.d.ts +11 -0
- package/lib/codegen/builtin-types.d.ts.map +1 -0
- package/lib/codegen/builtin-types.js +151 -0
- package/lib/codegen/builtin-types.js.map +1 -0
- package/lib/codegen/builtins/builtin-types.d.ts +2 -0
- package/lib/codegen/builtins/builtin-types.d.ts.map +1 -0
- package/lib/codegen/builtins/builtin-types.js +143 -0
- package/lib/codegen/builtins/builtin-types.js.map +1 -0
- package/lib/codegen/builtins/index.d.ts +2 -0
- package/lib/codegen/builtins/index.d.ts.map +1 -0
- package/lib/codegen/builtins/index.js +6 -0
- package/lib/codegen/builtins/index.js.map +1 -0
- package/lib/codegen/declare-types.d.ts +20 -0
- package/lib/codegen/declare-types.d.ts.map +1 -0
- package/lib/codegen/declare-types.js +299 -0
- package/lib/codegen/declare-types.js.map +1 -0
- package/lib/codegen/generate-typescript-definitions.d.ts +31 -0
- package/lib/codegen/generate-typescript-definitions.d.ts.map +1 -0
- package/lib/codegen/generate-typescript-definitions.js +37 -0
- package/lib/codegen/generate-typescript-definitions.js.map +1 -0
- package/lib/codegen/generate-typescript-definitions.test.d.ts +2 -0
- package/lib/codegen/generate-typescript-definitions.test.d.ts.map +1 -0
- package/lib/codegen/generate-typescript-definitions.test.js +587 -0
- package/lib/codegen/generate-typescript-definitions.test.js.map +1 -0
- package/lib/codegen/index.d.ts +2 -0
- package/lib/codegen/index.d.ts.map +1 -0
- package/lib/codegen/index.js +6 -0
- package/lib/codegen/index.js.map +1 -0
- package/lib/codegen/schema/component-types.d.ts +3 -0
- package/lib/codegen/schema/component-types.d.ts.map +1 -0
- package/lib/codegen/schema/component-types.js +118 -0
- package/lib/codegen/schema/component-types.js.map +1 -0
- package/lib/codegen/schema/data-types.d.ts +3 -0
- package/lib/codegen/schema/data-types.d.ts.map +1 -0
- package/lib/codegen/schema/data-types.js +98 -0
- package/lib/codegen/schema/data-types.js.map +1 -0
- package/lib/codegen/schema/index.d.ts +3 -0
- package/lib/codegen/schema/index.d.ts.map +1 -0
- package/lib/codegen/schema/index.js +8 -0
- package/lib/codegen/schema/index.js.map +1 -0
- package/lib/codegen/schema/utils.d.ts +4 -0
- package/lib/codegen/schema/utils.d.ts.map +1 -0
- package/lib/codegen/schema/utils.js +103 -0
- package/lib/codegen/schema/utils.js.map +1 -0
- package/lib/codegen/schema-to-types.d.ts +14 -0
- package/lib/codegen/schema-to-types.d.ts.map +1 -0
- package/lib/codegen/schema-to-types.js +203 -0
- package/lib/codegen/schema-to-types.js.map +1 -0
- package/lib/compile/compile.d.ts.map +1 -0
- package/lib/compile/compile.js +245 -0
- package/lib/compile/compile.js.map +1 -0
- package/lib/compile/compile.test.d.ts.map +1 -0
- package/lib/compile/compile.test.js +148 -0
- package/lib/compile/compile.test.js.map +1 -0
- package/lib/evaluate/evaluate.d.ts +26 -0
- package/lib/evaluate/evaluate.d.ts.map +1 -0
- package/lib/evaluate/evaluate.js +40 -0
- package/lib/evaluate/evaluation-error.d.ts +4 -0
- package/lib/evaluate/evaluation-error.d.ts.map +1 -0
- package/lib/evaluate/evaluation-error.js +6 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +16 -0
- package/lib/index.js.map +1 -0
- package/lib/render.d.ts +58 -0
- package/lib/render.d.ts.map +1 -0
- package/lib/render.js +52 -0
- package/lib/render.test.d.ts.map +1 -0
- package/lib/schema.d.ts +68 -0
- package/lib/schema.d.ts.map +1 -0
- package/lib/schema.js +106 -0
- package/lib/validate.d.ts +41 -0
- package/lib/validate.d.ts.map +1 -0
- package/lib/validate.js +42 -0
- package/package.json +27 -0
- package/src/analyze/analysis-error.ts +26 -0
- package/src/analyze/analysis-report.ts +228 -0
- package/src/analyze/analyze.test.ts +829 -0
- package/src/analyze/analyze.ts +10 -0
- package/src/analyze/data/analyze.ts +15 -0
- package/src/analyze/data/data-access.ts +349 -0
- package/src/analyze/data/index.ts +1 -0
- package/src/analyze/data/method-calls.ts +96 -0
- package/src/analyze/data/utils.ts +329 -0
- package/src/analyze/index.ts +3 -0
- package/src/analyze/jsx/analyze.ts +17 -0
- package/src/analyze/jsx/element-attributes.ts +143 -0
- package/src/analyze/jsx/element-children.ts +97 -0
- package/src/analyze/jsx/element-tags.ts +55 -0
- package/src/analyze/jsx/index.ts +1 -0
- package/src/analyze/security/analyze.ts +21 -0
- package/src/analyze/security/call-expressions.ts +41 -0
- package/src/analyze/security/declarations.ts +67 -0
- package/src/analyze/security/expressions.ts +100 -0
- package/src/analyze/security/identifiers.ts +113 -0
- package/src/analyze/security/index.ts +1 -0
- package/src/analyze/security/member-access.ts +35 -0
- package/src/analyze/utils.ts +118 -0
- package/src/analyze/validation-context.ts +32 -0
- package/src/builtins.ts +1149 -0
- package/src/codegen/builtins/builtin-types.ts +174 -0
- package/src/codegen/builtins/index.ts +1 -0
- package/src/codegen/generate-typescript-definitions.test.ts +676 -0
- package/src/codegen/generate-typescript-definitions.ts +35 -0
- package/src/codegen/index.ts +1 -0
- package/src/codegen/schema/component-types.ts +140 -0
- package/src/codegen/schema/data-types.ts +109 -0
- package/src/codegen/schema/index.ts +2 -0
- package/src/codegen/schema/utils.ts +111 -0
- package/src/compile/compilation-error.ts +37 -0
- package/src/compile/compile.test.ts +180 -0
- package/src/compile/compile.ts +282 -0
- package/src/compile/index.ts +2 -0
- package/src/evaluate/evaluate.test.ts +316 -0
- package/src/evaluate/evaluate.ts +81 -0
- package/src/evaluate/evaluation-error.ts +6 -0
- package/src/evaluate/index.ts +2 -0
- package/src/index.ts +10 -0
- package/src/jsx.ts +161 -0
- package/src/parse/index.ts +2 -0
- package/src/parse/parse-error.ts +14 -0
- package/src/parse/parse.test.ts +99 -0
- package/src/parse/parse.ts +40 -0
- package/src/render.test.ts +425 -0
- package/src/render.ts +75 -0
- package/src/schema.ts +195 -0
- package/src/traverse.test.ts +104 -0
- package/src/traverse.ts +78 -0
- package/src/types.ts +17 -0
- package/src/validate.ts +59 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render } from "./render";
|
|
3
|
+
import { ParseError } from "./parse";
|
|
4
|
+
import { AnalysisError } from "./analyze";
|
|
5
|
+
import { EvaluationError } from "./evaluate";
|
|
6
|
+
|
|
7
|
+
type TestNode = {
|
|
8
|
+
type: string;
|
|
9
|
+
props?: Record<string, unknown>;
|
|
10
|
+
children: unknown[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe("render", () => {
|
|
14
|
+
it("should render complete JSX pipeline successfully", () => {
|
|
15
|
+
const TestComponent = (props: { x: number; y: number }, ...children: any[]): TestNode => ({
|
|
16
|
+
type: "TestComponent",
|
|
17
|
+
props,
|
|
18
|
+
children,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const result = render(
|
|
22
|
+
"<Test x={data.x} y={Math.floor(data.y)}>Hello</Test>",
|
|
23
|
+
{
|
|
24
|
+
data: {
|
|
25
|
+
x: { type: "number" },
|
|
26
|
+
y: { type: "number" },
|
|
27
|
+
},
|
|
28
|
+
elements: {
|
|
29
|
+
Test: {
|
|
30
|
+
props: {
|
|
31
|
+
x: { type: "number" },
|
|
32
|
+
y: { type: "number" },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
data: { x: 10, y: 3.7 },
|
|
39
|
+
components: { Test: TestComponent },
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(result.type).toBe("TestComponent");
|
|
44
|
+
expect(result.props).toEqual({ x: 10, y: 3 });
|
|
45
|
+
expect(result.children).toEqual(["Hello"]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should render complex expressions end-to-end", () => {
|
|
49
|
+
const TestComponent = (props: { values: string }): TestNode => ({
|
|
50
|
+
type: "TestComponent",
|
|
51
|
+
props,
|
|
52
|
+
children: [],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const result = render(
|
|
56
|
+
"<Test values={data.items.filter(x => x > 2).map(x => x * 2).join(', ')}>Text</Test>",
|
|
57
|
+
{
|
|
58
|
+
data: {
|
|
59
|
+
items: { type: "array", shape: { type: "number" } },
|
|
60
|
+
},
|
|
61
|
+
elements: {
|
|
62
|
+
Test: {
|
|
63
|
+
props: {
|
|
64
|
+
values: { type: "string" },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
data: { items: [1, 2, 3, 4, 5] },
|
|
71
|
+
components: { Test: TestComponent },
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(result.props.values).toBe("6, 8, 10");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should render nested JSX elements", () => {
|
|
79
|
+
const OuterComponent = (props: { x: number }, ...children: any[]): TestNode => ({
|
|
80
|
+
type: "OuterComponent",
|
|
81
|
+
props,
|
|
82
|
+
children,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const InnerComponent = (props: { y: number }): TestNode => ({
|
|
86
|
+
type: "InnerComponent",
|
|
87
|
+
props,
|
|
88
|
+
children: [],
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const result = render(
|
|
92
|
+
"<Outer x={10}><Inner y={20}>Nested</Inner></Outer>",
|
|
93
|
+
{
|
|
94
|
+
elements: {
|
|
95
|
+
Outer: {
|
|
96
|
+
props: {
|
|
97
|
+
x: { type: "number" },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
Inner: {
|
|
101
|
+
props: {
|
|
102
|
+
y: { type: "number" },
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
data: {},
|
|
109
|
+
components: {
|
|
110
|
+
Outer: OuterComponent,
|
|
111
|
+
Inner: InnerComponent,
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(result.type).toBe("OuterComponent");
|
|
117
|
+
expect(result.props).toEqual({ x: 10 });
|
|
118
|
+
expect(result.children).toHaveLength(1);
|
|
119
|
+
expect(result.children[0].type).toBe("InnerComponent");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should render JSX fragments", () => {
|
|
123
|
+
const TestComponent = (): TestNode => ({
|
|
124
|
+
type: "Test",
|
|
125
|
+
props: {},
|
|
126
|
+
children: [],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const result = render(
|
|
130
|
+
"<><Test>A</Test><Test>B</Test></>",
|
|
131
|
+
{
|
|
132
|
+
elements: {
|
|
133
|
+
Test: {
|
|
134
|
+
props: {},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
data: {},
|
|
140
|
+
components: { Test: TestComponent },
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
expect(result).toBeDefined();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should render bare expression containers", () => {
|
|
148
|
+
const result = render(
|
|
149
|
+
"{data.user.name}",
|
|
150
|
+
{
|
|
151
|
+
data: {
|
|
152
|
+
user: {
|
|
153
|
+
type: "object",
|
|
154
|
+
shape: {
|
|
155
|
+
name: { type: "string" },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
elements: {},
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
data: { user: { name: "Alice" } },
|
|
163
|
+
components: {},
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
expect(result).toBe("Alice");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("should render bare mathematical expressions", () => {
|
|
171
|
+
const result = render(
|
|
172
|
+
"{data.count * 2}",
|
|
173
|
+
{
|
|
174
|
+
data: {
|
|
175
|
+
count: { type: "number" },
|
|
176
|
+
},
|
|
177
|
+
elements: {},
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
data: { count: 42 },
|
|
181
|
+
components: {},
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
expect(result).toBe(84);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should render bare method calls", () => {
|
|
189
|
+
const result = render(
|
|
190
|
+
"{data.text.toUpperCase()}",
|
|
191
|
+
{
|
|
192
|
+
data: {
|
|
193
|
+
text: { type: "string" },
|
|
194
|
+
},
|
|
195
|
+
elements: {},
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
data: { text: "hello world" },
|
|
199
|
+
components: {},
|
|
200
|
+
}
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
expect(result).toBe("HELLO WORLD");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("should render bare complex expressions", () => {
|
|
207
|
+
const result = render(
|
|
208
|
+
"{data.numbers.map(x => x * 2).join(', ')}",
|
|
209
|
+
{
|
|
210
|
+
data: {
|
|
211
|
+
numbers: {
|
|
212
|
+
type: "array",
|
|
213
|
+
shape: { type: "number" },
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
elements: {},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
data: { numbers: [1, 2, 3, 4] },
|
|
220
|
+
components: {},
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
expect(result).toBe("2, 4, 6, 8");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("should propagate ParseError for syntax errors", () => {
|
|
228
|
+
const TestComponent = (): TestNode => ({ type: "Test", props: {}, children: [] });
|
|
229
|
+
|
|
230
|
+
expect(() =>
|
|
231
|
+
render(
|
|
232
|
+
"<Text>Unclosed",
|
|
233
|
+
{
|
|
234
|
+
elements: {
|
|
235
|
+
Text: {
|
|
236
|
+
props: {},
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
components: { Text: TestComponent },
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
).toThrow(ParseError);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("should propagate AnalysisError for disallowed identifiers", () => {
|
|
248
|
+
const TestComponent = (): TestNode => ({ type: "Test", props: {}, children: [] });
|
|
249
|
+
|
|
250
|
+
expect(() =>
|
|
251
|
+
render(
|
|
252
|
+
"<Text>{window.location}</Text>",
|
|
253
|
+
{
|
|
254
|
+
elements: {
|
|
255
|
+
Text: {
|
|
256
|
+
props: {},
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
components: { Text: TestComponent },
|
|
262
|
+
}
|
|
263
|
+
)
|
|
264
|
+
).toThrow(AnalysisError);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("should propagate AnalysisError for unknown JSX elements", () => {
|
|
268
|
+
const TestComponent = (): TestNode => ({ type: "Test", props: {}, children: [] });
|
|
269
|
+
|
|
270
|
+
expect(() =>
|
|
271
|
+
render(
|
|
272
|
+
"<UnknownElement>Hello</UnknownElement>",
|
|
273
|
+
{
|
|
274
|
+
elements: {
|
|
275
|
+
Text: {
|
|
276
|
+
props: {},
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
components: { Text: TestComponent },
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
).toThrow(AnalysisError);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("should propagate AnalysisError for disallowed methods", () => {
|
|
288
|
+
const TestComponent = (): TestNode => ({ type: "Test", props: {}, children: [] });
|
|
289
|
+
|
|
290
|
+
expect(() =>
|
|
291
|
+
render(
|
|
292
|
+
"<Test>{data.items.push(1)}</Test>",
|
|
293
|
+
{
|
|
294
|
+
data: {
|
|
295
|
+
items: {
|
|
296
|
+
type: "array",
|
|
297
|
+
shape: { type: "number" },
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
elements: {
|
|
301
|
+
Test: {
|
|
302
|
+
props: {},
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
data: { items: [1, 2, 3] },
|
|
308
|
+
components: { Test: TestComponent },
|
|
309
|
+
}
|
|
310
|
+
)
|
|
311
|
+
).toThrow(AnalysisError);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("should propagate AnalysisError for unknown components", () => {
|
|
315
|
+
const TestComponent = (): TestNode => ({ type: "Test", props: {}, children: [] });
|
|
316
|
+
|
|
317
|
+
expect(() =>
|
|
318
|
+
render(
|
|
319
|
+
"<Test>Hello</Test>",
|
|
320
|
+
{
|
|
321
|
+
elements: {
|
|
322
|
+
Test: {
|
|
323
|
+
props: {},
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
data: {},
|
|
329
|
+
components: { Test: TestComponent },
|
|
330
|
+
}
|
|
331
|
+
)
|
|
332
|
+
).not.toThrow();
|
|
333
|
+
|
|
334
|
+
expect(() =>
|
|
335
|
+
render("<Other>Hello</Other>", {
|
|
336
|
+
elements: {},
|
|
337
|
+
})
|
|
338
|
+
).toThrow(AnalysisError);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("should propagate EvaluationError for runtime errors", () => {
|
|
342
|
+
const TestComponent = (): TestNode => ({ type: "Test", props: {}, children: [] });
|
|
343
|
+
|
|
344
|
+
expect(() =>
|
|
345
|
+
render(
|
|
346
|
+
"<Test value={data.missing.nested}>Hello</Test>",
|
|
347
|
+
{
|
|
348
|
+
data: {
|
|
349
|
+
missing: {
|
|
350
|
+
type: "object",
|
|
351
|
+
shape: {
|
|
352
|
+
nested: { type: "number" },
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
elements: {
|
|
357
|
+
Test: {
|
|
358
|
+
props: {
|
|
359
|
+
value: { type: "number" },
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
data: {},
|
|
366
|
+
components: { Test: TestComponent },
|
|
367
|
+
}
|
|
368
|
+
)
|
|
369
|
+
).toThrow(EvaluationError);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("should handle empty options", () => {
|
|
373
|
+
expect(() => render("<Test>Hello</Test>", { elements: {} })).toThrow(AnalysisError);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("should handle empty props", () => {
|
|
377
|
+
const TestComponent = (): TestNode => ({ type: "TestComponent", props: {}, children: [] });
|
|
378
|
+
|
|
379
|
+
const result = render(
|
|
380
|
+
"<Test>Hello</Test>",
|
|
381
|
+
{
|
|
382
|
+
elements: {
|
|
383
|
+
Test: {
|
|
384
|
+
props: {},
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
components: { Test: TestComponent },
|
|
390
|
+
}
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
expect(result.type).toBe("TestComponent");
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("should handle conditional rendering with props", () => {
|
|
397
|
+
const TestComponent = (props: { msg: string }): TestNode => ({
|
|
398
|
+
type: "Test",
|
|
399
|
+
props,
|
|
400
|
+
children: [],
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const result = render(
|
|
404
|
+
"<Test msg={data.show ? 'visible' : 'hidden'}>Text</Test>",
|
|
405
|
+
{
|
|
406
|
+
data: {
|
|
407
|
+
show: { type: "boolean" },
|
|
408
|
+
},
|
|
409
|
+
elements: {
|
|
410
|
+
Test: {
|
|
411
|
+
props: {
|
|
412
|
+
msg: { type: "string" },
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
data: { show: false },
|
|
419
|
+
components: { Test: TestComponent },
|
|
420
|
+
}
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
expect(result.props!.msg).toBe("hidden");
|
|
424
|
+
});
|
|
425
|
+
});
|
package/src/render.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { compile } from "./compile";
|
|
2
|
+
import { evaluate, EvaluateOptions } from "./evaluate/evaluate";
|
|
3
|
+
import { parse } from "./parse";
|
|
4
|
+
import { Schema } from "./schema";
|
|
5
|
+
import { analyze, AnalysisError } from "./analyze";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Configuration options for rendering JSX expressions.
|
|
9
|
+
*
|
|
10
|
+
* Extends EvaluateOptions with render-specific functionality, including control over
|
|
11
|
+
* the minimum severity threshold that stops rendering.
|
|
12
|
+
*
|
|
13
|
+
* @template T - The return type of createElement function
|
|
14
|
+
*/
|
|
15
|
+
export interface RenderOptions<T = any> extends EvaluateOptions<T> {
|
|
16
|
+
/**
|
|
17
|
+
* Minimum severity level that counts as a failure: 1=info, 2=warning, 3=error.
|
|
18
|
+
*/
|
|
19
|
+
minSeverity?: 1 | 2 | 3;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Renders a JSX expression with data validation and security constraints.
|
|
24
|
+
*
|
|
25
|
+
* Parses JSX source code, validates it against a schema for security and type safety,
|
|
26
|
+
* compiles it to executable code, and evaluates it with the provided data and components.
|
|
27
|
+
*
|
|
28
|
+
* @template T - The return type of the rendered JSX (e.g., ReactElement, VNode, string)
|
|
29
|
+
* @param source - JSX expression source code to render (e.g., `"<Text x={data.user.name}>Hello</Text>"`)
|
|
30
|
+
* @param schema - Schema defining allowed data properties, JSX elements, and hierarchy rules
|
|
31
|
+
* @param options - Rendering configuration
|
|
32
|
+
* @param options.data - Data object available as `data.*` in expressions
|
|
33
|
+
* @param options.components - Map of component names to component functions
|
|
34
|
+
* @param options.createElement - Function to create elements (e.g., React.createElement)
|
|
35
|
+
* @param options.minSeverity - Minimum severity (1=info, 2=warning, 3=error) required to abort rendering; defaults to `3`
|
|
36
|
+
* @returns The rendered JSX element of type T
|
|
37
|
+
*
|
|
38
|
+
* @throws {AnalysisError} When JSX contains security violations, invalid data access, or schema mismatches
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* const schema: Schema = {
|
|
43
|
+
* data: {
|
|
44
|
+
* user: { type: "object", shape: { name: { type: "string" } } }
|
|
45
|
+
* },
|
|
46
|
+
* elements: {
|
|
47
|
+
* Text: { props: { children: { type: "string" } } }
|
|
48
|
+
* }
|
|
49
|
+
* };
|
|
50
|
+
*
|
|
51
|
+
* const result = render(
|
|
52
|
+
* "<Text>Hello {data.user.name}!</Text>",
|
|
53
|
+
* schema,
|
|
54
|
+
* {
|
|
55
|
+
* data: { user: { name: "World" } },
|
|
56
|
+
* components: { Text: ({ children }) => children },
|
|
57
|
+
* createElement: (type, props, ...children) => ({ type, props, children })
|
|
58
|
+
* }
|
|
59
|
+
* );
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function render<T = any>(
|
|
63
|
+
source: string,
|
|
64
|
+
schema: Schema,
|
|
65
|
+
{ minSeverity = 3, ...options }: RenderOptions<T> = {}
|
|
66
|
+
): T {
|
|
67
|
+
const ast = parse(source);
|
|
68
|
+
const report = analyze(ast, schema);
|
|
69
|
+
|
|
70
|
+
if (report.hasIssues(minSeverity)) {
|
|
71
|
+
throw AnalysisError.fromReport(report);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return evaluate(compile(ast), options) as T;
|
|
75
|
+
}
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
export type StringPropertySchema = {
|
|
2
|
+
type: "string";
|
|
3
|
+
description?: string;
|
|
4
|
+
required?: boolean;
|
|
5
|
+
default?: string;
|
|
6
|
+
enum?: readonly string[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type NumberPropertySchema = {
|
|
10
|
+
type: "number";
|
|
11
|
+
description?: string;
|
|
12
|
+
required?: boolean;
|
|
13
|
+
default?: number;
|
|
14
|
+
enum?: readonly number[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type BooleanPropertySchema = {
|
|
18
|
+
type: "boolean";
|
|
19
|
+
description?: string;
|
|
20
|
+
required?: boolean;
|
|
21
|
+
default?: boolean;
|
|
22
|
+
enum?: readonly boolean[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type FunctionPropertySchema = {
|
|
26
|
+
type: "function";
|
|
27
|
+
description?: string;
|
|
28
|
+
required?: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type ObjectPropertySchema = {
|
|
32
|
+
type: "object";
|
|
33
|
+
description?: string;
|
|
34
|
+
required?: boolean;
|
|
35
|
+
shape: Record<string, PropertySchema>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type ArrayPropertySchema = {
|
|
39
|
+
type: "array";
|
|
40
|
+
description?: string;
|
|
41
|
+
required?: boolean;
|
|
42
|
+
shape: PropertySchema;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type PropertySchema =
|
|
46
|
+
| StringPropertySchema
|
|
47
|
+
| NumberPropertySchema
|
|
48
|
+
| BooleanPropertySchema
|
|
49
|
+
| FunctionPropertySchema
|
|
50
|
+
| ObjectPropertySchema
|
|
51
|
+
| ArrayPropertySchema;
|
|
52
|
+
|
|
53
|
+
export type ElementSchema = {
|
|
54
|
+
description?: string;
|
|
55
|
+
props?: Record<string, PropertySchema>;
|
|
56
|
+
allowedChildren?: string[];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export interface Schema {
|
|
60
|
+
data?: Record<string, PropertySchema>;
|
|
61
|
+
elements?: Record<string, ElementSchema>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function isElementAllowed(schema: Schema, tagName: string): boolean {
|
|
65
|
+
return !!schema.elements?.[tagName];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getElementSchema(schema: Schema, tagName: string): ElementSchema | undefined {
|
|
69
|
+
return schema.elements?.[tagName];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getAllowedAttributes(schema: Schema, tagName: string): string[] {
|
|
73
|
+
const elementConfig = getElementSchema(schema, tagName);
|
|
74
|
+
return elementConfig?.props ? Object.keys(elementConfig.props) : [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function isAttributeAllowed(schema: Schema, tagName: string, attributeName: string): boolean {
|
|
78
|
+
const allowedAttributes = getAllowedAttributes(schema, tagName);
|
|
79
|
+
return allowedAttributes.includes(attributeName);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getAllowedChildren(schema: Schema, tagName: string): string[] | undefined {
|
|
83
|
+
const elementConfig = getElementSchema(schema, tagName);
|
|
84
|
+
return elementConfig?.allowedChildren;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function canHaveChildren(schema: Schema, tagName: string): boolean {
|
|
88
|
+
const allowedChildren = getAllowedChildren(schema, tagName);
|
|
89
|
+
return allowedChildren === undefined || allowedChildren.length > 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function isSelfClosing(schema: Schema, tagName: string): boolean {
|
|
93
|
+
const allowedChildren = getAllowedChildren(schema, tagName);
|
|
94
|
+
return allowedChildren !== undefined && allowedChildren.length === 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function isChildAllowed(schema: Schema, parentTag: string, childTag: string): boolean {
|
|
98
|
+
const allowedChildren = getAllowedChildren(schema, parentTag);
|
|
99
|
+
return allowedChildren === undefined || allowedChildren.includes(childTag);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// New schema utilities for enhanced validation
|
|
103
|
+
export function getAllElements(schema: Schema): string[] {
|
|
104
|
+
return schema.elements ? Object.keys(schema.elements) : [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function getElementConfig(schema: Schema, tagName: string): ElementSchema | undefined {
|
|
108
|
+
return getElementSchema(schema, tagName);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getAttributeSchema(schema: Schema, tagName: string, attributeName: string): PropertySchema | undefined {
|
|
112
|
+
const elementSchema = getElementSchema(schema, tagName);
|
|
113
|
+
return elementSchema?.props?.[attributeName];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getRequiredAttributes(schema: Schema, tagName: string): string[] {
|
|
117
|
+
const elementSchema = getElementSchema(schema, tagName);
|
|
118
|
+
if (!elementSchema?.props) {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const required: string[] = [];
|
|
123
|
+
for (const [attrName, attrSchema] of Object.entries(elementSchema.props)) {
|
|
124
|
+
if (attrSchema.required === true) {
|
|
125
|
+
required.push(attrName);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return required;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function getOptionalAttributes(schema: Schema, tagName: string): string[] {
|
|
133
|
+
const allAttributes = getAllowedAttributes(schema, tagName);
|
|
134
|
+
const requiredAttributes = getRequiredAttributes(schema, tagName);
|
|
135
|
+
return allAttributes.filter((attr) => !requiredAttributes.includes(attr));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function isAttributeRequired(schema: Schema, tagName: string, attributeName: string): boolean {
|
|
139
|
+
const requiredAttributes = getRequiredAttributes(schema, tagName);
|
|
140
|
+
return requiredAttributes.includes(attributeName);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function getDataSchema(schema: Schema): Record<string, PropertySchema> | undefined {
|
|
144
|
+
return schema.data;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function getPropertyAtPath(schema: Schema, path: string[]): PropertySchema | undefined {
|
|
148
|
+
if (!schema.data || path.length === 0) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let current: PropertySchema | undefined = schema.data[path[0]];
|
|
153
|
+
|
|
154
|
+
for (let i = 1; i < path.length && current; i++) {
|
|
155
|
+
if (current.type === "object" && current.shape) {
|
|
156
|
+
current = current.shape[path[i]];
|
|
157
|
+
} else {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return current;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function getAvailablePropertiesAtPath(schema: Schema, path: string[]): string[] {
|
|
166
|
+
if (!schema.data) {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (path.length === 0) {
|
|
171
|
+
return Object.keys(schema.data);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const parentProperty = getPropertyAtPath(schema, path);
|
|
175
|
+
if (parentProperty?.type === "object" && parentProperty.shape) {
|
|
176
|
+
return Object.keys(parentProperty.shape);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export type PropertyEnumValue = string | number | boolean;
|
|
183
|
+
|
|
184
|
+
export function getEnumValues(property: PropertySchema): readonly PropertyEnumValue[] | undefined {
|
|
185
|
+
switch (property.type) {
|
|
186
|
+
case "string":
|
|
187
|
+
return property.enum;
|
|
188
|
+
case "number":
|
|
189
|
+
return property.enum;
|
|
190
|
+
case "boolean":
|
|
191
|
+
return property.enum;
|
|
192
|
+
default:
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
}
|