ai-props 2.1.3 → 2.4.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.
Files changed (105) hide show
  1. package/.dev.vars +2 -0
  2. package/.turbo/turbo-build.log +1 -1
  3. package/CHANGELOG.md +20 -0
  4. package/README.md +2 -0
  5. package/dist/ai.d.ts.map +1 -1
  6. package/dist/ai.js +4 -4
  7. package/dist/ai.js.map +1 -1
  8. package/dist/cascade.d.ts +329 -0
  9. package/dist/cascade.d.ts.map +1 -0
  10. package/dist/cascade.js +522 -0
  11. package/dist/cascade.js.map +1 -0
  12. package/dist/client.d.ts +233 -0
  13. package/dist/client.d.ts.map +1 -0
  14. package/dist/client.js +191 -0
  15. package/dist/client.js.map +1 -0
  16. package/dist/durable-cascade.d.ts +280 -0
  17. package/dist/durable-cascade.d.ts.map +1 -0
  18. package/dist/durable-cascade.js +469 -0
  19. package/dist/durable-cascade.js.map +1 -0
  20. package/dist/event-bridge.d.ts +257 -0
  21. package/dist/event-bridge.d.ts.map +1 -0
  22. package/dist/event-bridge.js +317 -0
  23. package/dist/event-bridge.js.map +1 -0
  24. package/dist/generate.d.ts.map +1 -1
  25. package/dist/generate.js +12 -6
  26. package/dist/generate.js.map +1 -1
  27. package/dist/hoc.d.ts.map +1 -1
  28. package/dist/hoc.js +13 -13
  29. package/dist/hoc.js.map +1 -1
  30. package/dist/hono-jsx.d.ts +208 -0
  31. package/dist/hono-jsx.d.ts.map +1 -0
  32. package/dist/hono-jsx.js +459 -0
  33. package/dist/hono-jsx.js.map +1 -0
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +2 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/mdx-types.d.ts +152 -0
  39. package/dist/mdx-types.d.ts.map +1 -0
  40. package/dist/mdx-types.js +9 -0
  41. package/dist/mdx-types.js.map +1 -0
  42. package/dist/mdx-utils.d.ts +106 -0
  43. package/dist/mdx-utils.d.ts.map +1 -0
  44. package/dist/mdx-utils.js +384 -0
  45. package/dist/mdx-utils.js.map +1 -0
  46. package/dist/mdx.d.ts +230 -0
  47. package/dist/mdx.d.ts.map +1 -0
  48. package/dist/mdx.js +820 -0
  49. package/dist/mdx.js.map +1 -0
  50. package/dist/rpc.d.ts +313 -0
  51. package/dist/rpc.d.ts.map +1 -0
  52. package/dist/rpc.js +359 -0
  53. package/dist/rpc.js.map +1 -0
  54. package/dist/streaming.d.ts +199 -0
  55. package/dist/streaming.d.ts.map +1 -0
  56. package/dist/streaming.js +402 -0
  57. package/dist/streaming.js.map +1 -0
  58. package/dist/validate.d.ts.map +1 -1
  59. package/dist/validate.js +11 -13
  60. package/dist/validate.js.map +1 -1
  61. package/dist/worker.d.ts +270 -0
  62. package/dist/worker.d.ts.map +1 -0
  63. package/dist/worker.js +405 -0
  64. package/dist/worker.js.map +1 -0
  65. package/package.json +39 -13
  66. package/src/ai.ts +12 -31
  67. package/src/cascade.ts +795 -0
  68. package/src/client.ts +440 -0
  69. package/src/durable-cascade.ts +743 -0
  70. package/src/event-bridge.ts +478 -0
  71. package/src/generate.ts +14 -12
  72. package/src/hoc.ts +15 -19
  73. package/src/hono-jsx.ts +675 -0
  74. package/src/index.ts +30 -0
  75. package/src/mdx-types.ts +169 -0
  76. package/src/mdx-utils.ts +437 -0
  77. package/src/mdx.ts +1008 -0
  78. package/src/rpc.ts +614 -0
  79. package/src/streaming.ts +618 -0
  80. package/src/validate.ts +15 -29
  81. package/src/worker.ts +547 -0
  82. package/test/cascade.test.ts +338 -0
  83. package/test/durable-cascade.test.ts +319 -0
  84. package/test/event-bridge.test.ts +351 -0
  85. package/test/generate.test.ts +6 -16
  86. package/test/mdx.test.ts +817 -0
  87. package/test/worker/capnweb-rpc.test.ts +1084 -0
  88. package/test/worker/full-flow.integration.test.ts +1463 -0
  89. package/test/worker/hono-jsx.test.ts +1258 -0
  90. package/test/worker/mdx-parsing.test.ts +1148 -0
  91. package/test/worker/setup.ts +56 -0
  92. package/test/worker.test.ts +595 -0
  93. package/tsconfig.json +2 -1
  94. package/vitest.config.js +6 -0
  95. package/vitest.config.ts +15 -1
  96. package/vitest.workers.config.ts +58 -0
  97. package/wrangler.jsonc +27 -0
  98. package/LICENSE +0 -21
  99. package/src/ai.js +0 -198
  100. package/src/cache.js +0 -182
  101. package/src/generate.js +0 -220
  102. package/src/hoc.js +0 -235
  103. package/src/index.js +0 -20
  104. package/src/types.js +0 -6
  105. package/src/validate.js +0 -252
@@ -0,0 +1,817 @@
1
+ /**
2
+ * Tests for MDX parsing and rendering with AI-generated props
3
+ *
4
+ * RED phase: These tests define the expected behavior for MDX integration.
5
+ * They will fail because the MDX parsing module does not exist yet.
6
+ *
7
+ * The MDX integration should:
8
+ * 1. Parse MDX content strings (with frontmatter)
9
+ * 2. Extract component prop schemas from MDX
10
+ * 3. Generate AI props for components found in MDX
11
+ * 4. Render MDX with injected props
12
+ * 5. Handle errors gracefully
13
+ */
14
+
15
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
16
+
17
+ // Mock the ai-functions generateObject (same pattern as other test files)
18
+ vi.mock('ai-functions', () => ({
19
+ generateObject: vi.fn().mockImplementation(async ({ schema }) => {
20
+ const mockData: Record<string, unknown> = {}
21
+ for (const [key, value] of Object.entries(schema)) {
22
+ if (typeof value === 'string') {
23
+ if (value.includes('(number)')) {
24
+ mockData[key] = 42
25
+ } else if (value.includes('(boolean)')) {
26
+ mockData[key] = true
27
+ } else {
28
+ mockData[key] = `generated-${key}`
29
+ }
30
+ } else if (Array.isArray(value)) {
31
+ mockData[key] = ['item1', 'item2']
32
+ } else if (typeof value === 'object') {
33
+ mockData[key] = { nested: 'value' }
34
+ }
35
+ }
36
+ return { object: mockData }
37
+ }),
38
+ schema: vi.fn((s) => s),
39
+ }))
40
+
41
+ // These imports will fail since the mdx module does not exist yet
42
+ import {
43
+ parseMDX,
44
+ extractComponentSchemas,
45
+ renderMDXWithProps,
46
+ createMDXPropsGenerator,
47
+ compileMDX,
48
+ } from '../src/mdx.js'
49
+ import { resetConfig, clearCache } from '../src/index.js'
50
+
51
+ describe('MDX parsing', () => {
52
+ beforeEach(() => {
53
+ resetConfig()
54
+ clearCache()
55
+ vi.clearAllMocks()
56
+ })
57
+
58
+ describe('parseMDX()', () => {
59
+ it('parses simple MDX content string', () => {
60
+ const mdx = `# Hello World
61
+
62
+ This is a simple MDX document.`
63
+
64
+ const result = parseMDX(mdx)
65
+
66
+ expect(result).toBeDefined()
67
+ expect(result.content).toBeDefined()
68
+ expect(result.frontmatter).toEqual({})
69
+ })
70
+
71
+ it('parses MDX content with frontmatter', () => {
72
+ const mdx = `---
73
+ title: My Page
74
+ description: A test page
75
+ author: Test Author
76
+ ---
77
+
78
+ # {title}
79
+
80
+ {description}`
81
+
82
+ const result = parseMDX(mdx)
83
+
84
+ expect(result.frontmatter).toBeDefined()
85
+ expect(result.frontmatter.title).toBe('My Page')
86
+ expect(result.frontmatter.description).toBe('A test page')
87
+ expect(result.frontmatter.author).toBe('Test Author')
88
+ })
89
+
90
+ it('parses MDX content with YAML frontmatter types', () => {
91
+ const mdx = `---
92
+ title: My Page
93
+ count: 5
94
+ published: true
95
+ tags:
96
+ - ai
97
+ - props
98
+ ---
99
+
100
+ # Content`
101
+
102
+ const result = parseMDX(mdx)
103
+
104
+ expect(result.frontmatter.title).toBe('My Page')
105
+ expect(result.frontmatter.count).toBe(5)
106
+ expect(result.frontmatter.published).toBe(true)
107
+ expect(result.frontmatter.tags).toEqual(['ai', 'props'])
108
+ })
109
+
110
+ it('separates frontmatter from body content', () => {
111
+ const mdx = `---
112
+ title: Hello
113
+ ---
114
+
115
+ # Body Content
116
+
117
+ Some paragraph text.`
118
+
119
+ const result = parseMDX(mdx)
120
+
121
+ expect(result.body).not.toContain('---')
122
+ expect(result.body).not.toContain('title: Hello')
123
+ expect(result.body).toContain('# Body Content')
124
+ expect(result.body).toContain('Some paragraph text.')
125
+ })
126
+
127
+ it('handles MDX with no frontmatter', () => {
128
+ const mdx = `# No Frontmatter
129
+
130
+ Just regular content.`
131
+
132
+ const result = parseMDX(mdx)
133
+
134
+ expect(result.frontmatter).toEqual({})
135
+ expect(result.body).toContain('# No Frontmatter')
136
+ })
137
+
138
+ it('handles empty MDX string', () => {
139
+ const result = parseMDX('')
140
+
141
+ expect(result).toBeDefined()
142
+ expect(result.frontmatter).toEqual({})
143
+ expect(result.body).toBe('')
144
+ })
145
+
146
+ it('identifies JSX components in MDX', () => {
147
+ const mdx = `---
148
+ title: Page
149
+ ---
150
+
151
+ # Hello
152
+
153
+ <Hero title="Welcome" />
154
+
155
+ <Card>
156
+ <CardBody>Some content</CardBody>
157
+ </Card>
158
+
159
+ <Footer />`
160
+
161
+ const result = parseMDX(mdx)
162
+
163
+ expect(result.components).toBeDefined()
164
+ expect(result.components).toContain('Hero')
165
+ expect(result.components).toContain('Card')
166
+ expect(result.components).toContain('CardBody')
167
+ expect(result.components).toContain('Footer')
168
+ })
169
+
170
+ it('identifies components with props in MDX', () => {
171
+ const mdx = `<Button variant="primary" size="lg" disabled>Click Me</Button>
172
+ <Input placeholder="Enter text" type="email" />`
173
+
174
+ const result = parseMDX(mdx)
175
+
176
+ expect(result.componentProps).toBeDefined()
177
+ expect(result.componentProps.Button).toEqual({
178
+ variant: 'primary',
179
+ size: 'lg',
180
+ disabled: true,
181
+ })
182
+ expect(result.componentProps.Input).toEqual({
183
+ placeholder: 'Enter text',
184
+ type: 'email',
185
+ })
186
+ })
187
+ })
188
+
189
+ describe('extractComponentSchemas()', () => {
190
+ it('extracts prop schemas from MDX component usage', () => {
191
+ const mdx = `<UserCard name="John" bio="A developer" avatar="/img.png" />`
192
+
193
+ const schemas = extractComponentSchemas(mdx)
194
+
195
+ expect(schemas).toBeDefined()
196
+ expect(schemas.UserCard).toBeDefined()
197
+ expect(Object.keys(schemas.UserCard)).toContain('name')
198
+ expect(Object.keys(schemas.UserCard)).toContain('bio')
199
+ expect(Object.keys(schemas.UserCard)).toContain('avatar')
200
+ })
201
+
202
+ it('extracts schemas from multiple component instances', () => {
203
+ const mdx = `<Card title="First" />
204
+ <Card title="Second" description="With desc" />`
205
+
206
+ const schemas = extractComponentSchemas(mdx)
207
+
208
+ // Should merge schemas from multiple instances
209
+ expect(schemas.Card).toBeDefined()
210
+ expect(Object.keys(schemas.Card)).toContain('title')
211
+ expect(Object.keys(schemas.Card)).toContain('description')
212
+ })
213
+
214
+ it('identifies components that need AI-generated props', () => {
215
+ const mdx = `<Hero />
216
+ <Card title="Provided" />
217
+ <Footer />`
218
+
219
+ const schemas = extractComponentSchemas(mdx)
220
+
221
+ // Components with no props should be flagged as needing generation
222
+ expect(schemas.Hero).toBeDefined()
223
+ expect(Object.keys(schemas.Hero)).toHaveLength(0)
224
+ expect(schemas.Footer).toBeDefined()
225
+ expect(Object.keys(schemas.Footer)).toHaveLength(0)
226
+ })
227
+
228
+ it('handles components with expression props', () => {
229
+ const mdx = `<Widget count={42} active={true} data={{ key: 'value' }} />`
230
+
231
+ const schemas = extractComponentSchemas(mdx)
232
+
233
+ expect(schemas.Widget).toBeDefined()
234
+ expect(Object.keys(schemas.Widget)).toContain('count')
235
+ expect(Object.keys(schemas.Widget)).toContain('active')
236
+ expect(Object.keys(schemas.Widget)).toContain('data')
237
+ })
238
+
239
+ it('returns empty object for MDX with no components', () => {
240
+ const mdx = `# Just Markdown
241
+
242
+ Regular paragraph text with **bold** and *italic*.`
243
+
244
+ const schemas = extractComponentSchemas(mdx)
245
+
246
+ expect(schemas).toEqual({})
247
+ })
248
+ })
249
+ })
250
+
251
+ describe('MDX props generation', () => {
252
+ beforeEach(() => {
253
+ resetConfig()
254
+ clearCache()
255
+ vi.clearAllMocks()
256
+ })
257
+
258
+ describe('createMDXPropsGenerator()', () => {
259
+ it('creates a props generator for MDX content', () => {
260
+ const generator = createMDXPropsGenerator({
261
+ schemas: {
262
+ Hero: {
263
+ title: 'Hero section title',
264
+ subtitle: 'Hero section subtitle',
265
+ },
266
+ Card: {
267
+ title: 'Card title',
268
+ description: 'Card description',
269
+ },
270
+ },
271
+ })
272
+
273
+ expect(generator).toBeDefined()
274
+ expect(typeof generator.generate).toBe('function')
275
+ })
276
+
277
+ it('generates props for components in MDX', async () => {
278
+ const generator = createMDXPropsGenerator({
279
+ schemas: {
280
+ Hero: {
281
+ title: 'Hero section title',
282
+ subtitle: 'Hero section subtitle',
283
+ },
284
+ },
285
+ })
286
+
287
+ const mdx = `---
288
+ topic: AI Applications
289
+ ---
290
+
291
+ <Hero />`
292
+
293
+ const result = await generator.generate(mdx)
294
+
295
+ expect(result).toBeDefined()
296
+ expect(result.Hero).toBeDefined()
297
+ expect(result.Hero.title).toBeDefined()
298
+ expect(result.Hero.subtitle).toBeDefined()
299
+ })
300
+
301
+ it('uses frontmatter context for generation', async () => {
302
+ const generator = createMDXPropsGenerator({
303
+ schemas: {
304
+ Hero: {
305
+ title: 'A title relevant to the page topic',
306
+ },
307
+ },
308
+ })
309
+
310
+ const mdx = `---
311
+ topic: Machine Learning
312
+ audience: developers
313
+ ---
314
+
315
+ <Hero />`
316
+
317
+ const result = await generator.generate(mdx)
318
+
319
+ expect(result.Hero).toBeDefined()
320
+ expect(result.Hero.title).toBeDefined()
321
+ // The generator should have used frontmatter as context
322
+ })
323
+
324
+ it('preserves explicitly provided props', async () => {
325
+ const generator = createMDXPropsGenerator({
326
+ schemas: {
327
+ Card: {
328
+ title: 'Card title',
329
+ description: 'Card description',
330
+ image: 'Image URL',
331
+ },
332
+ },
333
+ })
334
+
335
+ const mdx = `<Card title="My Explicit Title" />`
336
+
337
+ const result = await generator.generate(mdx)
338
+
339
+ // Explicit props should be preserved
340
+ expect(result.Card.title).toBe('My Explicit Title')
341
+ // Missing props should be generated
342
+ expect(result.Card.description).toBeDefined()
343
+ expect(result.Card.image).toBeDefined()
344
+ })
345
+
346
+ it('generates props for multiple component instances', async () => {
347
+ const generator = createMDXPropsGenerator({
348
+ schemas: {
349
+ Card: {
350
+ title: 'Card title',
351
+ description: 'Card description',
352
+ },
353
+ },
354
+ })
355
+
356
+ const mdx = `<Card />
357
+ <Card />
358
+ <Card />`
359
+
360
+ const result = await generator.generate(mdx)
361
+
362
+ // Should generate props for each instance
363
+ expect(result.Card).toBeDefined()
364
+ expect(Array.isArray(result.Card) || typeof result.Card === 'object').toBe(true)
365
+ })
366
+
367
+ it('caches generated props per component', async () => {
368
+ const generator = createMDXPropsGenerator({
369
+ schemas: {
370
+ Hero: {
371
+ title: 'Hero title',
372
+ },
373
+ },
374
+ cache: true,
375
+ })
376
+
377
+ const mdx = `<Hero />`
378
+
379
+ // First generation
380
+ const result1 = await generator.generate(mdx)
381
+
382
+ // Second generation (same content) should use cache
383
+ const result2 = await generator.generate(mdx)
384
+
385
+ expect(result1.Hero.title).toEqual(result2.Hero.title)
386
+ })
387
+
388
+ it('handles async prop generation', async () => {
389
+ const generator = createMDXPropsGenerator({
390
+ schemas: {
391
+ AsyncWidget: {
392
+ data: 'Complex data structure',
393
+ status: 'Widget status',
394
+ },
395
+ },
396
+ })
397
+
398
+ const mdx = `<AsyncWidget />`
399
+
400
+ const result = await generator.generate(mdx)
401
+
402
+ expect(result.AsyncWidget).toBeDefined()
403
+ expect(result.AsyncWidget.data).toBeDefined()
404
+ expect(result.AsyncWidget.status).toBeDefined()
405
+ })
406
+
407
+ it('passes custom model configuration', async () => {
408
+ const generator = createMDXPropsGenerator({
409
+ schemas: {
410
+ Hero: { title: 'Title' },
411
+ },
412
+ model: 'gpt-4',
413
+ })
414
+
415
+ const mdx = `<Hero />`
416
+
417
+ const result = await generator.generate(mdx)
418
+
419
+ expect(result.Hero).toBeDefined()
420
+ })
421
+ })
422
+ })
423
+
424
+ describe('MDX rendering with props', () => {
425
+ beforeEach(() => {
426
+ resetConfig()
427
+ clearCache()
428
+ vi.clearAllMocks()
429
+ })
430
+
431
+ describe('renderMDXWithProps()', () => {
432
+ it('renders MDX string with injected props', async () => {
433
+ const mdx = `# Hello
434
+
435
+ <Hero title="Welcome" subtitle="To the future" />`
436
+
437
+ const result = await renderMDXWithProps(mdx, {
438
+ Hero: {
439
+ title: 'Welcome',
440
+ subtitle: 'To the future',
441
+ },
442
+ })
443
+
444
+ expect(result).toBeDefined()
445
+ expect(typeof result).toBe('string')
446
+ expect(result).toContain('Welcome')
447
+ expect(result).toContain('To the future')
448
+ })
449
+
450
+ it('renders MDX with generated props for components missing props', async () => {
451
+ const mdx = `<Hero />`
452
+
453
+ const result = await renderMDXWithProps(mdx, {
454
+ Hero: {
455
+ title: 'Generated Title',
456
+ subtitle: 'Generated Subtitle',
457
+ },
458
+ })
459
+
460
+ expect(result).toContain('Generated Title')
461
+ })
462
+
463
+ it('passes props to custom component renderers', async () => {
464
+ const mdx = `<Card title="Test" description="A card" />`
465
+
466
+ const components = {
467
+ Card: (props: Record<string, unknown>) => {
468
+ return `<div class="card"><h2>${props.title}</h2><p>${props.description}</p></div>`
469
+ },
470
+ }
471
+
472
+ const result = await renderMDXWithProps(
473
+ mdx,
474
+ { Card: { title: 'Test', description: 'A card' } },
475
+ { components }
476
+ )
477
+
478
+ expect(result).toContain('Test')
479
+ expect(result).toContain('A card')
480
+ })
481
+
482
+ it('preserves component tree structure', async () => {
483
+ const mdx = `<Layout>
484
+ <Header title="Page Title" />
485
+ <Main>
486
+ <Card title="Card 1" />
487
+ <Card title="Card 2" />
488
+ </Main>
489
+ <Footer />
490
+ </Layout>`
491
+
492
+ const props = {
493
+ Layout: {},
494
+ Header: { title: 'Page Title' },
495
+ Main: {},
496
+ Card: { title: 'Card Title' },
497
+ Footer: { copyright: '2026' },
498
+ }
499
+
500
+ const result = await renderMDXWithProps(mdx, props)
501
+
502
+ expect(result).toBeDefined()
503
+ expect(typeof result).toBe('string')
504
+ })
505
+
506
+ it('renders MDX with frontmatter variables', async () => {
507
+ const mdx = `---
508
+ title: Dynamic Page
509
+ author: AI
510
+ ---
511
+
512
+ # {title}
513
+
514
+ Written by {author}.`
515
+
516
+ const result = await renderMDXWithProps(mdx, {})
517
+
518
+ expect(result).toContain('Dynamic Page')
519
+ expect(result).toContain('AI')
520
+ })
521
+
522
+ it('handles streaming render', async () => {
523
+ const mdx = `# Hello
524
+
525
+ <Hero title="Welcome" />
526
+
527
+ Some content after.`
528
+
529
+ const stream = await renderMDXWithProps(
530
+ mdx,
531
+ {
532
+ Hero: { title: 'Welcome' },
533
+ },
534
+ { stream: true }
535
+ )
536
+
537
+ expect(stream).toBeDefined()
538
+ // Stream should be iterable or a ReadableStream
539
+ if (stream instanceof ReadableStream) {
540
+ const reader = stream.getReader()
541
+ const { value } = await reader.read()
542
+ expect(value).toBeDefined()
543
+ reader.releaseLock()
544
+ }
545
+ })
546
+
547
+ it('merges provided props with generated props', async () => {
548
+ const mdx = `<ProductCard name="Widget" />`
549
+
550
+ const result = await renderMDXWithProps(mdx, {
551
+ ProductCard: {
552
+ name: 'Widget',
553
+ price: 29.99,
554
+ description: 'A useful widget',
555
+ },
556
+ })
557
+
558
+ expect(result).toContain('Widget')
559
+ })
560
+ })
561
+
562
+ describe('compileMDX()', () => {
563
+ it('compiles MDX string to executable function', async () => {
564
+ const mdx = `# Hello World
565
+
566
+ This is content.`
567
+
568
+ const compiled = await compileMDX(mdx)
569
+
570
+ expect(compiled).toBeDefined()
571
+ expect(typeof compiled).toBe('function')
572
+ })
573
+
574
+ it('compiled function accepts props argument', async () => {
575
+ const mdx = `<Greeting name="World" />`
576
+
577
+ const compiled = await compileMDX(mdx)
578
+ const result = compiled({
579
+ Greeting: { name: 'World' },
580
+ })
581
+
582
+ expect(result).toBeDefined()
583
+ })
584
+
585
+ it('compiled function accepts component map', async () => {
586
+ const mdx = `<Custom value="test" />`
587
+
588
+ const compiled = await compileMDX(mdx, {
589
+ components: {
590
+ Custom: (props: Record<string, unknown>) => `Custom: ${props.value}`,
591
+ },
592
+ })
593
+
594
+ const result = compiled({ Custom: { value: 'test' } })
595
+
596
+ expect(result).toContain('test')
597
+ })
598
+
599
+ it('handles MDX with imports', async () => {
600
+ const mdx = `import { Button } from './components'
601
+
602
+ # Page
603
+
604
+ <Button variant="primary">Click</Button>`
605
+
606
+ // Should handle import statements without throwing
607
+ const compiled = await compileMDX(mdx)
608
+
609
+ expect(compiled).toBeDefined()
610
+ })
611
+
612
+ it('handles MDX with export statements', async () => {
613
+ const mdx = `export const metadata = { title: 'Test' }
614
+
615
+ # Page Content`
616
+
617
+ const compiled = await compileMDX(mdx)
618
+
619
+ expect(compiled).toBeDefined()
620
+ expect(compiled.metadata).toBeDefined()
621
+ expect(compiled.metadata.title).toBe('Test')
622
+ })
623
+ })
624
+ })
625
+
626
+ describe('MDX error handling', () => {
627
+ beforeEach(() => {
628
+ resetConfig()
629
+ clearCache()
630
+ vi.clearAllMocks()
631
+ })
632
+
633
+ it('throws descriptive error for invalid MDX syntax', () => {
634
+ const invalidMDX = `<Unclosed
635
+
636
+ This has unclosed JSX.`
637
+
638
+ expect(() => parseMDX(invalidMDX)).toThrow()
639
+ })
640
+
641
+ it('throws for malformed frontmatter', () => {
642
+ const invalidMDX = `---
643
+ title: [invalid yaml
644
+ broken: {
645
+ ---
646
+
647
+ # Content`
648
+
649
+ expect(() => parseMDX(invalidMDX)).toThrow()
650
+ })
651
+
652
+ it('provides error location info for invalid MDX', () => {
653
+ const invalidMDX = `# Valid
654
+ <Valid />
655
+ <Invalid prop=>`
656
+
657
+ try {
658
+ parseMDX(invalidMDX)
659
+ // Should not reach here
660
+ expect(true).toBe(false)
661
+ } catch (error) {
662
+ expect(error).toBeInstanceOf(Error)
663
+ const err = error as Error & { line?: number; column?: number }
664
+ // Error should contain location information
665
+ expect(err.message).toBeDefined()
666
+ expect(err.message.length).toBeGreaterThan(0)
667
+ }
668
+ })
669
+
670
+ it('handles MDX compilation errors gracefully', async () => {
671
+ const invalidMDX = `<>{(() => { throw new Error("runtime error") })()}</>`
672
+
673
+ await expect(compileMDX(invalidMDX)).rejects.toThrow()
674
+ })
675
+
676
+ it('handles missing component schemas gracefully in generator', async () => {
677
+ const generator = createMDXPropsGenerator({
678
+ schemas: {},
679
+ })
680
+
681
+ const mdx = `<UnknownComponent />`
682
+
683
+ // Should not throw, but should return empty or skip unknown components
684
+ const result = await generator.generate(mdx)
685
+
686
+ expect(result).toBeDefined()
687
+ expect(result.UnknownComponent).toBeUndefined()
688
+ })
689
+
690
+ it('handles render errors for invalid props', async () => {
691
+ const mdx = `<StrictComponent />`
692
+
693
+ // Rendering with invalid/missing required props
694
+ await expect(
695
+ renderMDXWithProps(mdx, {
696
+ StrictComponent: null as unknown as Record<string, unknown>,
697
+ })
698
+ ).rejects.toThrow()
699
+ })
700
+ })
701
+
702
+ describe('MDX with AI props end-to-end', () => {
703
+ beforeEach(() => {
704
+ resetConfig()
705
+ clearCache()
706
+ vi.clearAllMocks()
707
+ })
708
+
709
+ it('parses MDX, generates props, and renders in one pipeline', async () => {
710
+ const mdx = `---
711
+ topic: Artificial Intelligence
712
+ audience: developers
713
+ ---
714
+
715
+ # {topic} Guide
716
+
717
+ <Hero />
718
+
719
+ <Card />
720
+
721
+ <Footer />`
722
+
723
+ // Step 1: Parse
724
+ const parsed = parseMDX(mdx)
725
+
726
+ expect(parsed.frontmatter.topic).toBe('Artificial Intelligence')
727
+ expect(parsed.components).toContain('Hero')
728
+ expect(parsed.components).toContain('Card')
729
+ expect(parsed.components).toContain('Footer')
730
+
731
+ // Step 2: Create generator with schemas
732
+ const generator = createMDXPropsGenerator({
733
+ schemas: {
734
+ Hero: {
735
+ title: 'Hero title for the page topic',
736
+ subtitle: 'Hero subtitle',
737
+ },
738
+ Card: {
739
+ title: 'Card title',
740
+ description: 'Card description',
741
+ },
742
+ Footer: {
743
+ copyright: 'Copyright notice',
744
+ },
745
+ },
746
+ })
747
+
748
+ // Step 3: Generate props
749
+ const generatedProps = await generator.generate(mdx)
750
+
751
+ expect(generatedProps.Hero).toBeDefined()
752
+ expect(generatedProps.Card).toBeDefined()
753
+ expect(generatedProps.Footer).toBeDefined()
754
+
755
+ // Step 4: Render with generated props
756
+ const rendered = await renderMDXWithProps(mdx, generatedProps)
757
+
758
+ expect(rendered).toBeDefined()
759
+ expect(typeof rendered).toBe('string')
760
+ expect(rendered.length).toBeGreaterThan(0)
761
+ })
762
+
763
+ it('supports schema-less generation from component usage', async () => {
764
+ const mdx = `<ProductCard
765
+ name="AI Widget"
766
+ price={29.99}
767
+ category="technology"
768
+ />`
769
+
770
+ // Extract schemas from actual component usage
771
+ const schemas = extractComponentSchemas(mdx)
772
+
773
+ expect(schemas.ProductCard).toBeDefined()
774
+ expect(Object.keys(schemas.ProductCard)).toContain('name')
775
+ expect(Object.keys(schemas.ProductCard)).toContain('price')
776
+ expect(Object.keys(schemas.ProductCard)).toContain('category')
777
+
778
+ // Use extracted schemas for generation
779
+ const generator = createMDXPropsGenerator({ schemas })
780
+ const props = await generator.generate(mdx)
781
+
782
+ expect(props.ProductCard).toBeDefined()
783
+ })
784
+
785
+ it('handles MDX with mixed static and AI-generated content', async () => {
786
+ const mdx = `---
787
+ title: Product Page
788
+ ---
789
+
790
+ # {title}
791
+
792
+ <StaticBanner text="Sale!" />
793
+
794
+ <DynamicRecommendation />
795
+
796
+ <StaticFooter year={2026} />`
797
+
798
+ const generator = createMDXPropsGenerator({
799
+ schemas: {
800
+ DynamicRecommendation: {
801
+ product: 'Recommended product name',
802
+ reason: 'Why this product is recommended',
803
+ },
804
+ },
805
+ })
806
+
807
+ const props = await generator.generate(mdx)
808
+
809
+ // Only DynamicRecommendation should have generated props
810
+ expect(props.DynamicRecommendation).toBeDefined()
811
+ expect(props.DynamicRecommendation.product).toBeDefined()
812
+ expect(props.DynamicRecommendation.reason).toBeDefined()
813
+ // Static components should not be in generated props
814
+ expect(props.StaticBanner).toBeUndefined()
815
+ expect(props.StaticFooter).toBeUndefined()
816
+ })
817
+ })