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
@@ -2,7 +2,7 @@ import { describe, it, expect, vi } from "vitest";
2
2
  import { evaluate, type EvaluateOptions } from "./evaluate.js";
3
3
  import { compile } from "../compile/index.js";
4
4
  import { parse } from "../parse/index.js";
5
- import { Schema } from "../schema.js";
5
+ import type { Schema } from "../schema.js";
6
6
 
7
7
  import { EvaluationError } from "./evaluation-error.js";
8
8
 
@@ -32,7 +32,7 @@ describe("evaluate", () => {
32
32
  children: props.children,
33
33
  });
34
34
 
35
- const result = compileAndEvaluate("<Test x={data.x} y={data.y}>Hello</Test>", {
35
+ const result = compileAndEvaluate("<Test x={x} y={y}>Hello</Test>", {
36
36
  data: { x: 10, y: 20 },
37
37
  components: {
38
38
  Test: TestComponent,
@@ -53,7 +53,7 @@ describe("evaluate", () => {
53
53
  children: [],
54
54
  });
55
55
 
56
- const result = compileAndEvaluate("<Test value={Math.floor(data.x * Math.PI)}>Text</Test>", {
56
+ const result = compileAndEvaluate("<Test value={Math.floor(x * Math.PI)}>Text</Test>", {
57
57
  data: { x: 5 },
58
58
  components: {
59
59
  Test: TestComponent,
@@ -105,7 +105,7 @@ describe("evaluate", () => {
105
105
  children: props.children,
106
106
  });
107
107
 
108
- const result = compileAndEvaluate("<Outer x={data.x}><Inner y={data.y}>Text</Inner></Outer>", {
108
+ const result = compileAndEvaluate("<Outer x={x}><Inner y={y}>Text</Inner></Outer>", {
109
109
  data: { x: 10, y: 20 },
110
110
  components: {
111
111
  Outer: OuterComponent,
@@ -152,7 +152,7 @@ describe("evaluate", () => {
152
152
  children: [],
153
153
  });
154
154
 
155
- const result = compileAndEvaluate("<Test msg={data.show ? 'visible' : 'hidden'}>Text</Test>", {
155
+ const result = compileAndEvaluate("<Test msg={show ? 'visible' : 'hidden'}>Text</Test>", {
156
156
  data: { show: true },
157
157
  components: {
158
158
  Test: TestComponent,
@@ -169,7 +169,7 @@ describe("evaluate", () => {
169
169
  children: [],
170
170
  });
171
171
 
172
- const result = compileAndEvaluate("<Test values={data.items.filter(x => x > 2).map(x => x * 2)}>Text</Test>", {
172
+ const result = compileAndEvaluate("<Test values={items.filter(x => x > 2).map(x => x * 2)}>Text</Test>", {
173
173
  data: { items: [1, 2, 3, 4, 5] },
174
174
  components: {
175
175
  Test: TestComponent,
@@ -180,7 +180,7 @@ describe("evaluate", () => {
180
180
  });
181
181
 
182
182
  it("should handle runtime errors gracefully", () => {
183
- const compiled = compile(parse("<Test value={data.missing.nested}>Text</Test>"));
183
+ const compiled = compile(parse("<Test value={missing.nested}>Text</Test>"));
184
184
  const TestComponent = (): Node => ({ type: "Test", props: {}, children: [] });
185
185
  const schema = createPermissiveSchema(["Test"]);
186
186
 
@@ -201,7 +201,7 @@ describe("evaluate", () => {
201
201
  children: [],
202
202
  });
203
203
 
204
- const result = compileAndEvaluate("<Test msg={data.name || 'Anonymous'}>Text</Test>", {
204
+ const result = compileAndEvaluate("<Test msg={name || 'Anonymous'}>Text</Test>", {
205
205
  data: { name: "" },
206
206
  components: {
207
207
  Test: TestComponent,
@@ -218,7 +218,7 @@ describe("evaluate", () => {
218
218
  children: props.children,
219
219
  });
220
220
 
221
- const result = compileAndEvaluate("<Test>{data.show && 'Visible'}{false}{'Always'}</Test>", {
221
+ const result = compileAndEvaluate("<Test>{show && 'Visible'}{false}{'Always'}</Test>", {
222
222
  data: { show: true },
223
223
  components: {
224
224
  Test: TestComponent,
@@ -246,7 +246,7 @@ describe("evaluate", () => {
246
246
  children: props.children,
247
247
  });
248
248
 
249
- const result = compileAndEvaluate("<Test>{}{data.value}{}</Test>", {
249
+ const result = compileAndEvaluate("<Test>{}{value}{}</Test>", {
250
250
  data: { value: "hello" },
251
251
  components: { Test: TestComponent },
252
252
  });
@@ -262,7 +262,7 @@ describe("evaluate", () => {
262
262
  });
263
263
 
264
264
  const result = compileAndEvaluate(
265
- "<Test values={data.items.filter(x => x > 2).map(x => x * 2).join(', ')}>Text</Test>",
265
+ "<Test values={items.filter(x => x > 2).map(x => x * 2).join(', ')}>Text</Test>",
266
266
  {
267
267
  data: { items: [1, 2, 3, 4, 5] },
268
268
  components: { Test: TestComponent },
@@ -290,7 +290,7 @@ describe("evaluate", () => {
290
290
  children: [],
291
291
  });
292
292
 
293
- const result = compileAndEvaluate('<Test msg={"Hello " + data.name}>Text</Test>', {
293
+ const result = compileAndEvaluate('<Test msg={"Hello " + name}>Text</Test>', {
294
294
  data: { name: "World" },
295
295
  components: { Test: TestComponent },
296
296
  });
@@ -305,7 +305,7 @@ describe("evaluate", () => {
305
305
  children: [],
306
306
  });
307
307
 
308
- const result = compileAndEvaluate("<Test value={data.x + data.y * 2}>Text</Test>", {
308
+ const result = compileAndEvaluate("<Test value={x + y * 2}>Text</Test>", {
309
309
  data: { x: 5, y: 3 },
310
310
  components: { Test: TestComponent },
311
311
  });
@@ -320,11 +320,273 @@ describe("evaluate", () => {
320
320
  children: props.children,
321
321
  });
322
322
 
323
- const result = compileAndEvaluate("<Test>Hello {data.name}!</Test>", {
323
+ const result = compileAndEvaluate("<Test>Hello {name}!</Test>", {
324
324
  data: { name: "World" },
325
325
  components: { Test: TestComponent },
326
326
  });
327
327
 
328
328
  expect(result.children).toEqual(["Hello", "World", "!"]);
329
329
  });
330
+
331
+ describe("const declarations", () => {
332
+ it("should evaluate const with literal value", () => {
333
+ const TestComponent = (props: any): Node => ({
334
+ type: "Test",
335
+ props,
336
+ children: [],
337
+ });
338
+
339
+ const result = compileAndEvaluate("const x = 42;\nreturn <Test value={x}>Text</Test>", {
340
+ components: { Test: TestComponent },
341
+ });
342
+
343
+ expect(result.props!.value).toBe(42);
344
+ });
345
+
346
+ it("should evaluate const with computed expression", () => {
347
+ const TestComponent = (props: any): Node => ({
348
+ type: "Test",
349
+ props,
350
+ children: [],
351
+ });
352
+
353
+ const result = compileAndEvaluate("const a = x + 1;\nconst b = a * 2;\nreturn <Test value={b}>Text</Test>", {
354
+ data: { x: 3 },
355
+ components: { Test: TestComponent },
356
+ });
357
+
358
+ expect(result.props!.value).toBe(8); // (3+1)*2
359
+ });
360
+
361
+ it("should evaluate const referencing data", () => {
362
+ const TestComponent = (props: any): Node => ({
363
+ type: "Test",
364
+ props,
365
+ children: [],
366
+ });
367
+
368
+ const result = compileAndEvaluate("const greeting = 'Hello ' + name;\nreturn <Test msg={greeting}>Text</Test>", {
369
+ data: { name: "World" },
370
+ components: { Test: TestComponent },
371
+ });
372
+
373
+ expect(result.props!.msg).toBe("Hello World");
374
+ });
375
+
376
+ it("should evaluate const with computed array", () => {
377
+ const TestComponent = (props: any): Node => ({
378
+ type: "Test",
379
+ props,
380
+ children: [],
381
+ });
382
+
383
+ const result = compileAndEvaluate(
384
+ "const filtered = items.filter(x => x > 2);\nreturn <Test count={filtered.length}>Text</Test>",
385
+ {
386
+ data: { items: [1, 2, 3, 4, 5] },
387
+ components: { Test: TestComponent },
388
+ }
389
+ );
390
+
391
+ expect(result.props!.count).toBe(3);
392
+ });
393
+ });
394
+
395
+ describe("function declarations", () => {
396
+ it("should evaluate user-defined component", () => {
397
+ const Span = (props: any): Node => ({
398
+ type: "Span",
399
+ props: {},
400
+ children: props.children,
401
+ });
402
+
403
+ const result = compileAndEvaluate(
404
+ 'function Badge({ label }) {\n return <Span>{label}</Span>\n}\nreturn <Badge label="hello" />',
405
+ {
406
+ components: { Span },
407
+ }
408
+ );
409
+
410
+ expect(result.type).toBe("Span");
411
+ expect(result.children).toEqual(["hello"]);
412
+ });
413
+
414
+ it("should evaluate function with const inside", () => {
415
+ const Span = (props: any): Node => ({
416
+ type: "Span",
417
+ props,
418
+ children: [],
419
+ });
420
+
421
+ const result = compileAndEvaluate(
422
+ "function Doubled({ value }) {\n const d = value * 2;\n return <Span result={d} />\n}\nreturn <Doubled value={5} />",
423
+ {
424
+ components: { Span },
425
+ }
426
+ );
427
+
428
+ expect(result.props!.result).toBe(10);
429
+ });
430
+
431
+ it("should evaluate nested user-defined components", () => {
432
+ const Span = (props: any): Node => ({
433
+ type: "Span",
434
+ props: {},
435
+ children: props.children,
436
+ });
437
+
438
+ const Div = (props: any): Node => ({
439
+ type: "Div",
440
+ props: {},
441
+ children: props.children,
442
+ });
443
+
444
+ const result = compileAndEvaluate(
445
+ 'function Label({ text }) {\n return <Span>{text}</Span>\n}\nfunction Card({ title }) {\n return <Div><Label text={title} /></Div>\n}\nreturn <Card title="hi" />',
446
+ {
447
+ components: { Span, Div },
448
+ }
449
+ );
450
+
451
+ expect(result.type).toBe("Div");
452
+ expect(result.children).toHaveLength(1);
453
+ expect((result.children[0] as Node).type).toBe("Span");
454
+ expect((result.children[0] as Node).children).toEqual(["hi"]);
455
+ });
456
+
457
+ it("should evaluate function with children", () => {
458
+ const Div = (props: any): Node => ({
459
+ type: "Div",
460
+ props: {},
461
+ children: props.children,
462
+ });
463
+
464
+ const Span = (props: any): Node => ({
465
+ type: "Span",
466
+ props: {},
467
+ children: props.children,
468
+ });
469
+
470
+ const result = compileAndEvaluate(
471
+ "function Wrapper({ children }) {\n return <Div>{children}</Div>\n}\nreturn <Wrapper><Span>inside</Span></Wrapper>",
472
+ {
473
+ components: { Div, Span },
474
+ }
475
+ );
476
+
477
+ expect(result.type).toBe("Div");
478
+ expect(result.children).toHaveLength(1);
479
+ expect((result.children[0] as Node).type).toBe("Span");
480
+ });
481
+
482
+ it("should evaluate function with default parameter values", () => {
483
+ const Span = (props: any): Node => ({
484
+ type: "Span",
485
+ props: {},
486
+ children: props.children,
487
+ });
488
+
489
+ const result = compileAndEvaluate(
490
+ "function Badge({ label = 'default' }) {\n return <Span>{label}</Span>\n}\nreturn <Badge />",
491
+ {
492
+ components: { Span },
493
+ }
494
+ );
495
+
496
+ expect(result.children).toEqual(["default"]);
497
+ });
498
+
499
+ it("should evaluate function with data access", () => {
500
+ const Span = (props: any): Node => ({
501
+ type: "Span",
502
+ props: {},
503
+ children: props.children,
504
+ });
505
+
506
+ const result = compileAndEvaluate(
507
+ "function Greeting() {\n return <Span>Hello {name}</Span>\n}\nreturn <Greeting />",
508
+ {
509
+ data: { name: "World" },
510
+ components: { Span },
511
+ }
512
+ );
513
+
514
+ expect(result.children).toEqual(["Hello", "World"]);
515
+ });
516
+ });
517
+
518
+ describe("const and function combined", () => {
519
+ it("should evaluate const used in function body", () => {
520
+ const Span = (props: any): Node => ({
521
+ type: "Span",
522
+ props,
523
+ children: [],
524
+ });
525
+
526
+ const result = compileAndEvaluate(
527
+ "const multiplier = 3;\nfunction Calc({ value }) {\n return <Span result={value * multiplier} />\n}\nreturn <Calc value={4} />",
528
+ {
529
+ components: { Span },
530
+ }
531
+ );
532
+
533
+ expect(result.props!.result).toBe(12);
534
+ });
535
+
536
+ it("should evaluate function used in map with const", () => {
537
+ const Div = (props: any): Node => ({
538
+ type: "Div",
539
+ props: {},
540
+ children: props.children,
541
+ });
542
+
543
+ const Span = (props: any): Node => ({
544
+ type: "Span",
545
+ props: {},
546
+ children: props.children,
547
+ });
548
+
549
+ const result = compileAndEvaluate(
550
+ "function Item({ text }) {\n return <Span>{text}</Span>\n}\nconst labels = items.map(i => i.name);\nreturn <Div>{labels.map(l => <Item text={l} />)}</Div>",
551
+ {
552
+ data: { items: [{ name: "a" }, { name: "b" }] },
553
+ components: { Div, Span },
554
+ }
555
+ );
556
+
557
+ expect(result.type).toBe("Div");
558
+ expect(result.children).toHaveLength(2);
559
+ });
560
+ });
561
+
562
+ describe("String builtin", () => {
563
+ it("should evaluate String() conversion", () => {
564
+ const TestComponent = (props: any): Node => ({
565
+ type: "Test",
566
+ props,
567
+ children: [],
568
+ });
569
+
570
+ const result = compileAndEvaluate("<Test value={String(42)}>Text</Test>", {
571
+ components: { Test: TestComponent },
572
+ });
573
+
574
+ expect(result.props!.value).toBe("42");
575
+ });
576
+
577
+ it("should evaluate String() with boolean", () => {
578
+ const TestComponent = (props: any): Node => ({
579
+ type: "Test",
580
+ props,
581
+ children: [],
582
+ });
583
+
584
+ const result = compileAndEvaluate("<Test value={String(flag)}>Text</Test>", {
585
+ data: { flag: true },
586
+ components: { Test: TestComponent },
587
+ });
588
+
589
+ expect(result.props!.value).toBe("true");
590
+ });
591
+ });
330
592
  });
@@ -1,5 +1,5 @@
1
+ import { canHaveChildren, isElementAllowed, type Schema } from "../schema.js";
1
2
  import { EvaluationError } from "./evaluation-error.js";
2
- import { Schema, isElementAllowed, canHaveChildren } from "../schema.js";
3
3
 
4
4
  export type ComponentDict = Record<string, Component>;
5
5
 
@@ -15,7 +15,7 @@ export type Node = {
15
15
  children: Node[];
16
16
  };
17
17
 
18
- export type H = (type: string, props?: PropsDict, ...children: Node[]) => Node;
18
+ export type H = (type: string | Function, props?: PropsDict, ...children: Node[]) => Node;
19
19
 
20
20
  export type Component = (...args: any[]) => any;
21
21
 
@@ -25,7 +25,7 @@ export type Component = (...args: any[]) => any;
25
25
  * @template T - The return type of createElement function
26
26
  */
27
27
  export interface EvaluateOptions<T = any> {
28
- /** Data object available as `data.*` in JSX expressions */
28
+ /** Data object whose keys become top-level variables in JSX expressions */
29
29
  data?: DataDict;
30
30
  /** Map of component names to component functions used in JSX */
31
31
  components?: ComponentDict;
@@ -35,14 +35,21 @@ export interface EvaluateOptions<T = any> {
35
35
  createElement?: CreateElement<T>;
36
36
  }
37
37
 
38
+ const RESERVED_PARAMS = new Set(["h", "Math"]);
39
+ const VALID_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
40
+
38
41
  export function evaluate<T = any>(source: string, schema: Schema, options: EvaluateOptions<T>): T {
39
42
  const { data = {}, components = {}, functions = {}, createElement } = options;
40
43
 
44
+ const dataKeys = Object.keys(data);
45
+ const functionKeys = Object.keys(functions);
46
+ validateParamKeys(dataKeys, functionKeys);
47
+
41
48
  const h = createH(components, createElement || defaultCreateElement, schema);
42
- const frozen = deepFreezeData(data);
49
+ const frozen = deepFreezeData(structuredClone(data));
43
50
  try {
44
51
  // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
45
- return new Function("h", "Math", ...Object.keys(frozen), ...Object.keys(functions), source)(
52
+ return new Function("h", "Math", ...dataKeys, ...functionKeys, source)(
46
53
  h,
47
54
  Math,
48
55
  ...Object.values(frozen),
@@ -58,8 +65,25 @@ export function evaluate<T = any>(source: string, schema: Schema, options: Evalu
58
65
  }
59
66
  }
60
67
 
68
+ function validateParamKeys(dataKeys: string[], functionKeys: string[]): void {
69
+ const seen = new Set<string>(RESERVED_PARAMS);
70
+ for (const key of [...dataKeys, ...functionKeys]) {
71
+ if (!VALID_IDENTIFIER.test(key)) {
72
+ throw new EvaluationError(`Invalid key "${key}": must be a valid JavaScript identifier`);
73
+ }
74
+ if (seen.has(key)) {
75
+ throw new EvaluationError(`Duplicate or reserved parameter name "${key}"`);
76
+ }
77
+ seen.add(key);
78
+ }
79
+ }
80
+
61
81
  function createH(components: ComponentDict, createElement: CreateElement, schema: Schema): H {
62
82
  return function h(type, props, ...children) {
83
+ if (typeof type === "function") {
84
+ return createElement(type as Component, props, ...children.flat().filter(Boolean));
85
+ }
86
+
63
87
  const Component = components[type];
64
88
 
65
89
  if (!Component) {
@@ -78,14 +102,14 @@ function createH(components: ComponentDict, createElement: CreateElement, schema
78
102
 
79
103
  function deepFreezeData(obj: Record<string, unknown>): Record<string, unknown> {
80
104
  Object.freeze(obj);
81
- Object.values(obj).filter(isDataDict).forEach(deepFreezeData);
105
+ for (const value of Object.values(obj)) {
106
+ if (typeof value === "object" && value !== null) {
107
+ deepFreezeData(value as Record<string, unknown>);
108
+ }
109
+ }
82
110
  return obj;
83
111
  }
84
112
 
85
- function isDataDict(value: any): value is DataDict {
86
- return typeof value === "object" && value !== null;
87
- }
88
-
89
113
  function defaultCreateElement(Component: Component, props: PropsDict = {}, ...children: any[]): Node {
90
114
  return Component({ ...props, children });
91
115
  }
package/src/jsx.ts CHANGED
@@ -12,28 +12,11 @@ import type {
12
12
  UnaryExpression,
13
13
  } from "acorn";
14
14
 
15
- export type JSXStringLiteral = {
15
+ export type JSXLiteral = {
16
16
  type: "Literal";
17
- value: string;
17
+ value: string | number | boolean | null;
18
18
  };
19
19
 
20
- export type JSXNumberLiteral = {
21
- type: "Literal";
22
- value: number;
23
- };
24
-
25
- export type JSXBooleanLiteral = {
26
- type: "Literal";
27
- value: boolean;
28
- };
29
-
30
- export type JSXNullLiteral = {
31
- type: "Literal";
32
- value: null;
33
- };
34
-
35
- export type JSXLiteral = JSXStringLiteral | JSXNumberLiteral | JSXBooleanLiteral | JSXNullLiteral;
36
-
37
20
  export type JSXAttributeValue = JSXLiteral | JSXExpressionContainer;
38
21
 
39
22
  export type JSXNameNode =
@@ -116,10 +99,6 @@ export type JSXRoot = JSXElement | JSXFragment | JSXExpressionContainer;
116
99
  export type JSXNodeWithChildren = JSXElement | JSXFragment;
117
100
  export type JSXLeafNode = JSXText | JSXEmptyExpression;
118
101
 
119
- export type JSXExpressionHost = JSXExpressionContainer;
120
-
121
- export type GetAttributeValue<T extends JSXAttribute> = T["value"];
122
-
123
102
  export function isJsxElement(value: unknown): value is JSXElement {
124
103
  return (value as JSXElement)?.type === "JSXElement";
125
104
  }
@@ -136,10 +115,6 @@ export function isJsxAttribute(value: unknown): value is JSXAttribute {
136
115
  return (value as JSXAttribute)?.type === "JSXAttribute";
137
116
  }
138
117
 
139
- export function isJsxLiteral(value: unknown): value is JSXLiteral {
140
- return (value as JSXLiteral)?.type === "Literal";
141
- }
142
-
143
118
  export function isJsxExpressionContainer(value: unknown): value is JSXExpressionContainer {
144
119
  return (value as JSXExpressionContainer)?.type === "JSXExpressionContainer";
145
120
  }
@@ -152,10 +127,6 @@ export function isJsxText(value: unknown): value is JSXText {
152
127
  return (value as JSXText)?.type === "JSXText";
153
128
  }
154
129
 
155
- export function isJsxNodeWithChildren(value: JSXNode): value is JSXNodeWithChildren {
156
- return isJsxElement(value) || isJsxFragment(value);
157
- }
158
-
159
130
  export function isJsxLeafNode(value: JSXNode): value is JSXLeafNode {
160
131
  return (value as JSXText)?.type === "JSXText" || isJsxEmptyExpression(value);
161
132
  }
@@ -43,28 +43,28 @@ describe("parse", () => {
43
43
  });
44
44
 
45
45
  it("should parse JSX with complex expressions", () => {
46
- const result = parse("<Text>{data.items.map(x => x * 2).join(', ')}</Text>");
46
+ const result = parse("<Text>{items.map(x => x * 2).join(', ')}</Text>");
47
47
 
48
48
  expect(result.type).toBe("Program");
49
49
  expect(result.body[0].type).toBe("ExpressionStatement");
50
50
  });
51
51
 
52
52
  it("should parse JSX with attributes", () => {
53
- const result = parse('<Text x={10} y="hello" z={data.value}>Content</Text>');
53
+ const result = parse('<Text x={10} y="hello" z={value}>Content</Text>');
54
54
 
55
55
  expect(result.type).toBe("Program");
56
56
  expect(result.body[0].type).toBe("ExpressionStatement");
57
57
  });
58
58
 
59
59
  it("should parse self-closing JSX elements", () => {
60
- const result = parse("<Input value={data.name} />");
60
+ const result = parse("<Input value={name} />");
61
61
 
62
62
  expect(result.type).toBe("Program");
63
63
  expect(result.body[0].type).toBe("ExpressionStatement");
64
64
  });
65
65
 
66
66
  it("should parse JSX with conditional expressions", () => {
67
- const result = parse("<Text>{data.show ? 'visible' : 'hidden'}</Text>");
67
+ const result = parse("<Text>{show ? 'visible' : 'hidden'}</Text>");
68
68
 
69
69
  expect(result.type).toBe("Program");
70
70
  expect(result.body[0].type).toBe("ExpressionStatement");
@@ -1,12 +1,13 @@
1
1
  import { Parser, type Program } from "acorn";
2
- import jsx from "acorn-jsx";
2
+ import { tsPlugin } from "acorn-typescript";
3
3
  import { ParseError } from "./parse-error.js";
4
4
 
5
- const JSXParser = Parser.extend(jsx());
5
+ // @ts-expect-error acorn-typescript jsx option types mismatch
6
+ const TSXParser = Parser.extend(tsPlugin({ jsx: true }));
6
7
 
7
8
  export function parse(source: string): Program {
8
9
  try {
9
- return JSXParser.parse(source, {
10
+ return TSXParser.parse(source, {
10
11
  ecmaVersion: "latest",
11
12
  sourceType: "module",
12
13
  locations: true,