safe-mdx 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -3
- package/dist/dynamic-esm-component.d.ts +6 -0
- package/dist/dynamic-esm-component.d.ts.map +1 -0
- package/dist/dynamic-esm-component.js +58 -0
- package/dist/dynamic-esm-component.js.map +1 -0
- package/dist/esm-parser.d.ts +19 -0
- package/dist/esm-parser.d.ts.map +1 -0
- package/dist/esm-parser.js +69 -0
- package/dist/esm-parser.js.map +1 -0
- package/dist/esm-parser.test.d.ts +2 -0
- package/dist/esm-parser.test.d.ts.map +1 -0
- package/dist/esm-parser.test.js +124 -0
- package/dist/esm-parser.test.js.map +1 -0
- package/dist/safe-mdx.bench.js +6 -0
- package/dist/safe-mdx.bench.js.map +1 -1
- package/dist/safe-mdx.d.ts +8 -2
- package/dist/safe-mdx.d.ts.map +1 -1
- package/dist/safe-mdx.js +291 -83
- package/dist/safe-mdx.js.map +1 -1
- package/dist/safe-mdx.test.js +593 -24
- package/dist/safe-mdx.test.js.map +1 -1
- package/package.json +3 -1
- package/src/dynamic-esm-component.tsx +85 -0
- package/src/esm-parser.test.ts +141 -0
- package/src/esm-parser.ts +89 -0
- package/src/safe-mdx.bench.tsx +6 -0
- package/src/safe-mdx.test.tsx +648 -24
- package/src/safe-mdx.tsx +405 -120
package/src/safe-mdx.test.tsx
CHANGED
|
@@ -17,9 +17,9 @@ const components = {
|
|
|
17
17
|
},
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function render(code, componentPropsSchema?: ComponentPropsSchema) {
|
|
20
|
+
function render(code, componentPropsSchema?: ComponentPropsSchema, allowClientEsmImports?: boolean) {
|
|
21
21
|
const mdast = mdxParse(code)
|
|
22
|
-
const visitor = new MdastToJsx({ markdown: code, mdast, components, componentPropsSchema })
|
|
22
|
+
const visitor = new MdastToJsx({ markdown: code, mdast, components, componentPropsSchema, allowClientEsmImports })
|
|
23
23
|
const result = visitor.run()
|
|
24
24
|
const html = renderToStaticMarkup(result)
|
|
25
25
|
// console.log(JSON.stringify(result, null, 2))
|
|
@@ -37,6 +37,20 @@ test('htmlToJsx', () => {
|
|
|
37
37
|
)
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
+
test('reference links with titles', () => {
|
|
41
|
+
const code = dedent`
|
|
42
|
+
> **Heads-up:** Check the [API docs][1] for more info.
|
|
43
|
+
|
|
44
|
+
Visit [Slack developers][2] for details.
|
|
45
|
+
|
|
46
|
+
[1]: https://api.slack.com/methods/search.messages "search.messages method - Slack API"
|
|
47
|
+
[2]: https://slack.dev/secure-data-connectivity/ "Secure Data Connectivity - Slack Developers"
|
|
48
|
+
`
|
|
49
|
+
|
|
50
|
+
const { html } = render(code)
|
|
51
|
+
expect(html).toMatchInlineSnapshot(`"<blockquote><p><strong>Heads-up:</strong> Check the <a href="https://api.slack.com/methods/search.messages" title="search.messages method - Slack API">API docs</a> for more info.</p></blockquote><p>Visit <a href="https://slack.dev/secure-data-connectivity/" title="Secure Data Connectivity - Slack Developers">Slack developers</a> for details.</p>"`)
|
|
52
|
+
})
|
|
53
|
+
|
|
40
54
|
test('markdown inside jsx', () => {
|
|
41
55
|
const code = dedent`
|
|
42
56
|
# Hello
|
|
@@ -512,36 +526,42 @@ test('props parsing', () => {
|
|
|
512
526
|
{
|
|
513
527
|
"errors": [
|
|
514
528
|
{
|
|
515
|
-
"line":
|
|
516
|
-
"message": "
|
|
529
|
+
"line": 8,
|
|
530
|
+
"message": "Failed to evaluate expression attribute: expression2={Boolean(1)}",
|
|
517
531
|
},
|
|
518
532
|
{
|
|
519
533
|
"line": 8,
|
|
520
|
-
"message": "Expressions in jsx
|
|
534
|
+
"message": "Expressions in jsx prop not evaluated: (expression2={Boolean(1)})",
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
"line": 9,
|
|
538
|
+
"message": "Unsupported jsx component SomeComponent in attribute",
|
|
521
539
|
},
|
|
522
540
|
{
|
|
523
541
|
"line": 9,
|
|
524
|
-
"message": "
|
|
542
|
+
"message": "Failed to evaluate expression attribute: jsx={<SomeComponent />}",
|
|
525
543
|
},
|
|
526
544
|
{
|
|
527
|
-
"line":
|
|
528
|
-
"message": "Expressions in jsx
|
|
545
|
+
"line": 9,
|
|
546
|
+
"message": "Expressions in jsx prop not evaluated: (jsx={<SomeComponent />})",
|
|
529
547
|
},
|
|
530
548
|
],
|
|
531
549
|
"html": "<h1><p>hi</p></h1>",
|
|
532
550
|
"result": <React.Fragment>
|
|
533
551
|
<Heading
|
|
534
|
-
backTick="some
|
|
552
|
+
backTick="some undefined value"
|
|
535
553
|
boolean={false}
|
|
536
554
|
doublequote="a " string"
|
|
555
|
+
expression1={4}
|
|
537
556
|
null={null}
|
|
538
557
|
num={2}
|
|
539
|
-
quote="a
|
|
558
|
+
quote="a ' string"
|
|
540
559
|
someJson={
|
|
541
560
|
{
|
|
542
561
|
"a": 1,
|
|
543
562
|
}
|
|
544
563
|
}
|
|
564
|
+
spread={true}
|
|
545
565
|
>
|
|
546
566
|
<p>
|
|
547
567
|
hi
|
|
@@ -551,6 +571,132 @@ test('props parsing', () => {
|
|
|
551
571
|
}
|
|
552
572
|
`)
|
|
553
573
|
})
|
|
574
|
+
|
|
575
|
+
test('jsx attributes with arithmetic expressions', () => {
|
|
576
|
+
expect(
|
|
577
|
+
render(dedent`
|
|
578
|
+
<Heading
|
|
579
|
+
level={1 + 2}
|
|
580
|
+
width={100 * 2}
|
|
581
|
+
active={!false}
|
|
582
|
+
comparison={5 > 3}
|
|
583
|
+
concat={"hello " + "world"}
|
|
584
|
+
/>
|
|
585
|
+
`),
|
|
586
|
+
).toMatchInlineSnapshot(`
|
|
587
|
+
{
|
|
588
|
+
"errors": [],
|
|
589
|
+
"html": "<h1></h1>",
|
|
590
|
+
"result": <React.Fragment>
|
|
591
|
+
<Heading
|
|
592
|
+
active={true}
|
|
593
|
+
comparison={true}
|
|
594
|
+
concat="hello world"
|
|
595
|
+
level={3}
|
|
596
|
+
width={200}
|
|
597
|
+
/>
|
|
598
|
+
</React.Fragment>,
|
|
599
|
+
}
|
|
600
|
+
`)
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
test('jsx attributes with complex objects and arrays', () => {
|
|
604
|
+
expect(
|
|
605
|
+
render(dedent`
|
|
606
|
+
<Heading
|
|
607
|
+
simpleArray={[1, 2, 3]}
|
|
608
|
+
stringArray={["one", "two", "three"]}
|
|
609
|
+
mixedArray={[1, "two", true, null]}
|
|
610
|
+
simpleObject={{name: "John", age: 30}}
|
|
611
|
+
nestedObject={{
|
|
612
|
+
user: {
|
|
613
|
+
name: "Alice",
|
|
614
|
+
preferences: {
|
|
615
|
+
theme: "dark",
|
|
616
|
+
lang: "en"
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
settings: {
|
|
620
|
+
notifications: true,
|
|
621
|
+
emails: ["alice@example.com", "alice.work@example.com"]
|
|
622
|
+
}
|
|
623
|
+
}}
|
|
624
|
+
arrayOfObjects={[
|
|
625
|
+
{id: 1, name: "Item 1"},
|
|
626
|
+
{id: 2, name: "Item 2"}
|
|
627
|
+
]}
|
|
628
|
+
/>
|
|
629
|
+
`),
|
|
630
|
+
).toMatchInlineSnapshot(`
|
|
631
|
+
{
|
|
632
|
+
"errors": [],
|
|
633
|
+
"html": "<h1></h1>",
|
|
634
|
+
"result": <React.Fragment>
|
|
635
|
+
<Heading
|
|
636
|
+
arrayOfObjects={
|
|
637
|
+
[
|
|
638
|
+
{
|
|
639
|
+
"id": 1,
|
|
640
|
+
"name": "Item 1",
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
"id": 2,
|
|
644
|
+
"name": "Item 2",
|
|
645
|
+
},
|
|
646
|
+
]
|
|
647
|
+
}
|
|
648
|
+
mixedArray={
|
|
649
|
+
[
|
|
650
|
+
1,
|
|
651
|
+
"two",
|
|
652
|
+
true,
|
|
653
|
+
null,
|
|
654
|
+
]
|
|
655
|
+
}
|
|
656
|
+
nestedObject={
|
|
657
|
+
{
|
|
658
|
+
"settings": {
|
|
659
|
+
"emails": [
|
|
660
|
+
"alice@example.com",
|
|
661
|
+
"alice.work@example.com",
|
|
662
|
+
],
|
|
663
|
+
"notifications": true,
|
|
664
|
+
},
|
|
665
|
+
"user": {
|
|
666
|
+
"name": "Alice",
|
|
667
|
+
"preferences": {
|
|
668
|
+
"lang": "en",
|
|
669
|
+
"theme": "dark",
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
simpleArray={
|
|
675
|
+
[
|
|
676
|
+
1,
|
|
677
|
+
2,
|
|
678
|
+
3,
|
|
679
|
+
]
|
|
680
|
+
}
|
|
681
|
+
simpleObject={
|
|
682
|
+
{
|
|
683
|
+
"age": 30,
|
|
684
|
+
"name": "John",
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
stringArray={
|
|
688
|
+
[
|
|
689
|
+
"one",
|
|
690
|
+
"two",
|
|
691
|
+
"three",
|
|
692
|
+
]
|
|
693
|
+
}
|
|
694
|
+
/>
|
|
695
|
+
</React.Fragment>,
|
|
696
|
+
}
|
|
697
|
+
`)
|
|
698
|
+
})
|
|
699
|
+
|
|
554
700
|
test('breaks', () => {
|
|
555
701
|
expect(
|
|
556
702
|
render(dedent`
|
|
@@ -1047,7 +1193,7 @@ test('kitchen sink', () => {
|
|
|
1047
1193
|
|
|
1048
1194
|
[arbitrary case-insensitive reference text]: https://www.mozilla.org
|
|
1049
1195
|
[1]: http://slashdot.org
|
|
1050
|
-
[link text itself]: http://www.reddit.com</code></pre><p><a href="https://www.google.com" title="">I'm an inline-style link</a></p><p><a href="https://www.google.com" title="Google's Homepage">I'm an inline-style link with title</a></p><p><a href="https://www.mozilla.org">I'm a reference-style link</a></p><p><a href="../blob/master/LICENSE" title="">I'm a relative reference to a repository file</a></p><p><a href="http://slashdot.org">You can use numbers for reference-style link definitions</a></p><p>Or leave it empty and use the <a href="http://www.reddit.com">link text itself</a>.</p><p>URLs and URLs in angle brackets will automatically get turned into links.
|
|
1196
|
+
[link text itself]: http://www.reddit.com</code></pre><p><a href="https://www.google.com" title="">I'm an inline-style link</a></p><p><a href="https://www.google.com" title="Google's Homepage">I'm an inline-style link with title</a></p><p><a href="https://www.mozilla.org" title="">I'm a reference-style link</a></p><p><a href="../blob/master/LICENSE" title="">I'm a relative reference to a repository file</a></p><p><a href="http://slashdot.org" title="">You can use numbers for reference-style link definitions</a></p><p>Or leave it empty and use the <a href="http://www.reddit.com" title="">link text itself</a>.</p><p>URLs and URLs in angle brackets will automatically get turned into links.
|
|
1051
1197
|
<a href="http://www.example.com" title="">http://www.example.com</a> and sometimes
|
|
1052
1198
|
example.com (but not on Github, for example).</p><p>Some text to show that the reference links can follow later.</p><a name="images"></a><h2>Images</h2><pre><code class="language-no-highlight">Here's our logo (hover to see the title text):
|
|
1053
1199
|
|
|
@@ -1546,6 +1692,7 @@ test('kitchen sink', () => {
|
|
|
1546
1692
|
<p>
|
|
1547
1693
|
<a
|
|
1548
1694
|
href="https://www.mozilla.org"
|
|
1695
|
+
title=""
|
|
1549
1696
|
>
|
|
1550
1697
|
I'm a reference-style link
|
|
1551
1698
|
</a>
|
|
@@ -1561,6 +1708,7 @@ test('kitchen sink', () => {
|
|
|
1561
1708
|
<p>
|
|
1562
1709
|
<a
|
|
1563
1710
|
href="http://slashdot.org"
|
|
1711
|
+
title=""
|
|
1564
1712
|
>
|
|
1565
1713
|
You can use numbers for reference-style link definitions
|
|
1566
1714
|
</a>
|
|
@@ -1569,6 +1717,7 @@ test('kitchen sink', () => {
|
|
|
1569
1717
|
Or leave it empty and use the
|
|
1570
1718
|
<a
|
|
1571
1719
|
href="http://www.reddit.com"
|
|
1720
|
+
title=""
|
|
1572
1721
|
>
|
|
1573
1722
|
link text itself
|
|
1574
1723
|
</a>
|
|
@@ -2377,6 +2526,82 @@ test('component props schema validation with zod', () => {
|
|
|
2377
2526
|
`)
|
|
2378
2527
|
})
|
|
2379
2528
|
|
|
2529
|
+
test('mdx expressions evaluation', () => {
|
|
2530
|
+
expect(
|
|
2531
|
+
render(dedent`
|
|
2532
|
+
# Expression Test
|
|
2533
|
+
|
|
2534
|
+
Simple math: {1 + 2}
|
|
2535
|
+
|
|
2536
|
+
<Heading>
|
|
2537
|
+
Inside JSX: {3 * 4}
|
|
2538
|
+
</Heading>
|
|
2539
|
+
|
|
2540
|
+
Boolean: {true}
|
|
2541
|
+
String concat: {"hello" + " world"}
|
|
2542
|
+
`),
|
|
2543
|
+
).toMatchInlineSnapshot(`
|
|
2544
|
+
{
|
|
2545
|
+
"errors": [],
|
|
2546
|
+
"html": "<h1>Expression Test</h1><p>Simple math: 3</p><h1><p>Inside JSX: 12</p></h1><p>Boolean:
|
|
2547
|
+
String concat: hello world</p>",
|
|
2548
|
+
"result": <React.Fragment>
|
|
2549
|
+
<h1>
|
|
2550
|
+
Expression Test
|
|
2551
|
+
</h1>
|
|
2552
|
+
<p>
|
|
2553
|
+
Simple math:
|
|
2554
|
+
3
|
|
2555
|
+
</p>
|
|
2556
|
+
<Heading>
|
|
2557
|
+
<p>
|
|
2558
|
+
Inside JSX:
|
|
2559
|
+
12
|
|
2560
|
+
</p>
|
|
2561
|
+
</Heading>
|
|
2562
|
+
<p>
|
|
2563
|
+
Boolean:
|
|
2564
|
+
true
|
|
2565
|
+
|
|
2566
|
+
String concat:
|
|
2567
|
+
hello world
|
|
2568
|
+
</p>
|
|
2569
|
+
</React.Fragment>,
|
|
2570
|
+
}
|
|
2571
|
+
`)
|
|
2572
|
+
})
|
|
2573
|
+
|
|
2574
|
+
test('mdx expressions with unsupported functions', () => {
|
|
2575
|
+
expect(
|
|
2576
|
+
render(dedent`
|
|
2577
|
+
Math function: {Math.max(5, 10)}
|
|
2578
|
+
Console: {console.log("test")}
|
|
2579
|
+
`),
|
|
2580
|
+
).toMatchInlineSnapshot(`
|
|
2581
|
+
{
|
|
2582
|
+
"errors": [
|
|
2583
|
+
{
|
|
2584
|
+
"line": 1,
|
|
2585
|
+
"message": "Failed to evaluate expression: Math.max(5, 10)",
|
|
2586
|
+
},
|
|
2587
|
+
{
|
|
2588
|
+
"line": 2,
|
|
2589
|
+
"message": "Failed to evaluate expression: console.log("test")",
|
|
2590
|
+
},
|
|
2591
|
+
],
|
|
2592
|
+
"html": "<p>Math function:
|
|
2593
|
+
Console: </p>",
|
|
2594
|
+
"result": <React.Fragment>
|
|
2595
|
+
<p>
|
|
2596
|
+
Math function:
|
|
2597
|
+
|
|
2598
|
+
Console:
|
|
2599
|
+
</p>
|
|
2600
|
+
</React.Fragment>,
|
|
2601
|
+
}
|
|
2602
|
+
`)
|
|
2603
|
+
})
|
|
2604
|
+
|
|
2380
2605
|
test('schema validation without errors', () => {
|
|
2381
2606
|
const HeadingSchema = z.object({
|
|
2382
2607
|
level: z.number().min(1).max(6),
|
|
@@ -2471,29 +2696,428 @@ test('validation error includes schema path', () => {
|
|
|
2471
2696
|
"errors": [
|
|
2472
2697
|
{
|
|
2473
2698
|
"line": 1,
|
|
2474
|
-
"message": "
|
|
2699
|
+
"message": "Invalid props for component "Heading" at "user.age": Number must be greater than or equal to 0",
|
|
2700
|
+
"schemaPath": "user.age",
|
|
2475
2701
|
},
|
|
2476
2702
|
{
|
|
2477
2703
|
"line": 1,
|
|
2478
|
-
"message": "
|
|
2479
|
-
|
|
2480
|
-
{
|
|
2481
|
-
"line": 1,
|
|
2482
|
-
"message": "Invalid props for component "Heading" at "user": Required",
|
|
2483
|
-
"schemaPath": "user",
|
|
2484
|
-
},
|
|
2485
|
-
{
|
|
2486
|
-
"line": 1,
|
|
2487
|
-
"message": "Invalid props for component "Heading" at "settings": Required",
|
|
2488
|
-
"schemaPath": "settings",
|
|
2704
|
+
"message": "Invalid props for component "Heading" at "settings.theme": Invalid enum value. Expected 'light' | 'dark', received 'invalid'",
|
|
2705
|
+
"schemaPath": "settings.theme",
|
|
2489
2706
|
},
|
|
2490
2707
|
],
|
|
2491
2708
|
"html": "<h1>Complex validation</h1>",
|
|
2492
2709
|
"result": <React.Fragment>
|
|
2493
|
-
<Heading
|
|
2710
|
+
<Heading
|
|
2711
|
+
settings={
|
|
2712
|
+
{
|
|
2713
|
+
"theme": "invalid",
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
user={
|
|
2717
|
+
{
|
|
2718
|
+
"age": -1,
|
|
2719
|
+
"name": "test",
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
>
|
|
2494
2723
|
Complex validation
|
|
2495
2724
|
</Heading>
|
|
2496
2725
|
</React.Fragment>,
|
|
2497
2726
|
}
|
|
2498
2727
|
`)
|
|
2499
2728
|
})
|
|
2729
|
+
|
|
2730
|
+
test('mdxJsxExpressionAttribute spread syntax', () => {
|
|
2731
|
+
expect(
|
|
2732
|
+
render(dedent`
|
|
2733
|
+
<Heading
|
|
2734
|
+
{...{key: '1', level: 2}}
|
|
2735
|
+
title="test"
|
|
2736
|
+
>
|
|
2737
|
+
Content with spread
|
|
2738
|
+
</Heading>
|
|
2739
|
+
`),
|
|
2740
|
+
).toMatchInlineSnapshot(`
|
|
2741
|
+
{
|
|
2742
|
+
"errors": [],
|
|
2743
|
+
"html": "<h1><p>Content with spread</p></h1>",
|
|
2744
|
+
"result": <React.Fragment>
|
|
2745
|
+
<Heading
|
|
2746
|
+
level={2}
|
|
2747
|
+
title="test"
|
|
2748
|
+
>
|
|
2749
|
+
<p>
|
|
2750
|
+
Content with spread
|
|
2751
|
+
</p>
|
|
2752
|
+
</Heading>
|
|
2753
|
+
</React.Fragment>,
|
|
2754
|
+
}
|
|
2755
|
+
`)
|
|
2756
|
+
})
|
|
2757
|
+
|
|
2758
|
+
test('mdxJsxExpressionAttribute complex spread cases', () => {
|
|
2759
|
+
expect(
|
|
2760
|
+
render(dedent`
|
|
2761
|
+
<Heading
|
|
2762
|
+
{...{
|
|
2763
|
+
level: 3,
|
|
2764
|
+
active: true,
|
|
2765
|
+
disabled: false,
|
|
2766
|
+
count: 42,
|
|
2767
|
+
title: "spread title",
|
|
2768
|
+
nested: {
|
|
2769
|
+
prop: "value"
|
|
2770
|
+
}
|
|
2771
|
+
}}
|
|
2772
|
+
>
|
|
2773
|
+
Complex spread test
|
|
2774
|
+
</Heading>
|
|
2775
|
+
|
|
2776
|
+
<Cards
|
|
2777
|
+
{...{style: {color: "red", fontSize: "16px"}}}
|
|
2778
|
+
{...{className: "test-class", id: "test-id"}}
|
|
2779
|
+
>
|
|
2780
|
+
Multiple spreads
|
|
2781
|
+
</Cards>
|
|
2782
|
+
`),
|
|
2783
|
+
).toMatchInlineSnapshot(`
|
|
2784
|
+
{
|
|
2785
|
+
"errors": [],
|
|
2786
|
+
"html": "<h1><p>Complex spread test</p></h1><div><p>Multiple spreads</p></div>",
|
|
2787
|
+
"result": <React.Fragment>
|
|
2788
|
+
<Heading
|
|
2789
|
+
active={true}
|
|
2790
|
+
count={42}
|
|
2791
|
+
disabled={false}
|
|
2792
|
+
level={3}
|
|
2793
|
+
nested={
|
|
2794
|
+
{
|
|
2795
|
+
"prop": "value",
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
title="spread title"
|
|
2799
|
+
>
|
|
2800
|
+
<p>
|
|
2801
|
+
Complex spread test
|
|
2802
|
+
</p>
|
|
2803
|
+
</Heading>
|
|
2804
|
+
<Cards
|
|
2805
|
+
className="test-class"
|
|
2806
|
+
id="test-id"
|
|
2807
|
+
style={
|
|
2808
|
+
{
|
|
2809
|
+
"color": "red",
|
|
2810
|
+
"fontSize": "16px",
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
>
|
|
2814
|
+
<p>
|
|
2815
|
+
Multiple spreads
|
|
2816
|
+
</p>
|
|
2817
|
+
</Cards>
|
|
2818
|
+
</React.Fragment>,
|
|
2819
|
+
}
|
|
2820
|
+
`)
|
|
2821
|
+
})
|
|
2822
|
+
|
|
2823
|
+
test('mdxJsxExpressionAttribute edge cases', () => {
|
|
2824
|
+
expect(
|
|
2825
|
+
render(dedent`
|
|
2826
|
+
<Heading {...{}} title="empty spread">Empty spread</Heading>
|
|
2827
|
+
|
|
2828
|
+
<Heading {...{null: null, undefined: undefined}} title="null/undefined values">Null/undefined</Heading>
|
|
2829
|
+
|
|
2830
|
+
<Heading {...{array: [1, 2, 3], object: {nested: true}}} title="complex values">Complex types</Heading>
|
|
2831
|
+
`),
|
|
2832
|
+
).toMatchInlineSnapshot(`
|
|
2833
|
+
{
|
|
2834
|
+
"errors": [
|
|
2835
|
+
{
|
|
2836
|
+
"line": 3,
|
|
2837
|
+
"message": "Failed to evaluate expression attribute: ...{null: null, undefined: undefined}",
|
|
2838
|
+
},
|
|
2839
|
+
],
|
|
2840
|
+
"html": "<h1>Empty spread</h1><h1>Null/undefined</h1><h1>Complex types</h1>",
|
|
2841
|
+
"result": <React.Fragment>
|
|
2842
|
+
<Heading
|
|
2843
|
+
title="empty spread"
|
|
2844
|
+
>
|
|
2845
|
+
Empty spread
|
|
2846
|
+
</Heading>
|
|
2847
|
+
<Heading
|
|
2848
|
+
title="null/undefined values"
|
|
2849
|
+
>
|
|
2850
|
+
Null/undefined
|
|
2851
|
+
</Heading>
|
|
2852
|
+
<Heading
|
|
2853
|
+
array={
|
|
2854
|
+
[
|
|
2855
|
+
1,
|
|
2856
|
+
2,
|
|
2857
|
+
3,
|
|
2858
|
+
]
|
|
2859
|
+
}
|
|
2860
|
+
object={
|
|
2861
|
+
{
|
|
2862
|
+
"nested": true,
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
title="complex values"
|
|
2866
|
+
>
|
|
2867
|
+
Complex types
|
|
2868
|
+
</Heading>
|
|
2869
|
+
</React.Fragment>,
|
|
2870
|
+
}
|
|
2871
|
+
`)
|
|
2872
|
+
})
|
|
2873
|
+
|
|
2874
|
+
test('ESM imports from https URLs', () => {
|
|
2875
|
+
const code = dedent`
|
|
2876
|
+
import Button from 'https://esm.sh/some-button-component'
|
|
2877
|
+
import { Card, Modal } from 'https://esm.sh/some-ui-library'
|
|
2878
|
+
|
|
2879
|
+
# Hello
|
|
2880
|
+
|
|
2881
|
+
<Button>Click me</Button>
|
|
2882
|
+
|
|
2883
|
+
<Card title="Test Card">
|
|
2884
|
+
Content inside card
|
|
2885
|
+
</Card>
|
|
2886
|
+
|
|
2887
|
+
<Modal open={true}>
|
|
2888
|
+
Modal content
|
|
2889
|
+
</Modal>
|
|
2890
|
+
`
|
|
2891
|
+
|
|
2892
|
+
const mdast = mdxParse(code)
|
|
2893
|
+
const visitor = new MdastToJsx({ markdown: code, mdast, components, allowClientEsmImports: true })
|
|
2894
|
+
const result = visitor.run()
|
|
2895
|
+
|
|
2896
|
+
// Check that imports were parsed correctly
|
|
2897
|
+
expect(visitor.esmImports.size).toBe(3)
|
|
2898
|
+
expect(visitor.esmImports.get('Button')).toBe('https://esm.sh/some-button-component')
|
|
2899
|
+
expect(visitor.esmImports.get('Card')).toBe('https://esm.sh/some-ui-library#Card')
|
|
2900
|
+
expect(visitor.esmImports.get('Modal')).toBe('https://esm.sh/some-ui-library#Modal')
|
|
2901
|
+
|
|
2902
|
+
// Since these are dynamic imports that only work on client, the server render should return null
|
|
2903
|
+
const html = renderToStaticMarkup(result)
|
|
2904
|
+
expect(html).toMatchInlineSnapshot(`"<h1>Hello</h1>"`)
|
|
2905
|
+
|
|
2906
|
+
expect(visitor.errors).toEqual([])
|
|
2907
|
+
})
|
|
2908
|
+
|
|
2909
|
+
test('ESM imports error handling', () => {
|
|
2910
|
+
const code = dedent`
|
|
2911
|
+
import Button from 'file:///local/path'
|
|
2912
|
+
import Component from './relative/path'
|
|
2913
|
+
|
|
2914
|
+
# Test
|
|
2915
|
+
|
|
2916
|
+
<Button>Local import should not work</Button>
|
|
2917
|
+
<Component>Relative import should not work</Component>
|
|
2918
|
+
`
|
|
2919
|
+
|
|
2920
|
+
const mdast = mdxParse(code)
|
|
2921
|
+
const visitor = new MdastToJsx({ markdown: code, mdast, components, allowClientEsmImports: true })
|
|
2922
|
+
const result = visitor.run()
|
|
2923
|
+
|
|
2924
|
+
// Only https imports should be processed
|
|
2925
|
+
expect(visitor.esmImports.size).toBe(0)
|
|
2926
|
+
|
|
2927
|
+
// Should have 4 errors: 2 for invalid imports, 2 for unsupported components
|
|
2928
|
+
expect(visitor.errors.length).toBe(4)
|
|
2929
|
+
|
|
2930
|
+
// First two errors are for invalid imports
|
|
2931
|
+
expect(visitor.errors[0].message).toContain('Invalid import URL')
|
|
2932
|
+
expect(visitor.errors[1].message).toContain('Invalid import URL')
|
|
2933
|
+
|
|
2934
|
+
// Last two errors are for unsupported components
|
|
2935
|
+
expect(visitor.errors[2].message).toContain('Unsupported jsx component Button')
|
|
2936
|
+
expect(visitor.errors[3].message).toContain('Unsupported jsx component Component')
|
|
2937
|
+
})
|
|
2938
|
+
|
|
2939
|
+
test('jsx components in attributes', () => {
|
|
2940
|
+
const code = dedent`
|
|
2941
|
+
# JSX Components in Attributes
|
|
2942
|
+
|
|
2943
|
+
<Heading icon={<span>👋</span>} level={1}>
|
|
2944
|
+
Hello World
|
|
2945
|
+
</Heading>
|
|
2946
|
+
|
|
2947
|
+
<Cards items={<div>Item 1</div>}>
|
|
2948
|
+
Some content
|
|
2949
|
+
</Cards>
|
|
2950
|
+
`
|
|
2951
|
+
|
|
2952
|
+
const { result, errors, html } = render(code)
|
|
2953
|
+
|
|
2954
|
+
// Should not have any errors
|
|
2955
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
2956
|
+
|
|
2957
|
+
// Should render correctly
|
|
2958
|
+
expect(html).toMatchInlineSnapshot(`"<h1>JSX Components in Attributes</h1><h1><p>Hello World</p></h1><div><p>Some content</p></div>"`)
|
|
2959
|
+
|
|
2960
|
+
expect(result).toMatchInlineSnapshot(`
|
|
2961
|
+
<React.Fragment>
|
|
2962
|
+
<h1>
|
|
2963
|
+
JSX Components in Attributes
|
|
2964
|
+
</h1>
|
|
2965
|
+
<Heading
|
|
2966
|
+
icon={
|
|
2967
|
+
<span>
|
|
2968
|
+
👋
|
|
2969
|
+
</span>
|
|
2970
|
+
}
|
|
2971
|
+
level={1}
|
|
2972
|
+
>
|
|
2973
|
+
<p>
|
|
2974
|
+
Hello World
|
|
2975
|
+
</p>
|
|
2976
|
+
</Heading>
|
|
2977
|
+
<Cards
|
|
2978
|
+
items={
|
|
2979
|
+
<div>
|
|
2980
|
+
Item 1
|
|
2981
|
+
</div>
|
|
2982
|
+
}
|
|
2983
|
+
>
|
|
2984
|
+
<p>
|
|
2985
|
+
Some content
|
|
2986
|
+
</p>
|
|
2987
|
+
</Cards>
|
|
2988
|
+
</React.Fragment>
|
|
2989
|
+
`)
|
|
2990
|
+
})
|
|
2991
|
+
|
|
2992
|
+
test('jsx components in attributes with ESM imports', () => {
|
|
2993
|
+
const code = dedent`
|
|
2994
|
+
import Button from 'https://esm.sh/some-button-component'
|
|
2995
|
+
import { Icon } from 'https://esm.sh/some-icon-library'
|
|
2996
|
+
|
|
2997
|
+
# ESM Components in Attributes
|
|
2998
|
+
|
|
2999
|
+
<Heading icon={<Icon name="star" />} level={1}>
|
|
3000
|
+
Hello World
|
|
3001
|
+
</Heading>
|
|
3002
|
+
|
|
3003
|
+
<Cards actionButton={<Button>Click me</Button>}>
|
|
3004
|
+
Some content
|
|
3005
|
+
</Cards>
|
|
3006
|
+
`
|
|
3007
|
+
|
|
3008
|
+
const { result, errors, html } = render(code, undefined, true)
|
|
3009
|
+
|
|
3010
|
+
// Should not have any errors
|
|
3011
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
3012
|
+
|
|
3013
|
+
// Should render correctly - ESM components should be wrapped in DynamicEsmComponent
|
|
3014
|
+
expect(html).toMatchInlineSnapshot(`"<h1>ESM Components in Attributes</h1><h1><p>Hello World</p></h1><div><p>Some content</p></div>"`)
|
|
3015
|
+
|
|
3016
|
+
expect(result).toMatchInlineSnapshot(`
|
|
3017
|
+
<React.Fragment>
|
|
3018
|
+
<h1>
|
|
3019
|
+
ESM Components in Attributes
|
|
3020
|
+
</h1>
|
|
3021
|
+
<Heading
|
|
3022
|
+
icon={
|
|
3023
|
+
<DynamicEsmComponent
|
|
3024
|
+
componentName="Icon"
|
|
3025
|
+
importUrl="https://esm.sh/some-icon-library"
|
|
3026
|
+
name="star"
|
|
3027
|
+
/>
|
|
3028
|
+
}
|
|
3029
|
+
level={1}
|
|
3030
|
+
>
|
|
3031
|
+
<p>
|
|
3032
|
+
Hello World
|
|
3033
|
+
</p>
|
|
3034
|
+
</Heading>
|
|
3035
|
+
<Cards
|
|
3036
|
+
actionButton={
|
|
3037
|
+
<DynamicEsmComponent
|
|
3038
|
+
componentName="default"
|
|
3039
|
+
importUrl="https://esm.sh/some-button-component"
|
|
3040
|
+
>
|
|
3041
|
+
Click me
|
|
3042
|
+
</DynamicEsmComponent>
|
|
3043
|
+
}
|
|
3044
|
+
>
|
|
3045
|
+
<p>
|
|
3046
|
+
Some content
|
|
3047
|
+
</p>
|
|
3048
|
+
</Cards>
|
|
3049
|
+
</React.Fragment>
|
|
3050
|
+
`)
|
|
3051
|
+
})
|
|
3052
|
+
|
|
3053
|
+
test('ESM imports disabled by default', () => {
|
|
3054
|
+
const code = dedent`
|
|
3055
|
+
import Button from 'https://esm.sh/some-button-component'
|
|
3056
|
+
|
|
3057
|
+
# Test Default Behavior
|
|
3058
|
+
|
|
3059
|
+
<Button>This should not work</Button>
|
|
3060
|
+
`
|
|
3061
|
+
|
|
3062
|
+
const { result, errors, html } = render(code) // No allowClientEsmImports flag
|
|
3063
|
+
|
|
3064
|
+
// ESM imports should not be processed when disabled
|
|
3065
|
+
const mdast = mdxParse(code)
|
|
3066
|
+
const visitor = new MdastToJsx({ markdown: code, mdast, components }) // Default allowClientEsmImports: false
|
|
3067
|
+
expect(visitor.esmImports.size).toBe(0)
|
|
3068
|
+
|
|
3069
|
+
// Should have error for unsupported component
|
|
3070
|
+
expect(errors.length).toBeGreaterThan(0)
|
|
3071
|
+
expect(errors.some(e => e.message.includes('Unsupported jsx component Button'))).toBe(true)
|
|
3072
|
+
|
|
3073
|
+
// Should render heading but not the button
|
|
3074
|
+
expect(html).toMatchInlineSnapshot(`"<h1>Test Default Behavior</h1>"`)
|
|
3075
|
+
})
|
|
3076
|
+
|
|
3077
|
+
test("jsx components in attributes error handling", () => {
|
|
3078
|
+
const code = dedent`
|
|
3079
|
+
# Error Handling Test
|
|
3080
|
+
|
|
3081
|
+
<Heading icon={<UnsupportedComponent />} level={1}>
|
|
3082
|
+
Hello World
|
|
3083
|
+
</Heading>
|
|
3084
|
+
`
|
|
3085
|
+
|
|
3086
|
+
const { result, errors, html } = render(code)
|
|
3087
|
+
|
|
3088
|
+
// Should have an error for the unsupported component
|
|
3089
|
+
expect(errors).toMatchInlineSnapshot(`
|
|
3090
|
+
[
|
|
3091
|
+
{
|
|
3092
|
+
"line": 3,
|
|
3093
|
+
"message": "Unsupported jsx component UnsupportedComponent in attribute",
|
|
3094
|
+
},
|
|
3095
|
+
{
|
|
3096
|
+
"line": 3,
|
|
3097
|
+
"message": "Failed to evaluate expression attribute: icon={<UnsupportedComponent />}",
|
|
3098
|
+
},
|
|
3099
|
+
{
|
|
3100
|
+
"line": 3,
|
|
3101
|
+
"message": "Expressions in jsx prop not evaluated: (icon={<UnsupportedComponent />})",
|
|
3102
|
+
},
|
|
3103
|
+
]
|
|
3104
|
+
`)
|
|
3105
|
+
|
|
3106
|
+
// Should still render the rest of the content
|
|
3107
|
+
expect(html).toMatchInlineSnapshot(`"<h1>Error Handling Test</h1><h1><p>Hello World</p></h1>"`)
|
|
3108
|
+
|
|
3109
|
+
expect(result).toMatchInlineSnapshot(`
|
|
3110
|
+
<React.Fragment>
|
|
3111
|
+
<h1>
|
|
3112
|
+
Error Handling Test
|
|
3113
|
+
</h1>
|
|
3114
|
+
<Heading
|
|
3115
|
+
level={1}
|
|
3116
|
+
>
|
|
3117
|
+
<p>
|
|
3118
|
+
Hello World
|
|
3119
|
+
</p>
|
|
3120
|
+
</Heading>
|
|
3121
|
+
</React.Fragment>
|
|
3122
|
+
`)
|
|
3123
|
+
})
|