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.
Files changed (141) hide show
  1. package/README.md +155 -188
  2. package/lib/analyze/analysis-report.d.ts +2 -2
  3. package/lib/analyze/analysis-report.d.ts.map +1 -1
  4. package/lib/analyze/analysis-report.js +4 -0
  5. package/lib/analyze/analysis-report.js.map +1 -1
  6. package/lib/analyze/data/data-access.d.ts.map +1 -1
  7. package/lib/analyze/data/data-access.js +45 -82
  8. package/lib/analyze/data/data-access.js.map +1 -1
  9. package/lib/analyze/data/method-calls.d.ts.map +1 -1
  10. package/lib/analyze/data/method-calls.js +4 -3
  11. package/lib/analyze/data/method-calls.js.map +1 -1
  12. package/lib/analyze/data/utils.d.ts +2 -3
  13. package/lib/analyze/data/utils.d.ts.map +1 -1
  14. package/lib/analyze/data/utils.js +10 -36
  15. package/lib/analyze/data/utils.js.map +1 -1
  16. package/lib/analyze/jsx/element-attributes.d.ts +1 -1
  17. package/lib/analyze/jsx/element-attributes.d.ts.map +1 -1
  18. package/lib/analyze/jsx/element-attributes.js +13 -3
  19. package/lib/analyze/jsx/element-attributes.js.map +1 -1
  20. package/lib/analyze/jsx/element-children.d.ts +1 -1
  21. package/lib/analyze/jsx/element-children.d.ts.map +1 -1
  22. package/lib/analyze/jsx/element-children.js +16 -5
  23. package/lib/analyze/jsx/element-children.js.map +1 -1
  24. package/lib/analyze/jsx/element-tags.d.ts +1 -1
  25. package/lib/analyze/jsx/element-tags.d.ts.map +1 -1
  26. package/lib/analyze/jsx/element-tags.js +12 -2
  27. package/lib/analyze/jsx/element-tags.js.map +1 -1
  28. package/lib/analyze/security/analyze.d.ts.map +1 -1
  29. package/lib/analyze/security/analyze.js +2 -4
  30. package/lib/analyze/security/analyze.js.map +1 -1
  31. package/lib/analyze/security/call-expressions.d.ts +2 -2
  32. package/lib/analyze/security/call-expressions.d.ts.map +1 -1
  33. package/lib/analyze/security/call-expressions.js +21 -5
  34. package/lib/analyze/security/call-expressions.js.map +1 -1
  35. package/lib/analyze/security/declarations.d.ts +1 -1
  36. package/lib/analyze/security/declarations.d.ts.map +1 -1
  37. package/lib/analyze/security/declarations.js +22 -2
  38. package/lib/analyze/security/declarations.js.map +1 -1
  39. package/lib/analyze/security/expressions.d.ts.map +1 -1
  40. package/lib/analyze/security/expressions.js +10 -2
  41. package/lib/analyze/security/expressions.js.map +1 -1
  42. package/lib/analyze/security/identifiers.d.ts +2 -2
  43. package/lib/analyze/security/identifiers.d.ts.map +1 -1
  44. package/lib/analyze/security/identifiers.js +159 -19
  45. package/lib/analyze/security/identifiers.js.map +1 -1
  46. package/lib/builtins.d.ts.map +1 -1
  47. package/lib/builtins.js +5 -0
  48. package/lib/builtins.js.map +1 -1
  49. package/lib/codegen/builtins/builtin-types.d.ts.map +1 -1
  50. package/lib/codegen/builtins/builtin-types.js +7 -1
  51. package/lib/codegen/builtins/builtin-types.js.map +1 -1
  52. package/lib/compile/compile.d.ts.map +1 -1
  53. package/lib/compile/compile.js +131 -61
  54. package/lib/compile/compile.js.map +1 -1
  55. package/lib/evaluate/evaluate.d.ts +3 -3
  56. package/lib/evaluate/evaluate.d.ts.map +1 -1
  57. package/lib/evaluate/evaluate.js +28 -7
  58. package/lib/evaluate/evaluate.js.map +1 -1
  59. package/lib/jsx.d.ts +2 -19
  60. package/lib/jsx.d.ts.map +1 -1
  61. package/lib/jsx.js +0 -6
  62. package/lib/jsx.js.map +1 -1
  63. package/lib/parse/parse.d.ts.map +1 -1
  64. package/lib/parse/parse.js +4 -3
  65. package/lib/parse/parse.js.map +1 -1
  66. package/lib/render.d.ts +4 -4
  67. package/lib/render.d.ts.map +1 -1
  68. package/lib/render.js +7 -9
  69. package/lib/render.js.map +1 -1
  70. package/lib/schema.d.ts +2 -7
  71. package/lib/schema.d.ts.map +1 -1
  72. package/lib/schema.js +0 -43
  73. package/lib/schema.js.map +1 -1
  74. package/lib/traverse.d.ts +4 -4
  75. package/lib/traverse.d.ts.map +1 -1
  76. package/lib/traverse.js +118 -40
  77. package/lib/traverse.js.map +1 -1
  78. package/lib/validate.d.ts +1 -1
  79. package/lib/validate.js +1 -1
  80. package/package.json +4 -3
  81. package/src/analyze/analysis-report.ts +6 -2
  82. package/src/analyze/analyze.test.ts +108 -69
  83. package/src/analyze/data/data-access.ts +45 -90
  84. package/src/analyze/data/method-calls.ts +4 -3
  85. package/src/analyze/data/utils.ts +11 -40
  86. package/src/analyze/jsx/element-attributes.ts +24 -9
  87. package/src/analyze/jsx/element-children.ts +20 -6
  88. package/src/analyze/jsx/element-tags.ts +15 -3
  89. package/src/analyze/security/analyze.ts +1 -3
  90. package/src/analyze/security/call-expressions.ts +37 -17
  91. package/src/analyze/security/declarations.ts +35 -9
  92. package/src/analyze/security/expressions.ts +17 -3
  93. package/src/analyze/security/identifiers.ts +172 -28
  94. package/src/builtins.ts +6 -0
  95. package/src/codegen/builtins/builtin-types.ts +7 -1
  96. package/src/codegen/generate-typescript-definitions.test.ts +16 -23
  97. package/src/compile/compile.test.ts +153 -30
  98. package/src/compile/compile.ts +163 -73
  99. package/src/evaluate/evaluate.test.ts +276 -14
  100. package/src/evaluate/evaluate.ts +34 -10
  101. package/src/jsx.ts +2 -31
  102. package/src/parse/parse.test.ts +4 -4
  103. package/src/parse/parse.ts +4 -3
  104. package/src/render.test.ts +476 -27
  105. package/src/render.ts +7 -14
  106. package/src/schema.ts +1 -55
  107. package/src/traverse.ts +135 -49
  108. package/src/validate.ts +1 -1
  109. package/LICENSE +0 -21
  110. package/lib/analyze/analyze.test.d.ts +0 -2
  111. package/lib/analyze/analyze.test.d.ts.map +0 -1
  112. package/lib/analyze/analyze.test.js +0 -763
  113. package/lib/analyze/analyze.test.js.map +0 -1
  114. package/lib/analyze/security/member-access.d.ts +0 -6
  115. package/lib/codegen/generate-typescript-definitions.test.d.ts +0 -2
  116. package/lib/codegen/generate-typescript-definitions.test.d.ts.map +0 -1
  117. package/lib/codegen/generate-typescript-definitions.test.js +0 -649
  118. package/lib/codegen/generate-typescript-definitions.test.js.map +0 -1
  119. package/lib/compile/compile.test.d.ts +0 -2
  120. package/lib/compile/compile.test.d.ts.map +0 -1
  121. package/lib/compile/compile.test.js +0 -146
  122. package/lib/compile/compile.test.js.map +0 -1
  123. package/lib/evaluate/evaluate.test.d.ts +0 -2
  124. package/lib/evaluate/evaluate.test.d.ts.map +0 -1
  125. package/lib/evaluate/evaluate.test.js +0 -260
  126. package/lib/evaluate/evaluate.test.js.map +0 -1
  127. package/lib/parse/parse.test.d.ts +0 -2
  128. package/lib/parse/parse.test.d.ts.map +0 -1
  129. package/lib/parse/parse.test.js +0 -80
  130. package/lib/parse/parse.test.js.map +0 -1
  131. package/lib/render.test.d.ts +0 -2
  132. package/lib/render.test.d.ts.map +0 -1
  133. package/lib/render.test.js +0 -301
  134. package/lib/render.test.js.map +0 -1
  135. package/lib/traverse.test.d.ts +0 -2
  136. package/lib/traverse.test.d.ts.map +0 -1
  137. package/lib/traverse.test.js +0 -84
  138. package/lib/traverse.test.js.map +0 -1
  139. package/lib/types.d.ts +0 -12
  140. package/src/analyze/security/member-access.ts +0 -35
  141. package/src/types.ts +0 -17
@@ -1,25 +1,25 @@
1
- import { describe, it, expect } from "vitest";
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?: Record<string, unknown>;
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 }, ...children: any[]): TestNode => ({
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={data.x} y={Math.floor(data.y)}>Hello</Test>",
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={data.items.filter(x => x > 2).map(x => x * 2).join(', ')}>Text</Test>",
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 }, ...children: any[]): TestNode => ({
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
- "{data.user.name}",
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
- "{data.count * 2}",
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
- "{data.text.toUpperCase()}",
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
- "{data.numbers.map(x => x * 2).join(', ')}",
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 => ({ type: "Test", props: {}, children: [] });
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 => ({ type: "Test", props: {}, children: [] });
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 => ({ type: "Test", props: {}, children: [] });
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 => ({ type: "Test", props: {}, children: [] });
300
+ const TestComponent = (): TestNode => ({
301
+ type: "Test",
302
+ props: {},
303
+ children: [],
304
+ });
289
305
 
290
306
  expect(() =>
291
307
  render(
292
- "<Test>{data.items.push(1)}</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 => ({ type: "Test", props: {}, children: [] });
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 => ({ type: "Test", props: {}, children: [] });
362
+ const TestComponent = (): TestNode => ({
363
+ type: "Test",
364
+ props: {},
365
+ children: [],
366
+ });
343
367
 
344
368
  expect(() =>
345
369
  render(
346
- "<Test value={data.missing.nested}>Hello</Test>",
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 => ({ type: "TestComponent", props: {}, children: [] });
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={data.show ? 'visible' : 'hidden'}>Text</Test>",
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!.msg).toBe("hidden");
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={data.user.name}>Hello</Text>"`)
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 available as `data.*` in expressions
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 {data.user.name}!</Text>",
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
- if (!tempSkipValidation) {
85
- const report = analyze(ast, schema);
86
- if (report.hasIssues(minSeverity)) {
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
  }