safe-mdx 1.0.4 → 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.
@@ -3,6 +3,7 @@ import dedent from 'dedent';
3
3
  import { htmlToJsx } from 'html-to-jsx-transform';
4
4
  import { renderToStaticMarkup } from 'react-dom/server';
5
5
  import { expect, test } from 'vitest';
6
+ import { z } from 'zod';
6
7
  import { mdxParse } from './parse.js';
7
8
  import { MdastToJsx, mdastBfs } from './safe-mdx.js';
8
9
  import { completeJsxTags } from './streaming.js';
@@ -14,9 +15,9 @@ const components = {
14
15
  return _jsx("div", { children: children });
15
16
  },
16
17
  };
17
- function render(code) {
18
+ function render(code, componentPropsSchema, allowClientEsmImports) {
18
19
  const mdast = mdxParse(code);
19
- const visitor = new MdastToJsx({ markdown: code, mdast, components });
20
+ const visitor = new MdastToJsx({ markdown: code, mdast, components, componentPropsSchema, allowClientEsmImports });
20
21
  const result = visitor.run();
21
22
  const html = renderToStaticMarkup(result);
22
23
  // console.log(JSON.stringify(result, null, 2))
@@ -28,6 +29,18 @@ test('htmlToJsx', () => {
28
29
  expect(htmlToJsx('before <p>text</p>')).toMatchInlineSnapshot(`"<>before <p>text</p></>"`);
29
30
  expect(htmlToJsx('<nonexisting>text</nonexisting>')).toMatchInlineSnapshot(`"<nonexisting>text</nonexisting>"`);
30
31
  });
32
+ test('reference links with titles', () => {
33
+ const code = dedent `
34
+ > **Heads-up:** Check the [API docs][1] for more info.
35
+
36
+ Visit [Slack developers][2] for details.
37
+
38
+ [1]: https://api.slack.com/methods/search.messages "search.messages method - Slack API"
39
+ [2]: https://slack.dev/secure-data-connectivity/ "Secure Data Connectivity - Slack Developers"
40
+ `;
41
+ const { html } = render(code);
42
+ 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>"`);
43
+ });
31
44
  test('markdown inside jsx', () => {
32
45
  const code = dedent `
33
46
  # Hello
@@ -441,6 +454,7 @@ test('missing components are ignored', () => {
441
454
  {
442
455
  "errors": [
443
456
  {
457
+ "line": 1,
444
458
  "message": "Unsupported jsx component MissingComponent",
445
459
  },
446
460
  ],
@@ -474,32 +488,42 @@ test('props parsing', () => {
474
488
  {
475
489
  "errors": [
476
490
  {
477
- "message": "Expressions in jsx props are not supported (expression1={1 + 3})",
491
+ "line": 8,
492
+ "message": "Failed to evaluate expression attribute: expression2={Boolean(1)}",
478
493
  },
479
494
  {
480
- "message": "Expressions in jsx props are not supported (expression2={Boolean(1)})",
495
+ "line": 8,
496
+ "message": "Expressions in jsx prop not evaluated: (expression2={Boolean(1)})",
481
497
  },
482
498
  {
483
- "message": "Expressions in jsx props are not supported (jsx={<SomeComponent />})",
499
+ "line": 9,
500
+ "message": "Unsupported jsx component SomeComponent in attribute",
484
501
  },
485
502
  {
486
- "message": "Expressions in jsx props are not supported (...{ spread: true })",
503
+ "line": 9,
504
+ "message": "Failed to evaluate expression attribute: jsx={<SomeComponent />}",
505
+ },
506
+ {
507
+ "line": 9,
508
+ "message": "Expressions in jsx prop not evaluated: (jsx={<SomeComponent />})",
487
509
  },
488
510
  ],
489
511
  "html": "<h1><p>hi</p></h1>",
490
512
  "result": <React.Fragment>
491
513
  <Heading
492
- backTick="some \${expr} value"
514
+ backTick="some undefined value"
493
515
  boolean={false}
494
516
  doublequote="a " string"
517
+ expression1={4}
495
518
  null={null}
496
519
  num={2}
497
- quote="a " string"
520
+ quote="a ' string"
498
521
  someJson={
499
522
  {
500
523
  "a": 1,
501
524
  }
502
525
  }
526
+ spread={true}
503
527
  >
504
528
  <p>
505
529
  hi
@@ -509,6 +533,125 @@ test('props parsing', () => {
509
533
  }
510
534
  `);
511
535
  });
536
+ test('jsx attributes with arithmetic expressions', () => {
537
+ expect(render(dedent `
538
+ <Heading
539
+ level={1 + 2}
540
+ width={100 * 2}
541
+ active={!false}
542
+ comparison={5 > 3}
543
+ concat={"hello " + "world"}
544
+ />
545
+ `)).toMatchInlineSnapshot(`
546
+ {
547
+ "errors": [],
548
+ "html": "<h1></h1>",
549
+ "result": <React.Fragment>
550
+ <Heading
551
+ active={true}
552
+ comparison={true}
553
+ concat="hello world"
554
+ level={3}
555
+ width={200}
556
+ />
557
+ </React.Fragment>,
558
+ }
559
+ `);
560
+ });
561
+ test('jsx attributes with complex objects and arrays', () => {
562
+ expect(render(dedent `
563
+ <Heading
564
+ simpleArray={[1, 2, 3]}
565
+ stringArray={["one", "two", "three"]}
566
+ mixedArray={[1, "two", true, null]}
567
+ simpleObject={{name: "John", age: 30}}
568
+ nestedObject={{
569
+ user: {
570
+ name: "Alice",
571
+ preferences: {
572
+ theme: "dark",
573
+ lang: "en"
574
+ }
575
+ },
576
+ settings: {
577
+ notifications: true,
578
+ emails: ["alice@example.com", "alice.work@example.com"]
579
+ }
580
+ }}
581
+ arrayOfObjects={[
582
+ {id: 1, name: "Item 1"},
583
+ {id: 2, name: "Item 2"}
584
+ ]}
585
+ />
586
+ `)).toMatchInlineSnapshot(`
587
+ {
588
+ "errors": [],
589
+ "html": "<h1></h1>",
590
+ "result": <React.Fragment>
591
+ <Heading
592
+ arrayOfObjects={
593
+ [
594
+ {
595
+ "id": 1,
596
+ "name": "Item 1",
597
+ },
598
+ {
599
+ "id": 2,
600
+ "name": "Item 2",
601
+ },
602
+ ]
603
+ }
604
+ mixedArray={
605
+ [
606
+ 1,
607
+ "two",
608
+ true,
609
+ null,
610
+ ]
611
+ }
612
+ nestedObject={
613
+ {
614
+ "settings": {
615
+ "emails": [
616
+ "alice@example.com",
617
+ "alice.work@example.com",
618
+ ],
619
+ "notifications": true,
620
+ },
621
+ "user": {
622
+ "name": "Alice",
623
+ "preferences": {
624
+ "lang": "en",
625
+ "theme": "dark",
626
+ },
627
+ },
628
+ }
629
+ }
630
+ simpleArray={
631
+ [
632
+ 1,
633
+ 2,
634
+ 3,
635
+ ]
636
+ }
637
+ simpleObject={
638
+ {
639
+ "age": 30,
640
+ "name": "John",
641
+ }
642
+ }
643
+ stringArray={
644
+ [
645
+ "one",
646
+ "two",
647
+ "three",
648
+ ]
649
+ }
650
+ />
651
+ </React.Fragment>,
652
+ }
653
+ `);
654
+ });
512
655
  test('breaks', () => {
513
656
  expect(render(dedent `
514
657
  To have a line break without a paragraph, you will need to use two trailing spaces.
@@ -1000,7 +1143,7 @@ test('kitchen sink', () => {
1000
1143
 
1001
1144
  [arbitrary case-insensitive reference text]: https://www.mozilla.org
1002
1145
  [1]: http://slashdot.org
1003
- [link text itself]: http://www.reddit.com</code></pre><p><a href="https://www.google.com" title="">I&#x27;m an inline-style link</a></p><p><a href="https://www.google.com" title="Google&#x27;s Homepage">I&#x27;m an inline-style link with title</a></p><p><a href="https://www.mozilla.org">I&#x27;m a reference-style link</a></p><p><a href="../blob/master/LICENSE" title="">I&#x27;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.
1146
+ [link text itself]: http://www.reddit.com</code></pre><p><a href="https://www.google.com" title="">I&#x27;m an inline-style link</a></p><p><a href="https://www.google.com" title="Google&#x27;s Homepage">I&#x27;m an inline-style link with title</a></p><p><a href="https://www.mozilla.org" title="">I&#x27;m a reference-style link</a></p><p><a href="../blob/master/LICENSE" title="">I&#x27;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.
1004
1147
  <a href="http://www.example.com" title="">http://www.example.com</a> and sometimes
1005
1148
  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&#x27;s our logo (hover to see the title text):
1006
1149
 
@@ -1499,6 +1642,7 @@ test('kitchen sink', () => {
1499
1642
  <p>
1500
1643
  <a
1501
1644
  href="https://www.mozilla.org"
1645
+ title=""
1502
1646
  >
1503
1647
  I'm a reference-style link
1504
1648
  </a>
@@ -1514,6 +1658,7 @@ test('kitchen sink', () => {
1514
1658
  <p>
1515
1659
  <a
1516
1660
  href="http://slashdot.org"
1661
+ title=""
1517
1662
  >
1518
1663
  You can use numbers for reference-style link definitions
1519
1664
  </a>
@@ -1522,6 +1667,7 @@ test('kitchen sink', () => {
1522
1667
  Or leave it empty and use the
1523
1668
  <a
1524
1669
  href="http://www.reddit.com"
1670
+ title=""
1525
1671
  >
1526
1672
  link text itself
1527
1673
  </a>
@@ -2243,4 +2389,619 @@ _No documentation_
2243
2389
  </React.Fragment>
2244
2390
  `);
2245
2391
  });
2392
+ test('component props schema validation with zod', () => {
2393
+ const HeadingSchema = z.object({
2394
+ level: z.number().min(1).max(6),
2395
+ title: z.string().optional(),
2396
+ });
2397
+ const CardsSchema = z.object({
2398
+ count: z.number().positive(),
2399
+ variant: z.enum(['default', 'outline']).optional(),
2400
+ });
2401
+ const componentPropsSchema = {
2402
+ Heading: HeadingSchema,
2403
+ Cards: CardsSchema,
2404
+ };
2405
+ const code = dedent `
2406
+ <Heading level={2} title="test">Valid heading</Heading>
2407
+
2408
+ <Cards count={3} variant="outline">Valid cards</Cards>
2409
+
2410
+ <Heading level={10} title="test">Invalid heading - level too high</Heading>
2411
+
2412
+ <Cards count={-1}>Invalid cards - negative count</Cards>
2413
+
2414
+ <Cards count="not a number">Invalid cards - wrong type</Cards>
2415
+ `;
2416
+ expect(render(code, componentPropsSchema)).toMatchInlineSnapshot(`
2417
+ {
2418
+ "errors": [
2419
+ {
2420
+ "line": 5,
2421
+ "message": "Invalid props for component "Heading" at "level": Number must be less than or equal to 6",
2422
+ "schemaPath": "level",
2423
+ },
2424
+ {
2425
+ "line": 7,
2426
+ "message": "Invalid props for component "Cards" at "count": Number must be greater than 0",
2427
+ "schemaPath": "count",
2428
+ },
2429
+ {
2430
+ "line": 9,
2431
+ "message": "Invalid props for component "Cards" at "count": Expected number, received string",
2432
+ "schemaPath": "count",
2433
+ },
2434
+ ],
2435
+ "html": "<h1>Valid heading</h1><div>Valid cards</div><h1>Invalid heading - level too high</h1><div>Invalid cards - negative count</div><div>Invalid cards - wrong type</div>",
2436
+ "result": <React.Fragment>
2437
+ <Heading
2438
+ level={2}
2439
+ title="test"
2440
+ >
2441
+ Valid heading
2442
+ </Heading>
2443
+ <Cards
2444
+ count={3}
2445
+ variant="outline"
2446
+ >
2447
+ Valid cards
2448
+ </Cards>
2449
+ <Heading
2450
+ level={10}
2451
+ title="test"
2452
+ >
2453
+ Invalid heading - level too high
2454
+ </Heading>
2455
+ <Cards
2456
+ count={-1}
2457
+ >
2458
+ Invalid cards - negative count
2459
+ </Cards>
2460
+ <Cards
2461
+ count="not a number"
2462
+ >
2463
+ Invalid cards - wrong type
2464
+ </Cards>
2465
+ </React.Fragment>,
2466
+ }
2467
+ `);
2468
+ });
2469
+ test('mdx expressions evaluation', () => {
2470
+ expect(render(dedent `
2471
+ # Expression Test
2472
+
2473
+ Simple math: {1 + 2}
2474
+
2475
+ <Heading>
2476
+ Inside JSX: {3 * 4}
2477
+ </Heading>
2478
+
2479
+ Boolean: {true}
2480
+ String concat: {"hello" + " world"}
2481
+ `)).toMatchInlineSnapshot(`
2482
+ {
2483
+ "errors": [],
2484
+ "html": "<h1>Expression Test</h1><p>Simple math: 3</p><h1><p>Inside JSX: 12</p></h1><p>Boolean:
2485
+ String concat: hello world</p>",
2486
+ "result": <React.Fragment>
2487
+ <h1>
2488
+ Expression Test
2489
+ </h1>
2490
+ <p>
2491
+ Simple math:
2492
+ 3
2493
+ </p>
2494
+ <Heading>
2495
+ <p>
2496
+ Inside JSX:
2497
+ 12
2498
+ </p>
2499
+ </Heading>
2500
+ <p>
2501
+ Boolean:
2502
+ true
2503
+
2504
+ String concat:
2505
+ hello world
2506
+ </p>
2507
+ </React.Fragment>,
2508
+ }
2509
+ `);
2510
+ });
2511
+ test('mdx expressions with unsupported functions', () => {
2512
+ expect(render(dedent `
2513
+ Math function: {Math.max(5, 10)}
2514
+ Console: {console.log("test")}
2515
+ `)).toMatchInlineSnapshot(`
2516
+ {
2517
+ "errors": [
2518
+ {
2519
+ "line": 1,
2520
+ "message": "Failed to evaluate expression: Math.max(5, 10)",
2521
+ },
2522
+ {
2523
+ "line": 2,
2524
+ "message": "Failed to evaluate expression: console.log("test")",
2525
+ },
2526
+ ],
2527
+ "html": "<p>Math function:
2528
+ Console: </p>",
2529
+ "result": <React.Fragment>
2530
+ <p>
2531
+ Math function:
2532
+
2533
+ Console:
2534
+ </p>
2535
+ </React.Fragment>,
2536
+ }
2537
+ `);
2538
+ });
2539
+ test('schema validation without errors', () => {
2540
+ const HeadingSchema = z.object({
2541
+ level: z.number().min(1).max(6),
2542
+ title: z.string().optional(),
2543
+ });
2544
+ const componentPropsSchema = {
2545
+ Heading: HeadingSchema,
2546
+ };
2547
+ const code = dedent `
2548
+ <Heading level={2} title="test">Valid heading</Heading>
2549
+ <Heading level={1}>Another valid heading</Heading>
2550
+ `;
2551
+ expect(render(code, componentPropsSchema)).toMatchInlineSnapshot(`
2552
+ {
2553
+ "errors": [],
2554
+ "html": "<h1>Valid heading</h1><h1>Another valid heading</h1>",
2555
+ "result": <React.Fragment>
2556
+ <Heading
2557
+ level={2}
2558
+ title="test"
2559
+ >
2560
+ Valid heading
2561
+ </Heading>
2562
+ <Heading
2563
+ level={1}
2564
+ >
2565
+ Another valid heading
2566
+ </Heading>
2567
+ </React.Fragment>,
2568
+ }
2569
+ `);
2570
+ });
2571
+ test('component without schema should not be validated', () => {
2572
+ const HeadingSchema = z.object({
2573
+ level: z.number().min(1).max(6),
2574
+ });
2575
+ const componentPropsSchema = {
2576
+ Heading: HeadingSchema,
2577
+ };
2578
+ const code = dedent `
2579
+ <Heading level={2}>Valid heading with schema</Heading>
2580
+ <Cards invalidProp="anything">Cards without schema - should not be validated</Cards>
2581
+ `;
2582
+ expect(render(code, componentPropsSchema)).toMatchInlineSnapshot(`
2583
+ {
2584
+ "errors": [],
2585
+ "html": "<h1>Valid heading with schema</h1><div>Cards without schema - should not be validated</div>",
2586
+ "result": <React.Fragment>
2587
+ <Heading
2588
+ level={2}
2589
+ >
2590
+ Valid heading with schema
2591
+ </Heading>
2592
+ <Cards
2593
+ invalidProp="anything"
2594
+ >
2595
+ Cards without schema - should not be validated
2596
+ </Cards>
2597
+ </React.Fragment>,
2598
+ }
2599
+ `);
2600
+ });
2601
+ test('validation error includes schema path', () => {
2602
+ const ComplexSchema = z.object({
2603
+ user: z.object({
2604
+ name: z.string(),
2605
+ age: z.number().min(0),
2606
+ }),
2607
+ settings: z.object({
2608
+ theme: z.enum(['light', 'dark']),
2609
+ }),
2610
+ });
2611
+ const componentPropsSchema = {
2612
+ Heading: ComplexSchema,
2613
+ };
2614
+ const code = dedent `
2615
+ <Heading user={{ name: "test", age: -1 }} settings={{ theme: "invalid" }}>Complex validation</Heading>
2616
+ `;
2617
+ expect(render(code, componentPropsSchema)).toMatchInlineSnapshot(`
2618
+ {
2619
+ "errors": [
2620
+ {
2621
+ "line": 1,
2622
+ "message": "Invalid props for component "Heading" at "user.age": Number must be greater than or equal to 0",
2623
+ "schemaPath": "user.age",
2624
+ },
2625
+ {
2626
+ "line": 1,
2627
+ "message": "Invalid props for component "Heading" at "settings.theme": Invalid enum value. Expected 'light' | 'dark', received 'invalid'",
2628
+ "schemaPath": "settings.theme",
2629
+ },
2630
+ ],
2631
+ "html": "<h1>Complex validation</h1>",
2632
+ "result": <React.Fragment>
2633
+ <Heading
2634
+ settings={
2635
+ {
2636
+ "theme": "invalid",
2637
+ }
2638
+ }
2639
+ user={
2640
+ {
2641
+ "age": -1,
2642
+ "name": "test",
2643
+ }
2644
+ }
2645
+ >
2646
+ Complex validation
2647
+ </Heading>
2648
+ </React.Fragment>,
2649
+ }
2650
+ `);
2651
+ });
2652
+ test('mdxJsxExpressionAttribute spread syntax', () => {
2653
+ expect(render(dedent `
2654
+ <Heading
2655
+ {...{key: '1', level: 2}}
2656
+ title="test"
2657
+ >
2658
+ Content with spread
2659
+ </Heading>
2660
+ `)).toMatchInlineSnapshot(`
2661
+ {
2662
+ "errors": [],
2663
+ "html": "<h1><p>Content with spread</p></h1>",
2664
+ "result": <React.Fragment>
2665
+ <Heading
2666
+ level={2}
2667
+ title="test"
2668
+ >
2669
+ <p>
2670
+ Content with spread
2671
+ </p>
2672
+ </Heading>
2673
+ </React.Fragment>,
2674
+ }
2675
+ `);
2676
+ });
2677
+ test('mdxJsxExpressionAttribute complex spread cases', () => {
2678
+ expect(render(dedent `
2679
+ <Heading
2680
+ {...{
2681
+ level: 3,
2682
+ active: true,
2683
+ disabled: false,
2684
+ count: 42,
2685
+ title: "spread title",
2686
+ nested: {
2687
+ prop: "value"
2688
+ }
2689
+ }}
2690
+ >
2691
+ Complex spread test
2692
+ </Heading>
2693
+
2694
+ <Cards
2695
+ {...{style: {color: "red", fontSize: "16px"}}}
2696
+ {...{className: "test-class", id: "test-id"}}
2697
+ >
2698
+ Multiple spreads
2699
+ </Cards>
2700
+ `)).toMatchInlineSnapshot(`
2701
+ {
2702
+ "errors": [],
2703
+ "html": "<h1><p>Complex spread test</p></h1><div><p>Multiple spreads</p></div>",
2704
+ "result": <React.Fragment>
2705
+ <Heading
2706
+ active={true}
2707
+ count={42}
2708
+ disabled={false}
2709
+ level={3}
2710
+ nested={
2711
+ {
2712
+ "prop": "value",
2713
+ }
2714
+ }
2715
+ title="spread title"
2716
+ >
2717
+ <p>
2718
+ Complex spread test
2719
+ </p>
2720
+ </Heading>
2721
+ <Cards
2722
+ className="test-class"
2723
+ id="test-id"
2724
+ style={
2725
+ {
2726
+ "color": "red",
2727
+ "fontSize": "16px",
2728
+ }
2729
+ }
2730
+ >
2731
+ <p>
2732
+ Multiple spreads
2733
+ </p>
2734
+ </Cards>
2735
+ </React.Fragment>,
2736
+ }
2737
+ `);
2738
+ });
2739
+ test('mdxJsxExpressionAttribute edge cases', () => {
2740
+ expect(render(dedent `
2741
+ <Heading {...{}} title="empty spread">Empty spread</Heading>
2742
+
2743
+ <Heading {...{null: null, undefined: undefined}} title="null/undefined values">Null/undefined</Heading>
2744
+
2745
+ <Heading {...{array: [1, 2, 3], object: {nested: true}}} title="complex values">Complex types</Heading>
2746
+ `)).toMatchInlineSnapshot(`
2747
+ {
2748
+ "errors": [
2749
+ {
2750
+ "line": 3,
2751
+ "message": "Failed to evaluate expression attribute: ...{null: null, undefined: undefined}",
2752
+ },
2753
+ ],
2754
+ "html": "<h1>Empty spread</h1><h1>Null/undefined</h1><h1>Complex types</h1>",
2755
+ "result": <React.Fragment>
2756
+ <Heading
2757
+ title="empty spread"
2758
+ >
2759
+ Empty spread
2760
+ </Heading>
2761
+ <Heading
2762
+ title="null/undefined values"
2763
+ >
2764
+ Null/undefined
2765
+ </Heading>
2766
+ <Heading
2767
+ array={
2768
+ [
2769
+ 1,
2770
+ 2,
2771
+ 3,
2772
+ ]
2773
+ }
2774
+ object={
2775
+ {
2776
+ "nested": true,
2777
+ }
2778
+ }
2779
+ title="complex values"
2780
+ >
2781
+ Complex types
2782
+ </Heading>
2783
+ </React.Fragment>,
2784
+ }
2785
+ `);
2786
+ });
2787
+ test('ESM imports from https URLs', () => {
2788
+ const code = dedent `
2789
+ import Button from 'https://esm.sh/some-button-component'
2790
+ import { Card, Modal } from 'https://esm.sh/some-ui-library'
2791
+
2792
+ # Hello
2793
+
2794
+ <Button>Click me</Button>
2795
+
2796
+ <Card title="Test Card">
2797
+ Content inside card
2798
+ </Card>
2799
+
2800
+ <Modal open={true}>
2801
+ Modal content
2802
+ </Modal>
2803
+ `;
2804
+ const mdast = mdxParse(code);
2805
+ const visitor = new MdastToJsx({ markdown: code, mdast, components, allowClientEsmImports: true });
2806
+ const result = visitor.run();
2807
+ // Check that imports were parsed correctly
2808
+ expect(visitor.esmImports.size).toBe(3);
2809
+ expect(visitor.esmImports.get('Button')).toBe('https://esm.sh/some-button-component');
2810
+ expect(visitor.esmImports.get('Card')).toBe('https://esm.sh/some-ui-library#Card');
2811
+ expect(visitor.esmImports.get('Modal')).toBe('https://esm.sh/some-ui-library#Modal');
2812
+ // Since these are dynamic imports that only work on client, the server render should return null
2813
+ const html = renderToStaticMarkup(result);
2814
+ expect(html).toMatchInlineSnapshot(`"<h1>Hello</h1>"`);
2815
+ expect(visitor.errors).toEqual([]);
2816
+ });
2817
+ test('ESM imports error handling', () => {
2818
+ const code = dedent `
2819
+ import Button from 'file:///local/path'
2820
+ import Component from './relative/path'
2821
+
2822
+ # Test
2823
+
2824
+ <Button>Local import should not work</Button>
2825
+ <Component>Relative import should not work</Component>
2826
+ `;
2827
+ const mdast = mdxParse(code);
2828
+ const visitor = new MdastToJsx({ markdown: code, mdast, components, allowClientEsmImports: true });
2829
+ const result = visitor.run();
2830
+ // Only https imports should be processed
2831
+ expect(visitor.esmImports.size).toBe(0);
2832
+ // Should have 4 errors: 2 for invalid imports, 2 for unsupported components
2833
+ expect(visitor.errors.length).toBe(4);
2834
+ // First two errors are for invalid imports
2835
+ expect(visitor.errors[0].message).toContain('Invalid import URL');
2836
+ expect(visitor.errors[1].message).toContain('Invalid import URL');
2837
+ // Last two errors are for unsupported components
2838
+ expect(visitor.errors[2].message).toContain('Unsupported jsx component Button');
2839
+ expect(visitor.errors[3].message).toContain('Unsupported jsx component Component');
2840
+ });
2841
+ test('jsx components in attributes', () => {
2842
+ const code = dedent `
2843
+ # JSX Components in Attributes
2844
+
2845
+ <Heading icon={<span>👋</span>} level={1}>
2846
+ Hello World
2847
+ </Heading>
2848
+
2849
+ <Cards items={<div>Item 1</div>}>
2850
+ Some content
2851
+ </Cards>
2852
+ `;
2853
+ const { result, errors, html } = render(code);
2854
+ // Should not have any errors
2855
+ expect(errors).toMatchInlineSnapshot(`[]`);
2856
+ // Should render correctly
2857
+ expect(html).toMatchInlineSnapshot(`"<h1>JSX Components in Attributes</h1><h1><p>Hello World</p></h1><div><p>Some content</p></div>"`);
2858
+ expect(result).toMatchInlineSnapshot(`
2859
+ <React.Fragment>
2860
+ <h1>
2861
+ JSX Components in Attributes
2862
+ </h1>
2863
+ <Heading
2864
+ icon={
2865
+ <span>
2866
+ 👋
2867
+ </span>
2868
+ }
2869
+ level={1}
2870
+ >
2871
+ <p>
2872
+ Hello World
2873
+ </p>
2874
+ </Heading>
2875
+ <Cards
2876
+ items={
2877
+ <div>
2878
+ Item 1
2879
+ </div>
2880
+ }
2881
+ >
2882
+ <p>
2883
+ Some content
2884
+ </p>
2885
+ </Cards>
2886
+ </React.Fragment>
2887
+ `);
2888
+ });
2889
+ test('jsx components in attributes with ESM imports', () => {
2890
+ const code = dedent `
2891
+ import Button from 'https://esm.sh/some-button-component'
2892
+ import { Icon } from 'https://esm.sh/some-icon-library'
2893
+
2894
+ # ESM Components in Attributes
2895
+
2896
+ <Heading icon={<Icon name="star" />} level={1}>
2897
+ Hello World
2898
+ </Heading>
2899
+
2900
+ <Cards actionButton={<Button>Click me</Button>}>
2901
+ Some content
2902
+ </Cards>
2903
+ `;
2904
+ const { result, errors, html } = render(code, undefined, true);
2905
+ // Should not have any errors
2906
+ expect(errors).toMatchInlineSnapshot(`[]`);
2907
+ // Should render correctly - ESM components should be wrapped in DynamicEsmComponent
2908
+ expect(html).toMatchInlineSnapshot(`"<h1>ESM Components in Attributes</h1><h1><p>Hello World</p></h1><div><p>Some content</p></div>"`);
2909
+ expect(result).toMatchInlineSnapshot(`
2910
+ <React.Fragment>
2911
+ <h1>
2912
+ ESM Components in Attributes
2913
+ </h1>
2914
+ <Heading
2915
+ icon={
2916
+ <DynamicEsmComponent
2917
+ componentName="Icon"
2918
+ importUrl="https://esm.sh/some-icon-library"
2919
+ name="star"
2920
+ />
2921
+ }
2922
+ level={1}
2923
+ >
2924
+ <p>
2925
+ Hello World
2926
+ </p>
2927
+ </Heading>
2928
+ <Cards
2929
+ actionButton={
2930
+ <DynamicEsmComponent
2931
+ componentName="default"
2932
+ importUrl="https://esm.sh/some-button-component"
2933
+ >
2934
+ Click me
2935
+ </DynamicEsmComponent>
2936
+ }
2937
+ >
2938
+ <p>
2939
+ Some content
2940
+ </p>
2941
+ </Cards>
2942
+ </React.Fragment>
2943
+ `);
2944
+ });
2945
+ test('ESM imports disabled by default', () => {
2946
+ const code = dedent `
2947
+ import Button from 'https://esm.sh/some-button-component'
2948
+
2949
+ # Test Default Behavior
2950
+
2951
+ <Button>This should not work</Button>
2952
+ `;
2953
+ const { result, errors, html } = render(code); // No allowClientEsmImports flag
2954
+ // ESM imports should not be processed when disabled
2955
+ const mdast = mdxParse(code);
2956
+ const visitor = new MdastToJsx({ markdown: code, mdast, components }); // Default allowClientEsmImports: false
2957
+ expect(visitor.esmImports.size).toBe(0);
2958
+ // Should have error for unsupported component
2959
+ expect(errors.length).toBeGreaterThan(0);
2960
+ expect(errors.some(e => e.message.includes('Unsupported jsx component Button'))).toBe(true);
2961
+ // Should render heading but not the button
2962
+ expect(html).toMatchInlineSnapshot(`"<h1>Test Default Behavior</h1>"`);
2963
+ });
2964
+ test("jsx components in attributes error handling", () => {
2965
+ const code = dedent `
2966
+ # Error Handling Test
2967
+
2968
+ <Heading icon={<UnsupportedComponent />} level={1}>
2969
+ Hello World
2970
+ </Heading>
2971
+ `;
2972
+ const { result, errors, html } = render(code);
2973
+ // Should have an error for the unsupported component
2974
+ expect(errors).toMatchInlineSnapshot(`
2975
+ [
2976
+ {
2977
+ "line": 3,
2978
+ "message": "Unsupported jsx component UnsupportedComponent in attribute",
2979
+ },
2980
+ {
2981
+ "line": 3,
2982
+ "message": "Failed to evaluate expression attribute: icon={<UnsupportedComponent />}",
2983
+ },
2984
+ {
2985
+ "line": 3,
2986
+ "message": "Expressions in jsx prop not evaluated: (icon={<UnsupportedComponent />})",
2987
+ },
2988
+ ]
2989
+ `);
2990
+ // Should still render the rest of the content
2991
+ expect(html).toMatchInlineSnapshot(`"<h1>Error Handling Test</h1><h1><p>Hello World</p></h1>"`);
2992
+ expect(result).toMatchInlineSnapshot(`
2993
+ <React.Fragment>
2994
+ <h1>
2995
+ Error Handling Test
2996
+ </h1>
2997
+ <Heading
2998
+ level={1}
2999
+ >
3000
+ <p>
3001
+ Hello World
3002
+ </p>
3003
+ </Heading>
3004
+ </React.Fragment>
3005
+ `);
3006
+ });
2246
3007
  //# sourceMappingURL=safe-mdx.test.js.map