astro-xmdx 0.0.2

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 (48) hide show
  1. package/index.ts +8 -0
  2. package/package.json +80 -0
  3. package/src/constants.ts +52 -0
  4. package/src/index.ts +150 -0
  5. package/src/pipeline/index.ts +38 -0
  6. package/src/pipeline/orchestrator.test.ts +324 -0
  7. package/src/pipeline/orchestrator.ts +121 -0
  8. package/src/pipeline/pipe.test.ts +251 -0
  9. package/src/pipeline/pipe.ts +70 -0
  10. package/src/pipeline/types.ts +59 -0
  11. package/src/plugins.test.ts +274 -0
  12. package/src/presets/index.ts +225 -0
  13. package/src/transforms/blocks-to-jsx.test.ts +590 -0
  14. package/src/transforms/blocks-to-jsx.ts +617 -0
  15. package/src/transforms/expressive-code.test.ts +274 -0
  16. package/src/transforms/expressive-code.ts +147 -0
  17. package/src/transforms/index.test.ts +143 -0
  18. package/src/transforms/index.ts +100 -0
  19. package/src/transforms/inject-components.test.ts +406 -0
  20. package/src/transforms/inject-components.ts +184 -0
  21. package/src/transforms/shiki.test.ts +289 -0
  22. package/src/transforms/shiki.ts +312 -0
  23. package/src/types.ts +92 -0
  24. package/src/utils/config.test.ts +252 -0
  25. package/src/utils/config.ts +146 -0
  26. package/src/utils/frontmatter.ts +33 -0
  27. package/src/utils/imports.test.ts +518 -0
  28. package/src/utils/imports.ts +201 -0
  29. package/src/utils/mdx-detection.test.ts +41 -0
  30. package/src/utils/mdx-detection.ts +209 -0
  31. package/src/utils/paths.test.ts +206 -0
  32. package/src/utils/paths.ts +92 -0
  33. package/src/utils/validation.test.ts +60 -0
  34. package/src/utils/validation.ts +15 -0
  35. package/src/vite-plugin/binding-loader.ts +81 -0
  36. package/src/vite-plugin/directive-rewriter.test.ts +331 -0
  37. package/src/vite-plugin/directive-rewriter.ts +272 -0
  38. package/src/vite-plugin/esbuild-pool.ts +173 -0
  39. package/src/vite-plugin/index.ts +37 -0
  40. package/src/vite-plugin/jsx-module.ts +106 -0
  41. package/src/vite-plugin/mdx-wrapper.ts +328 -0
  42. package/src/vite-plugin/normalize-config.test.ts +78 -0
  43. package/src/vite-plugin/normalize-config.ts +29 -0
  44. package/src/vite-plugin/shiki-highlighter.ts +46 -0
  45. package/src/vite-plugin/shiki-manager.test.ts +175 -0
  46. package/src/vite-plugin/shiki-manager.ts +53 -0
  47. package/src/vite-plugin/types.ts +189 -0
  48. package/src/vite-plugin.ts +1342 -0
@@ -0,0 +1,590 @@
1
+ import { describe, it, expect } from 'bun:test';
2
+ import { blocksToJsx, type Block } from './blocks-to-jsx.js';
3
+ import { createRegistry, starlightLibrary } from 'xmdx/registry';
4
+
5
+ describe('blocksToJsx', () => {
6
+ describe('user imports', () => {
7
+ it('should include user imports in output', () => {
8
+ const blocks: Block[] = [
9
+ { type: 'html', content: '<p>Hello</p>' },
10
+ ];
11
+ const userImports = ["import Card from '~/components/Card.astro';"];
12
+
13
+ const result = blocksToJsx(blocks, {}, [], null, undefined, userImports);
14
+
15
+ expect(result).toContain("import Card from '~/components/Card.astro';");
16
+ });
17
+
18
+ it('should skip registry imports for user-imported components', () => {
19
+ const registry = createRegistry([starlightLibrary]);
20
+
21
+ const blocks: Block[] = [
22
+ { type: 'component', name: 'Card', props: {}, slotChildren: [{ type: 'html', content: '<p>Content</p>' }] },
23
+ ];
24
+ const userImports = ["import Card from '~/components/Landing/Card.astro';"];
25
+
26
+ const result = blocksToJsx(blocks, {}, [], registry, undefined, userImports);
27
+
28
+ // Should include user import
29
+ expect(result).toContain("import Card from '~/components/Landing/Card.astro';");
30
+ // Should NOT include registry import for Card
31
+ expect(result).not.toContain('@astrojs/starlight/components');
32
+ });
33
+
34
+ it('should generate registry imports for non-user-imported components', () => {
35
+ const registry = createRegistry([starlightLibrary]);
36
+
37
+ const blocks: Block[] = [
38
+ { type: 'component', name: 'Card', props: {}, slotChildren: [{ type: 'html', content: '<p>Card Content</p>' }] },
39
+ { type: 'component', name: 'Aside', props: {}, slotChildren: [{ type: 'html', content: '<p>Aside Content</p>' }] },
40
+ ];
41
+ // Only Card is user-imported
42
+ const userImports = ["import Card from '~/components/Card.astro';"];
43
+
44
+ const result = blocksToJsx(blocks, {}, [], registry, undefined, userImports);
45
+
46
+ // User import for Card
47
+ expect(result).toContain("import Card from '~/components/Card.astro';");
48
+ // Registry import for Aside (since it's not user-imported)
49
+ expect(result).toContain("import { Aside } from '@astrojs/starlight/components';");
50
+ });
51
+
52
+ it('should handle multiple user imports', () => {
53
+ const blocks: Block[] = [
54
+ { type: 'component', name: 'Card', props: {} },
55
+ { type: 'component', name: 'Button', props: {} },
56
+ ];
57
+ const userImports = [
58
+ "import Card from '~/components/Card.astro';",
59
+ "import Button from '~/components/Button.astro';",
60
+ ];
61
+
62
+ const result = blocksToJsx(blocks, {}, [], null, undefined, userImports);
63
+
64
+ expect(result).toContain("import Card from '~/components/Card.astro';");
65
+ expect(result).toContain("import Button from '~/components/Button.astro';");
66
+ });
67
+
68
+ it('should handle named imports in user imports', () => {
69
+ const registry = createRegistry([starlightLibrary]);
70
+
71
+ const blocks: Block[] = [
72
+ { type: 'component', name: 'Aside', props: {} },
73
+ ];
74
+ // User provides named import for Aside
75
+ const userImports = ["import { Aside } from './my-components';"];
76
+
77
+ const result = blocksToJsx(blocks, {}, [], registry, undefined, userImports);
78
+
79
+ // Should include user import
80
+ expect(result).toContain("import { Aside } from './my-components';");
81
+ // Should NOT include registry import for Aside
82
+ expect(result).not.toContain('@astrojs/starlight/components');
83
+ });
84
+
85
+ it('should handle aliased imports in user imports', () => {
86
+ const registry = createRegistry([starlightLibrary]);
87
+
88
+ const blocks: Block[] = [
89
+ { type: 'component', name: 'MyCard', props: {} },
90
+ ];
91
+ // User imports Card as MyCard
92
+ const userImports = ["import { Card as MyCard } from './my-components';"];
93
+
94
+ const result = blocksToJsx(blocks, {}, [], registry, undefined, userImports);
95
+
96
+ // Should include user import
97
+ expect(result).toContain("import { Card as MyCard } from './my-components';");
98
+ });
99
+
100
+ it('should default to empty user imports when not provided', () => {
101
+ const blocks: Block[] = [
102
+ { type: 'html', content: '<p>Hello</p>' },
103
+ ];
104
+
105
+ // Call without userImports parameter
106
+ const result = blocksToJsx(blocks, {}, [], null, undefined);
107
+
108
+ // Should generate valid output without errors
109
+ expect(result).toContain('export const frontmatter');
110
+ expect(result).toContain('export default XmdxContent');
111
+ });
112
+ });
113
+
114
+ describe('basic functionality', () => {
115
+ it('should generate valid JSX module for HTML blocks', () => {
116
+ const blocks: Block[] = [
117
+ { type: 'html', content: '<p>Hello World</p>' },
118
+ ];
119
+
120
+ const result = blocksToJsx(blocks);
121
+
122
+ expect(result).toContain('export const frontmatter');
123
+ expect(result).toContain('export function getHeadings()');
124
+ expect(result).toContain('export const Content');
125
+ expect(result).toContain('export default XmdxContent');
126
+ });
127
+
128
+ it('should use set:html for HTML content', () => {
129
+ const blocks: Block[] = [
130
+ { type: 'html', content: '<p>Test</p>' },
131
+ ];
132
+
133
+ const result = blocksToJsx(blocks);
134
+
135
+ expect(result).toContain('set:html=');
136
+ });
137
+
138
+ it('should include runtime imports', () => {
139
+ const blocks: Block[] = [];
140
+
141
+ const result = blocksToJsx(blocks);
142
+
143
+ expect(result).toContain("import { createComponent, renderJSX } from 'astro/runtime/server/index.js';");
144
+ expect(result).toContain("import { Fragment, Fragment as _Fragment, jsx as _jsx } from 'astro/jsx-runtime';");
145
+ });
146
+ });
147
+
148
+ describe('nested components', () => {
149
+ it('should embed nested components directly without set:html', () => {
150
+ const blocks: Block[] = [
151
+ {
152
+ type: 'component',
153
+ name: 'CardGrid',
154
+ props: {},
155
+ // Use HTML-style attributes (what the Rust renderer produces)
156
+ slotChildren: [{ type: 'html', content: '<Card title="Getting Started">Content here</Card>' }],
157
+ },
158
+ ];
159
+
160
+ const result = blocksToJsx(blocks);
161
+
162
+ // Should embed JSX directly, not use set:html
163
+ expect(result).toContain('<CardGrid><Card title="Getting Started">Content here</Card></CardGrid>');
164
+ expect(result).not.toContain('set:html={');
165
+ });
166
+
167
+ it('should use set:html for pure HTML slot content', () => {
168
+ const blocks: Block[] = [
169
+ {
170
+ type: 'component',
171
+ name: 'Card',
172
+ props: {},
173
+ slotChildren: [{ type: 'html', content: '<p>Hello <strong>world</strong></p>' }],
174
+ },
175
+ ];
176
+
177
+ const result = blocksToJsx(blocks);
178
+
179
+ // Should use set:html for HTML content
180
+ expect(result).toContain('set:html=');
181
+ expect(result).toContain('<Card><_Fragment set:html=');
182
+ });
183
+
184
+ it('should use set:html for uppercase HTML tags (not components)', () => {
185
+ // Uppercase HTML tags like <SVG>, <DIV> should NOT be treated as components
186
+ // Only true PascalCase (uppercase followed by lowercase) should be components
187
+ const blocks: Block[] = [
188
+ {
189
+ type: 'component',
190
+ name: 'Container',
191
+ props: {},
192
+ slotChildren: [{ type: 'html', content: '<SVG><path d="M0 0h24v24H0z"/></SVG>' }],
193
+ },
194
+ ];
195
+
196
+ const result = blocksToJsx(blocks);
197
+
198
+ // Should use set:html because <SVG> is not a PascalCase component
199
+ expect(result).toContain('set:html=');
200
+ expect(result).toContain('<Container><_Fragment set:html=');
201
+ });
202
+
203
+ it('should detect acronym-prefixed PascalCase components like MDXProvider', () => {
204
+ // Components that start with acronyms like MDX, URL, API should be detected
205
+ const blocks: Block[] = [
206
+ {
207
+ type: 'component',
208
+ name: 'Container',
209
+ props: {},
210
+ slotChildren: [{ type: 'html', content: '<MDXProvider>content</MDXProvider>' }],
211
+ },
212
+ ];
213
+
214
+ const result = blocksToJsx(blocks);
215
+
216
+ // Should embed JSX directly, not use set:html
217
+ expect(result).toContain('<Container><MDXProvider>content</MDXProvider></Container>');
218
+ expect(result).not.toContain('set:html={');
219
+ });
220
+
221
+ it('should detect URLTable and other acronym-prefixed components', () => {
222
+ const blocks: Block[] = [
223
+ {
224
+ type: 'component',
225
+ name: 'Section',
226
+ props: {},
227
+ slotChildren: [{ type: 'html', content: '<URLTable /><APIClient>data</APIClient>' }],
228
+ },
229
+ ];
230
+
231
+ const result = blocksToJsx(blocks);
232
+
233
+ // Should embed JSX directly because these are PascalCase components
234
+ expect(result).toContain('<Section><URLTable /><APIClient>data</APIClient></Section>');
235
+ expect(result).not.toContain('set:html={');
236
+ });
237
+
238
+ it('should still use set:html for all-uppercase tags like HTML, DIV', () => {
239
+ const blocks: Block[] = [
240
+ {
241
+ type: 'component',
242
+ name: 'Container',
243
+ props: {},
244
+ slotChildren: [{ type: 'html', content: '<DIV>content</DIV><HTML><BODY></BODY></HTML>' }],
245
+ },
246
+ ];
247
+
248
+ const result = blocksToJsx(blocks);
249
+
250
+ // All-uppercase should use set:html path
251
+ expect(result).toContain('set:html=');
252
+ expect(result).toContain('<Container><_Fragment set:html=');
253
+ });
254
+
255
+ it('should handle multiple nested components', () => {
256
+ const blocks: Block[] = [
257
+ {
258
+ type: 'component',
259
+ name: 'CardGrid',
260
+ props: {},
261
+ // Use HTML-style attributes (what the Rust renderer produces)
262
+ slotChildren: [{ type: 'html', content: '<Card title="First">First card</Card><Card title="Second">Second card</Card>' }],
263
+ },
264
+ ];
265
+
266
+ const result = blocksToJsx(blocks);
267
+
268
+ // Should embed JSX directly
269
+ expect(result).toContain('<CardGrid><Card title="First">First card</Card><Card title="Second">Second card</Card></CardGrid>');
270
+ expect(result).not.toContain('set:html={');
271
+ });
272
+
273
+ it('should handle self-closing nested components', () => {
274
+ const blocks: Block[] = [
275
+ {
276
+ type: 'component',
277
+ name: 'Container',
278
+ props: {},
279
+ slotChildren: [{ type: 'html', content: '<Icon name="star" />' }],
280
+ },
281
+ ];
282
+
283
+ const result = blocksToJsx(blocks);
284
+
285
+ // Should embed JSX directly for self-closing component
286
+ expect(result).toContain('<Container><Icon name="star" /></Container>');
287
+ expect(result).not.toContain('set:html={');
288
+ });
289
+
290
+ it('should handle mixed HTML and component content', () => {
291
+ const blocks: Block[] = [
292
+ {
293
+ type: 'component',
294
+ name: 'Section',
295
+ props: {},
296
+ slotChildren: [{ type: 'html', content: '<p>Intro text</p><Card>Content</Card><p>More text</p>' }],
297
+ },
298
+ ];
299
+
300
+ const result = blocksToJsx(blocks);
301
+
302
+ // Should embed JSX directly because it contains a component
303
+ expect(result).toContain('<Section><p>Intro text</p><Card>Content</Card><p>More text</p></Section>');
304
+ expect(result).not.toContain('set:html={');
305
+ });
306
+
307
+ it('should self-close void HTML tags when embedding slot content', () => {
308
+ const blocks: Block[] = [
309
+ {
310
+ type: 'component',
311
+ name: 'Section',
312
+ props: {},
313
+ slotChildren: [{ type: 'html', content: '<Card>Content</Card><img src="/img.png">' }],
314
+ },
315
+ ];
316
+
317
+ const result = blocksToJsx(blocks);
318
+
319
+ expect(result).toContain('<Section><Card>Content</Card><img src="/img.png" /></Section>');
320
+ expect(result).not.toContain('<img src="/img.png">');
321
+ });
322
+
323
+ it('should convert HTML entities to JSX expressions in nested component content', () => {
324
+ const blocks: Block[] = [
325
+ {
326
+ type: 'component',
327
+ name: 'Card',
328
+ props: {},
329
+ // HTML entities that would appear literally in JSX
330
+ slotChildren: [{ type: 'html', content: '<Badge>a &lt; b &amp;&amp; c</Badge>' }],
331
+ },
332
+ ];
333
+
334
+ const result = blocksToJsx(blocks);
335
+
336
+ // Entities should become JSX expressions: &lt; becomes {"<"}, &amp; becomes {"&"}
337
+ expect(result).toContain('<Card><Badge>a {"<"} b {"&"}{"&"} c</Badge></Card>');
338
+ // Should NOT contain the encoded entities
339
+ expect(result).not.toContain('&lt;');
340
+ expect(result).not.toContain('&amp;');
341
+ // Should NOT decode to raw characters (that would break JSX)
342
+ expect(result).not.toContain('<Card><Badge>a < b && c</Badge></Card>');
343
+ });
344
+
345
+ it('should convert literal ampersands to JSX expressions in nested component content', () => {
346
+ const blocks: Block[] = [
347
+ {
348
+ type: 'component',
349
+ name: 'Card',
350
+ props: {},
351
+ // Literal & character (not encoded as entity)
352
+ slotChildren: [{ type: 'html', content: '<Badge>Languages & Frameworks</Badge>' }],
353
+ },
354
+ ];
355
+
356
+ const result = blocksToJsx(blocks);
357
+
358
+ // Literal & should become JSX expression
359
+ expect(result).toContain('Languages {"&"} Frameworks');
360
+ // Should NOT contain raw & (that would break JSX)
361
+ expect(result).not.toContain('Languages & Frameworks');
362
+ });
363
+
364
+ it('should preserve unknown HTML entities in nested component content', () => {
365
+ const blocks: Block[] = [
366
+ {
367
+ type: 'component',
368
+ name: 'Card',
369
+ props: {},
370
+ // Unknown entity like &nbsp; should be preserved
371
+ slotChildren: [{ type: 'html', content: '<Badge>Hello&nbsp;World</Badge>' }],
372
+ },
373
+ ];
374
+
375
+ const result = blocksToJsx(blocks);
376
+
377
+ // Unknown entities should be left as-is
378
+ expect(result).toContain('&nbsp;');
379
+ });
380
+
381
+ it('should preserve valid JSX expressions in nested components', () => {
382
+ const blocks: Block[] = [
383
+ {
384
+ type: 'component',
385
+ name: 'CardGrid',
386
+ props: {},
387
+ // Valid JSX expression that should NOT be escaped
388
+ slotChildren: [{
389
+ type: 'component',
390
+ name: 'Card',
391
+ props: { title: { type: 'expression', value: 'title' } },
392
+ slotChildren: [{ type: 'html', content: 'Content' }],
393
+ }],
394
+ },
395
+ ];
396
+
397
+ const result = blocksToJsx(blocks);
398
+
399
+ // JSX expressions should be preserved, not escaped
400
+ expect(result).toContain('title={title}');
401
+ expect(result).not.toContain("{'{'}");
402
+ expect(result).toContain('<CardGrid><Card title={title}>Content</Card></CardGrid>');
403
+ });
404
+ });
405
+
406
+ describe('code blocks', () => {
407
+ it('should render standalone code block as <pre><code> in set:html Fragment', () => {
408
+ const blocks: Block[] = [
409
+ { type: 'code', code: 'console.log("hello")' },
410
+ ];
411
+
412
+ const result = blocksToJsx(blocks);
413
+
414
+ expect(result).toContain('<_Fragment set:html=');
415
+ expect(result).toContain('astro-code');
416
+ expect(result).toContain('console.log');
417
+ });
418
+
419
+ it('should include language class when lang is set', () => {
420
+ const blocks: Block[] = [
421
+ { type: 'code', code: 'const x = 1;', lang: 'js' },
422
+ ];
423
+
424
+ const result = blocksToJsx(blocks);
425
+
426
+ expect(result).toContain('language-js');
427
+ });
428
+
429
+ it('should escape special characters in code content', () => {
430
+ const blocks: Block[] = [
431
+ { type: 'code', code: 'if (a < b && c > d) { run(); }', lang: 'js' },
432
+ ];
433
+
434
+ const result = blocksToJsx(blocks);
435
+
436
+ expect(result).toContain('&lt;');
437
+ expect(result).toContain('&amp;');
438
+ expect(result).toContain('&#123;');
439
+ expect(result).toContain('&#125;');
440
+ // Raw chars should not appear unescaped in the HTML
441
+ expect(result).not.toContain('"if (a < b');
442
+ });
443
+
444
+ it('should render code block in component slot via slotChildrenToHtml', () => {
445
+ const blocks: Block[] = [
446
+ {
447
+ type: 'component',
448
+ name: 'Card',
449
+ props: {},
450
+ slotChildren: [
451
+ { type: 'html', content: '<p>Intro</p>' },
452
+ { type: 'code', code: 'let x = 1;', lang: 'ts' },
453
+ ],
454
+ },
455
+ ];
456
+
457
+ const result = blocksToJsx(blocks);
458
+
459
+ expect(result).toContain('astro-code');
460
+ expect(result).toContain('language-ts');
461
+ expect(result).toContain('let x = 1;');
462
+ });
463
+
464
+ it('should always render code blocks as <pre><code> (EC rewriting is pipeline-only)', () => {
465
+ const blocks: Block[] = [
466
+ { type: 'code', code: 'const x = 1;', lang: 'js', meta: 'title="example"' },
467
+ ];
468
+
469
+ const result = blocksToJsx(blocks);
470
+
471
+ expect(result).toContain('astro-code');
472
+ expect(result).toContain('language-js');
473
+ expect(result).toContain('const x = 1;');
474
+ expect(result).not.toContain('<Code code=');
475
+ });
476
+
477
+ it('should render code blocks as <pre><code> in slots too', () => {
478
+ const blocks: Block[] = [
479
+ {
480
+ type: 'component',
481
+ name: 'Card',
482
+ props: {},
483
+ slotChildren: [
484
+ { type: 'code', code: 'hello()', lang: 'py' },
485
+ ],
486
+ },
487
+ ];
488
+
489
+ const result = blocksToJsx(blocks);
490
+
491
+ expect(result).toContain('astro-code');
492
+ expect(result).toContain('language-py');
493
+ expect(result).toContain('hello()');
494
+ expect(result).not.toContain('<Code code=');
495
+ });
496
+
497
+ it('should handle mixed code and HTML in slot', () => {
498
+ const blocks: Block[] = [
499
+ {
500
+ type: 'component',
501
+ name: 'Section',
502
+ props: {},
503
+ slotChildren: [
504
+ { type: 'html', content: '<p>Before code</p>' },
505
+ { type: 'code', code: 'fn main() {}', lang: 'rust' },
506
+ { type: 'html', content: '<p>After code</p>' },
507
+ ],
508
+ },
509
+ ];
510
+
511
+ const result = blocksToJsx(blocks);
512
+
513
+ expect(result).toContain('<p>Before code</p>');
514
+ expect(result).toContain('astro-code');
515
+ expect(result).toContain('language-rust');
516
+ expect(result).toContain('After code');
517
+ });
518
+ });
519
+
520
+ describe('Fragment slot stripping', () => {
521
+ it('should strip <p> wrapper from Fragment with slot attribute', () => {
522
+ const blocks: Block[] = [
523
+ {
524
+ type: 'component',
525
+ name: 'IslandsDiagram',
526
+ props: {},
527
+ slotChildren: [{ type: 'html', content: '<p><Fragment slot="headerApp">Header text</Fragment></p>' }],
528
+ },
529
+ ];
530
+
531
+ const result = blocksToJsx(blocks);
532
+
533
+ // Should NOT contain the <p> wrapper
534
+ expect(result).not.toContain('<p><Fragment slot=');
535
+ // Should contain the Fragment with slot directly
536
+ expect(result).toContain('<Fragment slot="headerApp">');
537
+ });
538
+
539
+ it('should strip multiple <p> wrappers from Fragment slots', () => {
540
+ const blocks: Block[] = [
541
+ {
542
+ type: 'component',
543
+ name: 'Container',
544
+ props: {},
545
+ slotChildren: [{ type: 'html', content: '<p><Fragment slot="header">Header</Fragment></p><p><Fragment slot="footer">Footer</Fragment></p>' }],
546
+ },
547
+ ];
548
+
549
+ const result = blocksToJsx(blocks);
550
+
551
+ // Should NOT contain any <p><Fragment patterns
552
+ expect(result).not.toContain('<p><Fragment slot=');
553
+ // Should contain both Fragment slots
554
+ expect(result).toContain('<Fragment slot="header">Header</Fragment>');
555
+ expect(result).toContain('<Fragment slot="footer">Footer</Fragment>');
556
+ });
557
+
558
+ it('should preserve regular paragraphs', () => {
559
+ const blocks: Block[] = [
560
+ {
561
+ type: 'component',
562
+ name: 'Card',
563
+ props: {},
564
+ slotChildren: [{ type: 'html', content: '<p>Regular paragraph content</p>' }],
565
+ },
566
+ ];
567
+
568
+ const result = blocksToJsx(blocks);
569
+
570
+ // Regular paragraphs should be preserved
571
+ expect(result).toContain('<p>Regular paragraph content</p>');
572
+ });
573
+
574
+ it('should preserve Fragment without slot attribute', () => {
575
+ const blocks: Block[] = [
576
+ {
577
+ type: 'component',
578
+ name: 'Wrapper',
579
+ props: {},
580
+ slotChildren: [{ type: 'html', content: '<p><Fragment>Content without slot</Fragment></p>' }],
581
+ },
582
+ ];
583
+
584
+ const result = blocksToJsx(blocks);
585
+
586
+ // Fragment without slot= should NOT be stripped
587
+ expect(result).toContain('<p><Fragment>Content without slot</Fragment></p>');
588
+ });
589
+ });
590
+ });