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
|
@@ -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={
|
|
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(
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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>{
|
|
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>{}{
|
|
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={
|
|
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 " +
|
|
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={
|
|
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 {
|
|
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
|
});
|
package/src/evaluate/evaluate.ts
CHANGED
|
@@ -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
|
|
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", ...
|
|
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)
|
|
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
|
|
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
|
}
|
package/src/parse/parse.test.ts
CHANGED
|
@@ -43,28 +43,28 @@ describe("parse", () => {
|
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
it("should parse JSX with complex expressions", () => {
|
|
46
|
-
const result = parse("<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={
|
|
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={
|
|
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>{
|
|
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");
|
package/src/parse/parse.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Parser, type Program } from "acorn";
|
|
2
|
-
import
|
|
2
|
+
import { tsPlugin } from "acorn-typescript";
|
|
3
3
|
import { ParseError } from "./parse-error.js";
|
|
4
4
|
|
|
5
|
-
|
|
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
|
|
10
|
+
return TSXParser.parse(source, {
|
|
10
11
|
ecmaVersion: "latest",
|
|
11
12
|
sourceType: "module",
|
|
12
13
|
locations: true,
|