jsxpression 0.1.28 → 0.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -188
- package/lib/analyze/analysis-report.d.ts +2 -2
- package/lib/analyze/analysis-report.d.ts.map +1 -1
- package/lib/analyze/analysis-report.js +4 -0
- package/lib/analyze/analysis-report.js.map +1 -1
- package/lib/analyze/data/data-access.d.ts.map +1 -1
- package/lib/analyze/data/data-access.js +45 -82
- package/lib/analyze/data/data-access.js.map +1 -1
- package/lib/analyze/data/method-calls.d.ts.map +1 -1
- package/lib/analyze/data/method-calls.js +4 -3
- package/lib/analyze/data/method-calls.js.map +1 -1
- package/lib/analyze/data/utils.d.ts +2 -3
- package/lib/analyze/data/utils.d.ts.map +1 -1
- package/lib/analyze/data/utils.js +10 -36
- package/lib/analyze/data/utils.js.map +1 -1
- package/lib/analyze/jsx/element-attributes.d.ts +1 -1
- package/lib/analyze/jsx/element-attributes.d.ts.map +1 -1
- package/lib/analyze/jsx/element-attributes.js +13 -3
- package/lib/analyze/jsx/element-attributes.js.map +1 -1
- package/lib/analyze/jsx/element-children.d.ts +1 -1
- package/lib/analyze/jsx/element-children.d.ts.map +1 -1
- package/lib/analyze/jsx/element-children.js +16 -5
- package/lib/analyze/jsx/element-children.js.map +1 -1
- package/lib/analyze/jsx/element-tags.d.ts +1 -1
- package/lib/analyze/jsx/element-tags.d.ts.map +1 -1
- package/lib/analyze/jsx/element-tags.js +12 -2
- package/lib/analyze/jsx/element-tags.js.map +1 -1
- package/lib/analyze/security/analyze.d.ts.map +1 -1
- package/lib/analyze/security/analyze.js +2 -4
- package/lib/analyze/security/analyze.js.map +1 -1
- package/lib/analyze/security/call-expressions.d.ts +2 -2
- package/lib/analyze/security/call-expressions.d.ts.map +1 -1
- package/lib/analyze/security/call-expressions.js +21 -5
- package/lib/analyze/security/call-expressions.js.map +1 -1
- package/lib/analyze/security/declarations.d.ts +1 -1
- package/lib/analyze/security/declarations.d.ts.map +1 -1
- package/lib/analyze/security/declarations.js +22 -2
- package/lib/analyze/security/declarations.js.map +1 -1
- package/lib/analyze/security/expressions.d.ts.map +1 -1
- package/lib/analyze/security/expressions.js +10 -2
- package/lib/analyze/security/expressions.js.map +1 -1
- package/lib/analyze/security/identifiers.d.ts +2 -2
- package/lib/analyze/security/identifiers.d.ts.map +1 -1
- package/lib/analyze/security/identifiers.js +159 -19
- package/lib/analyze/security/identifiers.js.map +1 -1
- package/lib/builtins.d.ts.map +1 -1
- package/lib/builtins.js +5 -0
- package/lib/builtins.js.map +1 -1
- package/lib/codegen/builtins/builtin-types.d.ts.map +1 -1
- package/lib/codegen/builtins/builtin-types.js +7 -1
- package/lib/codegen/builtins/builtin-types.js.map +1 -1
- package/lib/compile/compile.d.ts.map +1 -1
- package/lib/compile/compile.js +131 -61
- package/lib/compile/compile.js.map +1 -1
- package/lib/evaluate/evaluate.d.ts +3 -3
- package/lib/evaluate/evaluate.d.ts.map +1 -1
- package/lib/evaluate/evaluate.js +28 -7
- package/lib/evaluate/evaluate.js.map +1 -1
- package/lib/jsx.d.ts +2 -19
- package/lib/jsx.d.ts.map +1 -1
- package/lib/jsx.js +0 -6
- package/lib/jsx.js.map +1 -1
- package/lib/parse/parse.d.ts.map +1 -1
- package/lib/parse/parse.js +4 -3
- package/lib/parse/parse.js.map +1 -1
- package/lib/render.d.ts +4 -4
- package/lib/render.d.ts.map +1 -1
- package/lib/render.js +7 -9
- package/lib/render.js.map +1 -1
- package/lib/schema.d.ts +2 -7
- package/lib/schema.d.ts.map +1 -1
- package/lib/schema.js +0 -43
- package/lib/schema.js.map +1 -1
- package/lib/traverse.d.ts +4 -4
- package/lib/traverse.d.ts.map +1 -1
- package/lib/traverse.js +118 -40
- package/lib/traverse.js.map +1 -1
- package/lib/validate.d.ts +1 -1
- package/lib/validate.js +1 -1
- package/package.json +4 -3
- package/src/analyze/analysis-report.ts +6 -2
- package/src/analyze/analyze.test.ts +108 -69
- package/src/analyze/data/data-access.ts +45 -90
- package/src/analyze/data/method-calls.ts +4 -3
- package/src/analyze/data/utils.ts +11 -40
- package/src/analyze/jsx/element-attributes.ts +24 -9
- package/src/analyze/jsx/element-children.ts +20 -6
- package/src/analyze/jsx/element-tags.ts +15 -3
- package/src/analyze/security/analyze.ts +1 -3
- package/src/analyze/security/call-expressions.ts +37 -17
- package/src/analyze/security/declarations.ts +35 -9
- package/src/analyze/security/expressions.ts +17 -3
- package/src/analyze/security/identifiers.ts +172 -28
- package/src/builtins.ts +6 -0
- package/src/codegen/builtins/builtin-types.ts +7 -1
- package/src/codegen/generate-typescript-definitions.test.ts +16 -23
- package/src/compile/compile.test.ts +153 -30
- package/src/compile/compile.ts +163 -73
- package/src/evaluate/evaluate.test.ts +276 -14
- package/src/evaluate/evaluate.ts +34 -10
- package/src/jsx.ts +2 -31
- package/src/parse/parse.test.ts +4 -4
- package/src/parse/parse.ts +4 -3
- package/src/render.test.ts +476 -27
- package/src/render.ts +7 -14
- package/src/schema.ts +1 -55
- package/src/traverse.ts +135 -49
- package/src/validate.ts +1 -1
- package/LICENSE +0 -21
- package/lib/analyze/analyze.test.d.ts +0 -2
- package/lib/analyze/analyze.test.d.ts.map +0 -1
- package/lib/analyze/analyze.test.js +0 -763
- package/lib/analyze/analyze.test.js.map +0 -1
- package/lib/analyze/security/member-access.d.ts +0 -6
- package/lib/codegen/generate-typescript-definitions.test.d.ts +0 -2
- package/lib/codegen/generate-typescript-definitions.test.d.ts.map +0 -1
- package/lib/codegen/generate-typescript-definitions.test.js +0 -649
- package/lib/codegen/generate-typescript-definitions.test.js.map +0 -1
- package/lib/compile/compile.test.d.ts +0 -2
- package/lib/compile/compile.test.d.ts.map +0 -1
- package/lib/compile/compile.test.js +0 -146
- package/lib/compile/compile.test.js.map +0 -1
- package/lib/evaluate/evaluate.test.d.ts +0 -2
- package/lib/evaluate/evaluate.test.d.ts.map +0 -1
- package/lib/evaluate/evaluate.test.js +0 -260
- package/lib/evaluate/evaluate.test.js.map +0 -1
- package/lib/parse/parse.test.d.ts +0 -2
- package/lib/parse/parse.test.d.ts.map +0 -1
- package/lib/parse/parse.test.js +0 -80
- package/lib/parse/parse.test.js.map +0 -1
- package/lib/render.test.d.ts +0 -2
- package/lib/render.test.d.ts.map +0 -1
- package/lib/render.test.js +0 -301
- package/lib/render.test.js.map +0 -1
- package/lib/traverse.test.d.ts +0 -2
- package/lib/traverse.test.d.ts.map +0 -1
- package/lib/traverse.test.js +0 -84
- package/lib/traverse.test.js.map +0 -1
- package/lib/types.d.ts +0 -12
- package/src/analyze/security/member-access.ts +0 -35
- package/src/types.ts +0 -17
package/src/render.test.ts
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import { describe,
|
|
2
|
-
import { render } from "./render.js";
|
|
3
|
-
import { ParseError } from "./parse/index.js";
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
4
2
|
import { AnalysisError } from "./analyze/index.js";
|
|
5
3
|
import { EvaluationError } from "./evaluate/index.js";
|
|
4
|
+
import { ParseError } from "./parse/index.js";
|
|
5
|
+
import { render } from "./render.js";
|
|
6
6
|
|
|
7
7
|
type TestNode = {
|
|
8
8
|
type: string;
|
|
9
|
-
props
|
|
9
|
+
props: Record<string, unknown>;
|
|
10
10
|
children: unknown[];
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
describe("render", () => {
|
|
14
14
|
it("should render complete JSX pipeline successfully", () => {
|
|
15
|
-
const TestComponent = (props: { x: number; y: number
|
|
15
|
+
const TestComponent = ({ children, ...props }: { x: number; y: number; children?: any[] }): TestNode => ({
|
|
16
16
|
type: "TestComponent",
|
|
17
17
|
props,
|
|
18
|
-
children,
|
|
18
|
+
children: children ?? [],
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
const result = render(
|
|
22
|
-
"<Test x={
|
|
22
|
+
"<Test x={x} y={Math.floor(y)}>Hello</Test>",
|
|
23
23
|
{
|
|
24
24
|
data: {
|
|
25
25
|
x: { type: "number" },
|
|
@@ -53,7 +53,7 @@ describe("render", () => {
|
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
const result = render(
|
|
56
|
-
"<Test values={
|
|
56
|
+
"<Test values={items.filter(x => x > 2).map(x => x * 2).join(', ')}>Text</Test>",
|
|
57
57
|
{
|
|
58
58
|
data: {
|
|
59
59
|
items: { type: "array", shape: { type: "number" } },
|
|
@@ -76,16 +76,16 @@ describe("render", () => {
|
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
it("should render nested JSX elements", () => {
|
|
79
|
-
const OuterComponent = (props: { x: number
|
|
79
|
+
const OuterComponent = ({ children, ...props }: { x: number; children?: any[] }): TestNode => ({
|
|
80
80
|
type: "OuterComponent",
|
|
81
81
|
props,
|
|
82
|
-
children,
|
|
82
|
+
children: children ?? [],
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
const InnerComponent = (props: { y: number }): TestNode => ({
|
|
85
|
+
const InnerComponent = ({ children, ...props }: { y: number; children?: any[] }): TestNode => ({
|
|
86
86
|
type: "InnerComponent",
|
|
87
87
|
props,
|
|
88
|
-
children: [],
|
|
88
|
+
children: children ?? [],
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
const result = render(
|
|
@@ -146,7 +146,7 @@ describe("render", () => {
|
|
|
146
146
|
|
|
147
147
|
it("should render bare expression containers", () => {
|
|
148
148
|
const result = render(
|
|
149
|
-
"{
|
|
149
|
+
"{user.name}",
|
|
150
150
|
{
|
|
151
151
|
data: {
|
|
152
152
|
user: {
|
|
@@ -169,7 +169,7 @@ describe("render", () => {
|
|
|
169
169
|
|
|
170
170
|
it("should render bare mathematical expressions", () => {
|
|
171
171
|
const result = render(
|
|
172
|
-
"{
|
|
172
|
+
"{count * 2}",
|
|
173
173
|
{
|
|
174
174
|
data: {
|
|
175
175
|
count: { type: "number" },
|
|
@@ -187,7 +187,7 @@ describe("render", () => {
|
|
|
187
187
|
|
|
188
188
|
it("should render bare method calls", () => {
|
|
189
189
|
const result = render(
|
|
190
|
-
"{
|
|
190
|
+
"{text.toUpperCase()}",
|
|
191
191
|
{
|
|
192
192
|
data: {
|
|
193
193
|
text: { type: "string" },
|
|
@@ -205,7 +205,7 @@ describe("render", () => {
|
|
|
205
205
|
|
|
206
206
|
it("should render bare complex expressions", () => {
|
|
207
207
|
const result = render(
|
|
208
|
-
"{
|
|
208
|
+
"{numbers.map(x => x * 2).join(', ')}",
|
|
209
209
|
{
|
|
210
210
|
data: {
|
|
211
211
|
numbers: {
|
|
@@ -225,7 +225,11 @@ describe("render", () => {
|
|
|
225
225
|
});
|
|
226
226
|
|
|
227
227
|
it("should propagate ParseError for syntax errors", () => {
|
|
228
|
-
const TestComponent = (): TestNode => ({
|
|
228
|
+
const TestComponent = (): TestNode => ({
|
|
229
|
+
type: "Test",
|
|
230
|
+
props: {},
|
|
231
|
+
children: [],
|
|
232
|
+
});
|
|
229
233
|
|
|
230
234
|
expect(() =>
|
|
231
235
|
render(
|
|
@@ -245,7 +249,11 @@ describe("render", () => {
|
|
|
245
249
|
});
|
|
246
250
|
|
|
247
251
|
it("should propagate AnalysisError for disallowed identifiers", () => {
|
|
248
|
-
const TestComponent = (): TestNode => ({
|
|
252
|
+
const TestComponent = (): TestNode => ({
|
|
253
|
+
type: "Test",
|
|
254
|
+
props: {},
|
|
255
|
+
children: [],
|
|
256
|
+
});
|
|
249
257
|
|
|
250
258
|
expect(() =>
|
|
251
259
|
render(
|
|
@@ -265,7 +273,11 @@ describe("render", () => {
|
|
|
265
273
|
});
|
|
266
274
|
|
|
267
275
|
it("should propagate AnalysisError for unknown JSX elements", () => {
|
|
268
|
-
const TestComponent = (): TestNode => ({
|
|
276
|
+
const TestComponent = (): TestNode => ({
|
|
277
|
+
type: "Test",
|
|
278
|
+
props: {},
|
|
279
|
+
children: [],
|
|
280
|
+
});
|
|
269
281
|
|
|
270
282
|
expect(() =>
|
|
271
283
|
render(
|
|
@@ -285,11 +297,15 @@ describe("render", () => {
|
|
|
285
297
|
});
|
|
286
298
|
|
|
287
299
|
it("should propagate AnalysisError for disallowed methods", () => {
|
|
288
|
-
const TestComponent = (): TestNode => ({
|
|
300
|
+
const TestComponent = (): TestNode => ({
|
|
301
|
+
type: "Test",
|
|
302
|
+
props: {},
|
|
303
|
+
children: [],
|
|
304
|
+
});
|
|
289
305
|
|
|
290
306
|
expect(() =>
|
|
291
307
|
render(
|
|
292
|
-
"<Test>{
|
|
308
|
+
"<Test>{items.push(1)}</Test>",
|
|
293
309
|
{
|
|
294
310
|
data: {
|
|
295
311
|
items: {
|
|
@@ -312,7 +328,11 @@ describe("render", () => {
|
|
|
312
328
|
});
|
|
313
329
|
|
|
314
330
|
it("should propagate AnalysisError for unknown components", () => {
|
|
315
|
-
const TestComponent = (): TestNode => ({
|
|
331
|
+
const TestComponent = (): TestNode => ({
|
|
332
|
+
type: "Test",
|
|
333
|
+
props: {},
|
|
334
|
+
children: [],
|
|
335
|
+
});
|
|
316
336
|
|
|
317
337
|
expect(() =>
|
|
318
338
|
render(
|
|
@@ -339,11 +359,15 @@ describe("render", () => {
|
|
|
339
359
|
});
|
|
340
360
|
|
|
341
361
|
it("should propagate EvaluationError for runtime errors", () => {
|
|
342
|
-
const TestComponent = (): TestNode => ({
|
|
362
|
+
const TestComponent = (): TestNode => ({
|
|
363
|
+
type: "Test",
|
|
364
|
+
props: {},
|
|
365
|
+
children: [],
|
|
366
|
+
});
|
|
343
367
|
|
|
344
368
|
expect(() =>
|
|
345
369
|
render(
|
|
346
|
-
"<Test value={
|
|
370
|
+
"<Test value={missing.nested}>Hello</Test>",
|
|
347
371
|
{
|
|
348
372
|
data: {
|
|
349
373
|
missing: {
|
|
@@ -374,7 +398,11 @@ describe("render", () => {
|
|
|
374
398
|
});
|
|
375
399
|
|
|
376
400
|
it("should handle empty props", () => {
|
|
377
|
-
const TestComponent = (): TestNode => ({
|
|
401
|
+
const TestComponent = (): TestNode => ({
|
|
402
|
+
type: "TestComponent",
|
|
403
|
+
props: {},
|
|
404
|
+
children: [],
|
|
405
|
+
});
|
|
378
406
|
|
|
379
407
|
const result = render(
|
|
380
408
|
"<Test>Hello</Test>",
|
|
@@ -401,7 +429,7 @@ describe("render", () => {
|
|
|
401
429
|
});
|
|
402
430
|
|
|
403
431
|
const result = render(
|
|
404
|
-
"<Test msg={
|
|
432
|
+
"<Test msg={show ? 'visible' : 'hidden'}>Text</Test>",
|
|
405
433
|
{
|
|
406
434
|
data: {
|
|
407
435
|
show: { type: "boolean" },
|
|
@@ -420,6 +448,427 @@ describe("render", () => {
|
|
|
420
448
|
}
|
|
421
449
|
);
|
|
422
450
|
|
|
423
|
-
expect(result.props
|
|
451
|
+
expect(result.props.msg).toBe("hidden");
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("should tolerate TypeScript syntax in templates", () => {
|
|
455
|
+
const TestComponent = ({ children, ...props }: Record<string, unknown>): TestNode => ({
|
|
456
|
+
type: "Test",
|
|
457
|
+
props,
|
|
458
|
+
children: children ? [children].flat() : [],
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const result = render(
|
|
462
|
+
"interface Nice {\n semi: true\n}\n\nreturn <Test x={10}>Hello</Test>",
|
|
463
|
+
{
|
|
464
|
+
data: {},
|
|
465
|
+
elements: {
|
|
466
|
+
Test: {
|
|
467
|
+
props: {
|
|
468
|
+
x: { type: "number" },
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
data: {},
|
|
475
|
+
components: { Test: TestComponent },
|
|
476
|
+
}
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
expect(result.type).toBe("Test");
|
|
480
|
+
expect(result.props.x).toBe(10);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
describe("const declarations end-to-end", () => {
|
|
484
|
+
it("should render with const used in prop", () => {
|
|
485
|
+
const TestComponent = ({ children, ...props }: Record<string, unknown>): TestNode => ({
|
|
486
|
+
type: "Test",
|
|
487
|
+
props,
|
|
488
|
+
children: children ? [children].flat() : [],
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
const result = render(
|
|
492
|
+
"const doubled = count * 2;\nreturn <Test value={doubled}>Text</Test>",
|
|
493
|
+
{
|
|
494
|
+
data: {
|
|
495
|
+
count: { type: "number" },
|
|
496
|
+
},
|
|
497
|
+
elements: {
|
|
498
|
+
Test: {
|
|
499
|
+
props: {
|
|
500
|
+
value: { type: "number" },
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
data: { count: 7 },
|
|
507
|
+
components: { Test: TestComponent },
|
|
508
|
+
}
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
expect(result.props.value).toBe(14);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it("should render with const used in children", () => {
|
|
515
|
+
const TestComponent = ({ children, ...props }: Record<string, unknown>): TestNode => ({
|
|
516
|
+
type: "Test",
|
|
517
|
+
props,
|
|
518
|
+
children: children ? [children].flat() : [],
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
const result = render(
|
|
522
|
+
"const upper = name.toUpperCase();\nreturn <Test>{upper}</Test>",
|
|
523
|
+
{
|
|
524
|
+
data: {
|
|
525
|
+
name: { type: "string" },
|
|
526
|
+
},
|
|
527
|
+
elements: {
|
|
528
|
+
Test: { props: {} },
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
data: { name: "alice" },
|
|
533
|
+
components: { Test: TestComponent },
|
|
534
|
+
}
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
expect(result.children).toEqual(["ALICE"]);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it("should render with multiple consts chained", () => {
|
|
541
|
+
const TestComponent = ({ children, ...props }: Record<string, unknown>): TestNode => ({
|
|
542
|
+
type: "Test",
|
|
543
|
+
props,
|
|
544
|
+
children: children ? [children].flat() : [],
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
const result = render(
|
|
548
|
+
"const filtered = items.filter(x => x > 2);\nconst total = filtered.length;\nreturn <Test count={total}>Text</Test>",
|
|
549
|
+
{
|
|
550
|
+
data: {
|
|
551
|
+
items: { type: "array", shape: { type: "number" } },
|
|
552
|
+
},
|
|
553
|
+
elements: {
|
|
554
|
+
Test: {
|
|
555
|
+
props: {
|
|
556
|
+
count: { type: "number" },
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
data: { items: [1, 2, 3, 4, 5] },
|
|
563
|
+
components: { Test: TestComponent },
|
|
564
|
+
}
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
expect(result.props.count).toBe(3);
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
describe("function declarations end-to-end", () => {
|
|
572
|
+
it("should render with user-defined component", () => {
|
|
573
|
+
const Span = ({ children, ...props }: Record<string, unknown>): TestNode => ({
|
|
574
|
+
type: "Span",
|
|
575
|
+
props,
|
|
576
|
+
children: children ? [children].flat() : [],
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
const result = render(
|
|
580
|
+
'function Badge({ label }) {\n return <Span>{label}</Span>\n}\nreturn <Badge label="hello" />',
|
|
581
|
+
{
|
|
582
|
+
data: {},
|
|
583
|
+
elements: {
|
|
584
|
+
Span: { props: {} },
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
data: {},
|
|
589
|
+
components: { Span },
|
|
590
|
+
}
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
expect(result.type).toBe("Span");
|
|
594
|
+
expect(result.children).toEqual(["hello"]);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it("should render with nested user-defined components", () => {
|
|
598
|
+
const Span = ({ children, ...props }: Record<string, unknown>): TestNode => ({
|
|
599
|
+
type: "Span",
|
|
600
|
+
props,
|
|
601
|
+
children: children ? [children].flat() : [],
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
const Div = ({ children, ...props }: Record<string, unknown>): TestNode => ({
|
|
605
|
+
type: "Div",
|
|
606
|
+
props,
|
|
607
|
+
children: children ? [children].flat() : [],
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const result = render(
|
|
611
|
+
'function Label({ text }) {\n return <Span>{text}</Span>\n}\nfunction Card({ title }) {\n return <Div><Label text={title} /></Div>\n}\nreturn <Card title="world" />',
|
|
612
|
+
{
|
|
613
|
+
data: {},
|
|
614
|
+
elements: {
|
|
615
|
+
Span: { props: {} },
|
|
616
|
+
Div: { props: {} },
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
data: {},
|
|
621
|
+
components: { Span, Div },
|
|
622
|
+
}
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
expect(result.type).toBe("Div");
|
|
626
|
+
expect(result.children).toHaveLength(1);
|
|
627
|
+
expect((result.children[0] as TestNode).type).toBe("Span");
|
|
628
|
+
expect((result.children[0] as TestNode).children).toEqual(["world"]);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it("should render function component with default props", () => {
|
|
632
|
+
const Span = ({ children, ...props }: Record<string, unknown>): TestNode => ({
|
|
633
|
+
type: "Span",
|
|
634
|
+
props,
|
|
635
|
+
children: children ? [children].flat() : [],
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
const result = render(
|
|
639
|
+
"function Tag({ label = 'default' }) {\n return <Span>{label}</Span>\n}\nreturn <Tag />",
|
|
640
|
+
{
|
|
641
|
+
data: {},
|
|
642
|
+
elements: {
|
|
643
|
+
Span: { props: {} },
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
data: {},
|
|
648
|
+
components: { Span },
|
|
649
|
+
}
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
expect(result.children).toEqual(["default"]);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it("should render function component accessing data", () => {
|
|
656
|
+
const Span = ({ children, ...props }: Record<string, unknown>): TestNode => ({
|
|
657
|
+
type: "Span",
|
|
658
|
+
props,
|
|
659
|
+
children: children ? [children].flat() : [],
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
const result = render(
|
|
663
|
+
"function Greeting() {\n return <Span>Hello {name}</Span>\n}\nreturn <Greeting />",
|
|
664
|
+
{
|
|
665
|
+
data: {
|
|
666
|
+
name: { type: "string" },
|
|
667
|
+
},
|
|
668
|
+
elements: {
|
|
669
|
+
Span: { props: {} },
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
data: { name: "World" },
|
|
674
|
+
components: { Span },
|
|
675
|
+
}
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
expect(result.children).toEqual(["Hello", "World"]);
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it("should render function component with children", () => {
|
|
682
|
+
const Div = ({ children, ...props }: Record<string, unknown>): TestNode => ({
|
|
683
|
+
type: "Div",
|
|
684
|
+
props,
|
|
685
|
+
children: children ? [children].flat() : [],
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const Span = ({ children, ...props }: Record<string, unknown>): TestNode => ({
|
|
689
|
+
type: "Span",
|
|
690
|
+
props,
|
|
691
|
+
children: children ? [children].flat() : [],
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
const result = render(
|
|
695
|
+
"function Wrapper({ children }) {\n return <Div>{children}</Div>\n}\nreturn <Wrapper><Span>inside</Span></Wrapper>",
|
|
696
|
+
{
|
|
697
|
+
data: {},
|
|
698
|
+
elements: {
|
|
699
|
+
Div: { props: {} },
|
|
700
|
+
Span: { props: {} },
|
|
701
|
+
},
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
data: {},
|
|
705
|
+
components: { Div, Span },
|
|
706
|
+
}
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
expect(result.type).toBe("Div");
|
|
710
|
+
expect(result.children).toHaveLength(1);
|
|
711
|
+
expect((result.children[0] as TestNode).type).toBe("Span");
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
describe("const + function combined end-to-end", () => {
|
|
716
|
+
it("should render template with const and function together", () => {
|
|
717
|
+
const Span = ({ children, ...props }: Record<string, unknown>): TestNode => ({
|
|
718
|
+
type: "Span",
|
|
719
|
+
props,
|
|
720
|
+
children: children ? [children].flat() : [],
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
const Div = ({ children, ...props }: Record<string, unknown>): TestNode => ({
|
|
724
|
+
type: "Div",
|
|
725
|
+
props,
|
|
726
|
+
children: children ? [children].flat() : [],
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
const result = render(
|
|
730
|
+
"const title = name.toUpperCase();\nfunction Header({ text }) {\n return <Span>{text}</Span>\n}\nreturn <Div><Header text={title} /></Div>",
|
|
731
|
+
{
|
|
732
|
+
data: {
|
|
733
|
+
name: { type: "string" },
|
|
734
|
+
},
|
|
735
|
+
elements: {
|
|
736
|
+
Span: { props: {} },
|
|
737
|
+
Div: { props: {} },
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
data: { name: "hello" },
|
|
742
|
+
components: { Span, Div },
|
|
743
|
+
}
|
|
744
|
+
);
|
|
745
|
+
|
|
746
|
+
expect(result.type).toBe("Div");
|
|
747
|
+
expect(result.children).toHaveLength(1);
|
|
748
|
+
expect((result.children[0] as TestNode).children).toEqual(["HELLO"]);
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it("should render with map over data using function component", () => {
|
|
752
|
+
const Span = ({ children, ...props }: Record<string, unknown>): TestNode => ({
|
|
753
|
+
type: "Span",
|
|
754
|
+
props,
|
|
755
|
+
children: children ? [children].flat() : [],
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
const Div = ({ children, ...props }: Record<string, unknown>): TestNode => ({
|
|
759
|
+
type: "Div",
|
|
760
|
+
props,
|
|
761
|
+
children: children ? [children].flat() : [],
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
const result = render(
|
|
765
|
+
"function Item({ label }) {\n return <Span>{label}</Span>\n}\nreturn <Div>{items.map(i => <Item label={i} />)}</Div>",
|
|
766
|
+
{
|
|
767
|
+
data: {
|
|
768
|
+
items: { type: "array", shape: { type: "string" } },
|
|
769
|
+
},
|
|
770
|
+
elements: {
|
|
771
|
+
Span: { props: {} },
|
|
772
|
+
Div: { props: {} },
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
data: { items: ["a", "b", "c"] },
|
|
777
|
+
components: { Span, Div },
|
|
778
|
+
}
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
expect(result.type).toBe("Div");
|
|
782
|
+
expect(result.children).toHaveLength(3);
|
|
783
|
+
expect((result.children[0] as TestNode).children).toEqual(["a"]);
|
|
784
|
+
expect((result.children[1] as TestNode).children).toEqual(["b"]);
|
|
785
|
+
expect((result.children[2] as TestNode).children).toEqual(["c"]);
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
describe("String builtin end-to-end", () => {
|
|
790
|
+
it("should render with String() conversion", () => {
|
|
791
|
+
const TestComponent = ({ children, ...props }: Record<string, unknown>): TestNode => ({
|
|
792
|
+
type: "Test",
|
|
793
|
+
props,
|
|
794
|
+
children: children ? [children].flat() : [],
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
const result = render(
|
|
798
|
+
"<Test value={String(count)}>Text</Test>",
|
|
799
|
+
{
|
|
800
|
+
data: {
|
|
801
|
+
count: { type: "number" },
|
|
802
|
+
},
|
|
803
|
+
elements: {
|
|
804
|
+
Test: {
|
|
805
|
+
props: {
|
|
806
|
+
value: { type: "string" },
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
},
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
data: { count: 42 },
|
|
813
|
+
components: { Test: TestComponent },
|
|
814
|
+
}
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
expect(result.props.value).toBe("42");
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
describe("return requirement end-to-end", () => {
|
|
822
|
+
it("should throw AnalysisError for template with const but no return", () => {
|
|
823
|
+
const TestComponent = (): TestNode => ({
|
|
824
|
+
type: "Test",
|
|
825
|
+
props: {},
|
|
826
|
+
children: [],
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
expect(() =>
|
|
830
|
+
render(
|
|
831
|
+
"const x = 1;\n<Test>hello</Test>",
|
|
832
|
+
{
|
|
833
|
+
data: {},
|
|
834
|
+
elements: {
|
|
835
|
+
Test: { props: {} },
|
|
836
|
+
},
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
data: {},
|
|
840
|
+
components: { Test: TestComponent },
|
|
841
|
+
}
|
|
842
|
+
)
|
|
843
|
+
).toThrow(AnalysisError);
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
it("should work with const + explicit return", () => {
|
|
847
|
+
const TestComponent = ({ children, ...props }: Record<string, unknown>): TestNode => ({
|
|
848
|
+
type: "Test",
|
|
849
|
+
props,
|
|
850
|
+
children: children ? [children].flat() : [],
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
const result = render(
|
|
854
|
+
"const x = 1;\nreturn <Test value={x}>hello</Test>",
|
|
855
|
+
{
|
|
856
|
+
data: {},
|
|
857
|
+
elements: {
|
|
858
|
+
Test: {
|
|
859
|
+
props: {
|
|
860
|
+
value: { type: "number" },
|
|
861
|
+
},
|
|
862
|
+
},
|
|
863
|
+
},
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
data: {},
|
|
867
|
+
components: { Test: TestComponent },
|
|
868
|
+
}
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
expect(result.props.value).toBe(1);
|
|
872
|
+
});
|
|
424
873
|
});
|
|
425
874
|
});
|
package/src/render.ts
CHANGED
|
@@ -26,10 +26,10 @@ export interface RenderOptions<T = any> extends EvaluateOptions<T> {
|
|
|
26
26
|
* compiles it to executable code, and evaluates it with the provided data and components.
|
|
27
27
|
*
|
|
28
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={
|
|
29
|
+
* @param source - JSX expression source code to render (e.g., `"<Text x={user.name}>Hello</Text>"`)
|
|
30
30
|
* @param schema - Schema defining allowed data properties, JSX elements, and hierarchy rules
|
|
31
31
|
* @param options - Rendering configuration
|
|
32
|
-
* @param options.data - Data object
|
|
32
|
+
* @param options.data - Data object whose keys become top-level variables in expressions
|
|
33
33
|
* @param options.components - Map of component names to component functions
|
|
34
34
|
* @param options.createElement - Function to create elements (e.g., React.createElement)
|
|
35
35
|
* @param options.minSeverity - Minimum severity (1=info, 2=warning, 3=error) required to abort rendering; defaults to `3`
|
|
@@ -49,7 +49,7 @@ export interface RenderOptions<T = any> extends EvaluateOptions<T> {
|
|
|
49
49
|
* };
|
|
50
50
|
*
|
|
51
51
|
* const result = render(
|
|
52
|
-
* "<Text>Hello {
|
|
52
|
+
* "<Text>Hello {user.name}!</Text>",
|
|
53
53
|
* schema,
|
|
54
54
|
* {
|
|
55
55
|
* data: { user: { name: "World" } },
|
|
@@ -74,18 +74,11 @@ export function render<T = any>(
|
|
|
74
74
|
return evaluate(compileInternal(ast), schema, options) as T;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
export function compile<T = any>(
|
|
78
|
-
source: string,
|
|
79
|
-
schema: Schema,
|
|
80
|
-
{ minSeverity = 3 }: RenderOptions<T> = {},
|
|
81
|
-
tempSkipValidation?: boolean | undefined
|
|
82
|
-
): string {
|
|
77
|
+
export function compile<T = any>(source: string, schema: Schema, { minSeverity = 3 }: RenderOptions<T> = {}): string {
|
|
83
78
|
const ast = parse(source);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
throw AnalysisError.fromReport(report);
|
|
88
|
-
}
|
|
79
|
+
const report = analyze(ast, schema);
|
|
80
|
+
if (report.hasIssues(minSeverity)) {
|
|
81
|
+
throw AnalysisError.fromReport(report);
|
|
89
82
|
}
|
|
90
83
|
return compileInternal(ast);
|
|
91
84
|
}
|