ai-props 2.3.0 → 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 (75) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +9 -0
  3. package/dist/ai.d.ts +125 -0
  4. package/dist/ai.d.ts.map +1 -0
  5. package/dist/ai.js +199 -0
  6. package/dist/ai.js.map +1 -0
  7. package/dist/cache.d.ts +66 -0
  8. package/dist/cache.d.ts.map +1 -0
  9. package/dist/cache.js +183 -0
  10. package/dist/cache.js.map +1 -0
  11. package/dist/cascade.d.ts +329 -0
  12. package/dist/cascade.d.ts.map +1 -0
  13. package/dist/cascade.js +522 -0
  14. package/dist/cascade.js.map +1 -0
  15. package/dist/client.d.ts +233 -0
  16. package/dist/client.d.ts.map +1 -0
  17. package/dist/client.js +191 -0
  18. package/dist/client.js.map +1 -0
  19. package/dist/durable-cascade.d.ts +280 -0
  20. package/dist/durable-cascade.d.ts.map +1 -0
  21. package/dist/durable-cascade.js +469 -0
  22. package/dist/durable-cascade.js.map +1 -0
  23. package/dist/event-bridge.d.ts +257 -0
  24. package/dist/event-bridge.d.ts.map +1 -0
  25. package/dist/event-bridge.js +317 -0
  26. package/dist/event-bridge.js.map +1 -0
  27. package/dist/generate.d.ts +69 -0
  28. package/dist/generate.d.ts.map +1 -0
  29. package/dist/generate.js +227 -0
  30. package/dist/generate.js.map +1 -0
  31. package/dist/hoc.d.ts +164 -0
  32. package/dist/hoc.d.ts.map +1 -0
  33. package/dist/hoc.js +236 -0
  34. package/dist/hoc.js.map +1 -0
  35. package/dist/hono-jsx.d.ts +208 -0
  36. package/dist/hono-jsx.d.ts.map +1 -0
  37. package/dist/hono-jsx.js +459 -0
  38. package/dist/hono-jsx.js.map +1 -0
  39. package/dist/index.d.ts +16 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +23 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/mdx-types.d.ts +152 -0
  44. package/dist/mdx-types.d.ts.map +1 -0
  45. package/dist/mdx-types.js +9 -0
  46. package/dist/mdx-types.js.map +1 -0
  47. package/dist/mdx-utils.d.ts +106 -0
  48. package/dist/mdx-utils.d.ts.map +1 -0
  49. package/dist/mdx-utils.js +384 -0
  50. package/dist/mdx-utils.js.map +1 -0
  51. package/dist/mdx.d.ts +230 -0
  52. package/dist/mdx.d.ts.map +1 -0
  53. package/dist/mdx.js +820 -0
  54. package/dist/mdx.js.map +1 -0
  55. package/dist/rpc.d.ts +313 -0
  56. package/dist/rpc.d.ts.map +1 -0
  57. package/dist/rpc.js +359 -0
  58. package/dist/rpc.js.map +1 -0
  59. package/dist/streaming.d.ts +199 -0
  60. package/dist/streaming.d.ts.map +1 -0
  61. package/dist/streaming.js +402 -0
  62. package/dist/streaming.js.map +1 -0
  63. package/dist/types.d.ts +152 -0
  64. package/dist/types.d.ts.map +1 -0
  65. package/dist/types.js +7 -0
  66. package/dist/types.js.map +1 -0
  67. package/dist/validate.d.ts +58 -0
  68. package/dist/validate.d.ts.map +1 -0
  69. package/dist/validate.js +251 -0
  70. package/dist/validate.js.map +1 -0
  71. package/dist/worker.d.ts +270 -0
  72. package/dist/worker.d.ts.map +1 -0
  73. package/dist/worker.js +405 -0
  74. package/dist/worker.js.map +1 -0
  75. package/package.json +4 -4
package/dist/mdx.js ADDED
@@ -0,0 +1,820 @@
1
+ /**
2
+ * MDX parsing and rendering with AI-generated props
3
+ *
4
+ * Provides utilities for parsing MDX content, extracting component schemas,
5
+ * and rendering with AI-generated props.
6
+ *
7
+ * Key features:
8
+ * - Content-hash based caching for parsed MDX
9
+ * - Parallel prop generation for multiple components
10
+ * - Streaming-ready architecture
11
+ * - Graceful error handling with detailed messages
12
+ *
13
+ * @packageDocumentation
14
+ */
15
+ import { generateObject } from 'ai-functions';
16
+ import { getDefaultCache, createCacheKey } from './cache.js';
17
+ import { hashContent, parseYAML, extractComponents, extractPropsFromTag, extractComponentProps, validateMDX, serializeProps, createMDXCacheKey, MDX_CACHE_TTL, createParseError, } from './mdx-utils.js';
18
+ // ============================================================================
19
+ // Parsed MDX Cache
20
+ // ============================================================================
21
+ /**
22
+ * LRU cache for parsed MDX content with advanced invalidation strategies
23
+ *
24
+ * Features:
25
+ * - Content-hash based lookups for efficient cache hits
26
+ * - TTL-based expiration
27
+ * - LRU eviction when at capacity
28
+ * - Tag-based invalidation for grouped cache clearing
29
+ * - Cache statistics for monitoring
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * const cache = new MDXParseCache({ maxSize: 200, ttl: 10 * 60 * 1000 })
34
+ *
35
+ * // Set with tags for group invalidation
36
+ * cache.set('hash123', parsed, ['component:Hero', 'page:home'])
37
+ *
38
+ * // Invalidate all entries tagged with 'page:home'
39
+ * cache.invalidateByTag('page:home')
40
+ *
41
+ * // Get cache statistics
42
+ * const stats = cache.getStats()
43
+ * console.log(`Hit ratio: ${stats.hitRatio}`)
44
+ * ```
45
+ */
46
+ class MDXParseCache {
47
+ cache = new Map();
48
+ tagIndex = new Map(); // tag -> set of cache keys
49
+ maxSize;
50
+ ttl;
51
+ hits = 0;
52
+ misses = 0;
53
+ cleanupTimer = null;
54
+ constructor(options = {}) {
55
+ this.maxSize = options.maxSize ?? 100;
56
+ this.ttl = options.ttl ?? MDX_CACHE_TTL;
57
+ // Set up automatic cleanup if enabled
58
+ if (options.autoCleanup) {
59
+ const interval = options.cleanupInterval ?? 60000;
60
+ this.cleanupTimer = setInterval(() => this.cleanup(), interval);
61
+ }
62
+ }
63
+ /**
64
+ * Get cached parse result by content hash
65
+ *
66
+ * @param hash - Content hash key
67
+ * @returns Parsed MDX or undefined if not found/expired
68
+ */
69
+ get(hash) {
70
+ const entry = this.cache.get(hash);
71
+ if (!entry) {
72
+ this.misses++;
73
+ return undefined;
74
+ }
75
+ // Check TTL
76
+ if (Date.now() - entry.timestamp > this.ttl) {
77
+ this.evict(hash);
78
+ this.misses++;
79
+ return undefined;
80
+ }
81
+ // Move to end (LRU)
82
+ this.cache.delete(hash);
83
+ this.cache.set(hash, entry);
84
+ this.hits++;
85
+ return entry.parsed;
86
+ }
87
+ /**
88
+ * Set cached parse result with optional tags
89
+ *
90
+ * @param hash - Content hash key
91
+ * @param parsed - Parsed MDX result
92
+ * @param tags - Optional tags for group invalidation
93
+ */
94
+ set(hash, parsed, tags) {
95
+ // Evict oldest if at capacity
96
+ while (this.cache.size >= this.maxSize) {
97
+ const oldest = this.cache.keys().next().value;
98
+ if (oldest) {
99
+ this.evict(oldest);
100
+ }
101
+ else {
102
+ break;
103
+ }
104
+ }
105
+ const entry = {
106
+ hash,
107
+ parsed,
108
+ timestamp: Date.now(),
109
+ };
110
+ if (tags) {
111
+ entry.tags = tags;
112
+ }
113
+ this.cache.set(hash, entry);
114
+ // Update tag index
115
+ if (tags) {
116
+ for (const tag of tags) {
117
+ if (!this.tagIndex.has(tag)) {
118
+ this.tagIndex.set(tag, new Set());
119
+ }
120
+ this.tagIndex.get(tag).add(hash);
121
+ }
122
+ }
123
+ }
124
+ /**
125
+ * Invalidate a specific cache entry
126
+ *
127
+ * @param hash - Content hash key to invalidate
128
+ * @returns True if entry was found and removed
129
+ */
130
+ invalidate(hash) {
131
+ return this.evict(hash);
132
+ }
133
+ /**
134
+ * Invalidate all cache entries with a specific tag
135
+ *
136
+ * Use this for grouped invalidation, e.g., when a component schema changes
137
+ * or when refreshing all entries for a specific page.
138
+ *
139
+ * @param tag - Tag to invalidate
140
+ * @returns Number of entries invalidated
141
+ *
142
+ * @example
143
+ * ```ts
144
+ * // Invalidate all Hero component cache entries
145
+ * cache.invalidateByTag('component:Hero')
146
+ *
147
+ * // Invalidate all entries for a specific page
148
+ * cache.invalidateByTag('page:/products')
149
+ * ```
150
+ */
151
+ invalidateByTag(tag) {
152
+ const keys = this.tagIndex.get(tag);
153
+ if (!keys)
154
+ return 0;
155
+ let count = 0;
156
+ for (const hash of keys) {
157
+ if (this.evict(hash)) {
158
+ count++;
159
+ }
160
+ }
161
+ this.tagIndex.delete(tag);
162
+ return count;
163
+ }
164
+ /**
165
+ * Invalidate all entries matching a tag pattern
166
+ *
167
+ * @param pattern - Regex pattern to match tags
168
+ * @returns Number of entries invalidated
169
+ *
170
+ * @example
171
+ * ```ts
172
+ * // Invalidate all component-related entries
173
+ * cache.invalidateByTagPattern(/^component:/)
174
+ * ```
175
+ */
176
+ invalidateByTagPattern(pattern) {
177
+ let count = 0;
178
+ for (const tag of this.tagIndex.keys()) {
179
+ if (pattern.test(tag)) {
180
+ count += this.invalidateByTag(tag);
181
+ }
182
+ }
183
+ return count;
184
+ }
185
+ /**
186
+ * Clear all cached entries
187
+ */
188
+ clear() {
189
+ this.cache.clear();
190
+ this.tagIndex.clear();
191
+ this.hits = 0;
192
+ this.misses = 0;
193
+ }
194
+ /**
195
+ * Get current cache size
196
+ */
197
+ get size() {
198
+ return this.cache.size;
199
+ }
200
+ /**
201
+ * Get cache statistics
202
+ *
203
+ * @returns Cache statistics including hit ratio
204
+ */
205
+ getStats() {
206
+ const total = this.hits + this.misses;
207
+ return {
208
+ hits: this.hits,
209
+ misses: this.misses,
210
+ size: this.cache.size,
211
+ maxSize: this.maxSize,
212
+ hitRatio: total > 0 ? this.hits / total : 0,
213
+ };
214
+ }
215
+ /**
216
+ * Remove expired entries
217
+ *
218
+ * @returns Number of entries removed
219
+ */
220
+ cleanup() {
221
+ const now = Date.now();
222
+ let removed = 0;
223
+ for (const [hash, entry] of this.cache) {
224
+ if (now - entry.timestamp > this.ttl) {
225
+ this.evict(hash);
226
+ removed++;
227
+ }
228
+ }
229
+ return removed;
230
+ }
231
+ /**
232
+ * Destroy the cache and cleanup timers
233
+ */
234
+ destroy() {
235
+ if (this.cleanupTimer) {
236
+ clearInterval(this.cleanupTimer);
237
+ this.cleanupTimer = null;
238
+ }
239
+ this.clear();
240
+ }
241
+ /**
242
+ * Internal method to evict a cache entry and update tag index
243
+ */
244
+ evict(hash) {
245
+ const entry = this.cache.get(hash);
246
+ if (!entry)
247
+ return false;
248
+ // Remove from tag index
249
+ if (entry.tags) {
250
+ for (const tag of entry.tags) {
251
+ const keys = this.tagIndex.get(tag);
252
+ if (keys) {
253
+ keys.delete(hash);
254
+ if (keys.size === 0) {
255
+ this.tagIndex.delete(tag);
256
+ }
257
+ }
258
+ }
259
+ }
260
+ return this.cache.delete(hash);
261
+ }
262
+ }
263
+ // Global parse cache instance
264
+ let parseCache = new MDXParseCache();
265
+ /**
266
+ * Configure the global MDX parse cache
267
+ *
268
+ * @param options - Cache configuration options
269
+ *
270
+ * @example
271
+ * ```ts
272
+ * // Configure with larger cache and longer TTL
273
+ * configureMDXCache({
274
+ * maxSize: 500,
275
+ * ttl: 30 * 60 * 1000, // 30 minutes
276
+ * autoCleanup: true,
277
+ * })
278
+ * ```
279
+ */
280
+ export function configureMDXCache(options) {
281
+ // Destroy old cache to clean up timers
282
+ parseCache.destroy();
283
+ parseCache = new MDXParseCache(options);
284
+ }
285
+ /**
286
+ * Get MDX cache statistics
287
+ *
288
+ * Useful for monitoring cache performance and tuning configuration.
289
+ *
290
+ * @returns Cache statistics
291
+ *
292
+ * @example
293
+ * ```ts
294
+ * const stats = getMDXCacheStats()
295
+ * console.log(`Cache hit ratio: ${(stats.hitRatio * 100).toFixed(1)}%`)
296
+ * console.log(`Cache size: ${stats.size}/${stats.maxSize}`)
297
+ * ```
298
+ */
299
+ export function getMDXCacheStats() {
300
+ return parseCache.getStats();
301
+ }
302
+ /**
303
+ * Invalidate MDX cache entries by tag
304
+ *
305
+ * @param tag - Tag to invalidate
306
+ * @returns Number of entries invalidated
307
+ */
308
+ export function invalidateMDXCacheByTag(tag) {
309
+ return parseCache.invalidateByTag(tag);
310
+ }
311
+ /**
312
+ * Cleanup expired MDX cache entries
313
+ *
314
+ * @returns Number of entries removed
315
+ */
316
+ export function cleanupMDXCache() {
317
+ return parseCache.cleanup();
318
+ }
319
+ /**
320
+ * Parse MDX content string
321
+ *
322
+ * Extracts frontmatter, identifies components, and parses component props.
323
+ * Results are cached based on content hash for performance.
324
+ *
325
+ * @param mdx - MDX content string
326
+ * @param options - Parse options (optional)
327
+ * @returns Parsed MDX structure
328
+ *
329
+ * @example
330
+ * ```ts
331
+ * const result = parseMDX(`---
332
+ * title: Hello
333
+ * ---
334
+ *
335
+ * # {title}
336
+ *
337
+ * <Hero />
338
+ * `)
339
+ *
340
+ * console.log(result.frontmatter.title) // 'Hello'
341
+ * console.log(result.components) // ['Hero']
342
+ * ```
343
+ *
344
+ * @example
345
+ * ```ts
346
+ * // Parse with cache tags for group invalidation
347
+ * const result = parseMDX(content, {
348
+ * tags: ['page:/home', 'component:Hero']
349
+ * })
350
+ *
351
+ * // Later, invalidate all home page entries
352
+ * invalidateMDXCacheByTag('page:/home')
353
+ * ```
354
+ */
355
+ export function parseMDX(mdx, options) {
356
+ const { tags, skipCache = false } = options ?? {};
357
+ // Check cache first (unless skipped)
358
+ const contentHash = hashContent(mdx);
359
+ if (!skipCache) {
360
+ const cached = parseCache.get(contentHash);
361
+ if (cached) {
362
+ return cached;
363
+ }
364
+ }
365
+ let body = mdx;
366
+ let frontmatter = {};
367
+ // Extract frontmatter
368
+ const frontmatterMatch = mdx.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
369
+ if (frontmatterMatch) {
370
+ const yaml = frontmatterMatch[1];
371
+ const rest = frontmatterMatch[2];
372
+ if (yaml !== undefined) {
373
+ frontmatter = parseYAML(yaml);
374
+ }
375
+ if (rest !== undefined) {
376
+ body = rest;
377
+ }
378
+ }
379
+ // Validate MDX syntax (only if there's content)
380
+ if (body.trim()) {
381
+ validateMDX(body);
382
+ }
383
+ // Extract components
384
+ const components = extractComponents(body);
385
+ // Extract component props
386
+ const componentProps = extractComponentProps(body);
387
+ const result = {
388
+ content: mdx,
389
+ body,
390
+ frontmatter,
391
+ components,
392
+ componentProps,
393
+ };
394
+ // Cache the result with optional tags
395
+ if (!skipCache) {
396
+ // Auto-generate component tags if not provided
397
+ const cacheTags = tags ?? components.map((c) => `component:${c}`);
398
+ parseCache.set(contentHash, result, cacheTags);
399
+ }
400
+ return result;
401
+ }
402
+ /**
403
+ * Extract prop schemas from MDX component usage
404
+ *
405
+ * Analyzes component tags in MDX to infer prop schemas.
406
+ *
407
+ * @param mdx - MDX content string
408
+ * @returns Schemas for each component
409
+ *
410
+ * @example
411
+ * ```ts
412
+ * const schemas = extractComponentSchemas(`
413
+ * <Card title="Hello" count={5} />
414
+ * `)
415
+ *
416
+ * // schemas.Card = { title: 'title (string)', count: 'count (number)' }
417
+ * ```
418
+ */
419
+ export function extractComponentSchemas(mdx) {
420
+ const schemas = {};
421
+ // Match full component tags (including multi-line)
422
+ const tagRegex = /<([A-Z][a-zA-Z0-9]*)([\s\S]*?)(?:\/>|>)/g;
423
+ let match;
424
+ while ((match = tagRegex.exec(mdx)) !== null) {
425
+ const componentName = match[1];
426
+ const propsStr = match[2];
427
+ if (componentName === undefined || propsStr === undefined)
428
+ continue;
429
+ // Initialize schema for this component
430
+ if (!schemas[componentName]) {
431
+ schemas[componentName] = {};
432
+ }
433
+ // Extract prop names and infer types
434
+ const propRegex = /(\w+)(?:=(?:"([^"]*)"|{([^}]*)}))?/g;
435
+ let propMatch;
436
+ while ((propMatch = propRegex.exec(propsStr)) !== null) {
437
+ const propName = propMatch[1];
438
+ const stringValue = propMatch[2];
439
+ const exprValue = propMatch[3];
440
+ // Skip if prop name doesn't start with lowercase (likely a tag attribute)
441
+ if (!propName || !propName.match(/^[a-z]/))
442
+ continue;
443
+ // Add to schema with description based on value type
444
+ if (stringValue !== undefined) {
445
+ schemas[componentName][propName] = `${propName} (string)`;
446
+ }
447
+ else if (exprValue !== undefined) {
448
+ // Try to infer type from expression
449
+ if (exprValue === 'true' || exprValue === 'false') {
450
+ schemas[componentName][propName] = `${propName} (boolean)`;
451
+ }
452
+ else if (!isNaN(Number(exprValue))) {
453
+ schemas[componentName][propName] = `${propName} (number)`;
454
+ }
455
+ else if (exprValue.startsWith('{') || exprValue.startsWith('[')) {
456
+ schemas[componentName][propName] = `${propName} (object)`;
457
+ }
458
+ else {
459
+ schemas[componentName][propName] = `${propName}`;
460
+ }
461
+ }
462
+ }
463
+ }
464
+ return schemas;
465
+ }
466
+ // ============================================================================
467
+ // Props Generation
468
+ // ============================================================================
469
+ /**
470
+ * Create an MDX props generator
471
+ *
472
+ * The generator uses content-hash based caching and supports parallel
473
+ * generation for multiple components.
474
+ *
475
+ * @param options - Generator options
476
+ * @returns MDX props generator instance
477
+ *
478
+ * @example
479
+ * ```ts
480
+ * const generator = createMDXPropsGenerator({
481
+ * schemas: {
482
+ * Hero: { title: 'Hero title', subtitle: 'Hero subtitle' },
483
+ * },
484
+ * cache: true,
485
+ * maxParallel: 3,
486
+ * })
487
+ *
488
+ * const props = await generator.generate(`<Hero />`)
489
+ * // props.Hero = { title: '...', subtitle: '...' }
490
+ * ```
491
+ */
492
+ export function createMDXPropsGenerator(options) {
493
+ const { schemas, cache = false, model, maxParallel = 3 } = options;
494
+ const propsCache = cache ? getDefaultCache() : null;
495
+ /**
496
+ * Generate props for a single component
497
+ */
498
+ async function generateComponentProps(componentName, schema, explicitProps, frontmatter) {
499
+ // Build schema for missing props only
500
+ const missingPropsSchema = {};
501
+ for (const [key, value] of Object.entries(schema)) {
502
+ if (!(key in explicitProps)) {
503
+ missingPropsSchema[key] = value;
504
+ }
505
+ }
506
+ // If no missing props, return explicit props
507
+ if (Object.keys(missingPropsSchema).length === 0) {
508
+ return explicitProps;
509
+ }
510
+ // Check cache if enabled
511
+ if (propsCache) {
512
+ const cacheKey = createMDXCacheKey(componentName, missingPropsSchema, frontmatter);
513
+ const cached = propsCache.get(cacheKey);
514
+ if (cached) {
515
+ return { ...cached.props, ...explicitProps };
516
+ }
517
+ }
518
+ // Build context from frontmatter
519
+ const contextParts = [];
520
+ if (Object.keys(frontmatter).length > 0) {
521
+ contextParts.push('Page context:');
522
+ contextParts.push(JSON.stringify(frontmatter, null, 2));
523
+ }
524
+ contextParts.push(`Generate props for the ${componentName} component.`);
525
+ // Use full model ID to avoid alias resolution issues in bundled environments
526
+ const genResult = await generateObject({
527
+ model: model || 'anthropic/claude-sonnet-4.5',
528
+ schema: missingPropsSchema,
529
+ prompt: contextParts.join('\n'),
530
+ });
531
+ const generatedProps = genResult.object;
532
+ // Cache if enabled
533
+ if (propsCache) {
534
+ const cacheKey = createMDXCacheKey(componentName, missingPropsSchema, frontmatter);
535
+ propsCache.set(cacheKey, generatedProps);
536
+ }
537
+ return { ...generatedProps, ...explicitProps };
538
+ }
539
+ return {
540
+ async generate(mdx) {
541
+ const parsed = parseMDX(mdx);
542
+ const result = {};
543
+ // Get components that have schemas
544
+ const componentsToGenerate = parsed.components.filter((c) => schemas[c]);
545
+ if (componentsToGenerate.length === 0) {
546
+ return result;
547
+ }
548
+ // Generate props in parallel batches
549
+ const batches = [];
550
+ for (let i = 0; i < componentsToGenerate.length; i += maxParallel) {
551
+ batches.push(componentsToGenerate.slice(i, i + maxParallel));
552
+ }
553
+ for (const batch of batches) {
554
+ const promises = batch.map(async (componentName) => {
555
+ const schema = schemas[componentName];
556
+ if (!schema)
557
+ return { componentName, props: {} };
558
+ const explicitProps = parsed.componentProps[componentName] || {};
559
+ const props = await generateComponentProps(componentName, schema, explicitProps, parsed.frontmatter);
560
+ return { componentName, props };
561
+ });
562
+ const batchResults = await Promise.all(promises);
563
+ for (const { componentName, props } of batchResults) {
564
+ result[componentName] = props;
565
+ }
566
+ }
567
+ return result;
568
+ },
569
+ clearCache() {
570
+ if (propsCache) {
571
+ propsCache.clear();
572
+ }
573
+ },
574
+ };
575
+ }
576
+ // ============================================================================
577
+ // Rendering Functions
578
+ // ============================================================================
579
+ /**
580
+ * Render MDX with injected props
581
+ *
582
+ * @param mdx - MDX content string
583
+ * @param props - Props for each component
584
+ * @param options - Render options
585
+ * @returns Rendered content (string or stream)
586
+ *
587
+ * @example
588
+ * ```ts
589
+ * const html = await renderMDXWithProps(
590
+ * `<Hero title="Welcome" />`,
591
+ * { Hero: { title: 'Welcome', subtitle: 'To the site' } }
592
+ * )
593
+ * ```
594
+ */
595
+ export async function renderMDXWithProps(mdx, props, options = {}) {
596
+ // Validate props
597
+ for (const [componentName, componentProps] of Object.entries(props)) {
598
+ if (componentProps === null) {
599
+ throw createParseError(`Invalid props for component ${componentName}: props cannot be null`);
600
+ }
601
+ }
602
+ const { components = {}, stream = false } = options;
603
+ const parsed = parseMDX(mdx);
604
+ // Build component prop map - filter out nulls
605
+ const componentPropsMap = {};
606
+ for (const [name, propsValue] of Object.entries(props)) {
607
+ if (propsValue !== null) {
608
+ componentPropsMap[name] = propsValue;
609
+ }
610
+ }
611
+ // Merge with props extracted from MDX (MDX props take precedence)
612
+ for (const [name, mdxProps] of Object.entries(parsed.componentProps)) {
613
+ componentPropsMap[name] = {
614
+ ...componentPropsMap[name],
615
+ ...mdxProps,
616
+ };
617
+ }
618
+ // Simple renderer that replaces components with their rendered output
619
+ let output = parsed.body;
620
+ // Replace frontmatter variables: {varName}
621
+ for (const [key, value] of Object.entries(parsed.frontmatter)) {
622
+ const regex = new RegExp(`\\{${key}\\}`, 'g');
623
+ output = output.replace(regex, String(value));
624
+ }
625
+ // Render components
626
+ for (const componentName of parsed.components) {
627
+ const componentProps = componentPropsMap[componentName] || {};
628
+ const renderer = components[componentName];
629
+ // Match component tags
630
+ const selfCloseRegex = new RegExp(`<${componentName}([^>]*)\\/>`, 'g');
631
+ const fullTagRegex = new RegExp(`<${componentName}([^>]*)>([\\s\\S]*?)<\\/${componentName}>`, 'g');
632
+ if (renderer) {
633
+ // Use custom renderer
634
+ output = output.replace(selfCloseRegex, () => renderer(componentProps));
635
+ output = output.replace(fullTagRegex, (_, __, children) => {
636
+ return renderer({ ...componentProps, children });
637
+ });
638
+ }
639
+ else {
640
+ // Default: inject props into the tag
641
+ const propsStr = serializeProps(componentProps);
642
+ // For self-closing tags, inject props
643
+ output = output.replace(selfCloseRegex, () => {
644
+ return `<${componentName} ${propsStr} />`;
645
+ });
646
+ }
647
+ }
648
+ if (stream) {
649
+ // Return as a ReadableStream
650
+ return new ReadableStream({
651
+ start(controller) {
652
+ // Split output into chunks and enqueue
653
+ const chunks = output.split('\n');
654
+ for (const chunk of chunks) {
655
+ controller.enqueue(chunk + '\n');
656
+ }
657
+ controller.close();
658
+ },
659
+ });
660
+ }
661
+ return output;
662
+ }
663
+ /**
664
+ * Stream MDX content with injected props
665
+ *
666
+ * Returns a ReadableStream for progressive rendering of MDX content.
667
+ *
668
+ * @param mdx - MDX content string
669
+ * @param props - Props for each component
670
+ * @param options - Stream options
671
+ * @returns ReadableStream of rendered content
672
+ *
673
+ * @example
674
+ * ```ts
675
+ * const stream = await streamMDXWithProps(
676
+ * `<Hero title="Welcome" />`,
677
+ * { Hero: { title: 'Welcome', subtitle: 'To the site' } }
678
+ * )
679
+ *
680
+ * const reader = stream.getReader()
681
+ * while (true) {
682
+ * const { done, value } = await reader.read()
683
+ * if (done) break
684
+ * console.log(new TextDecoder().decode(value))
685
+ * }
686
+ * ```
687
+ */
688
+ export async function streamMDXWithProps(mdx, props, options = {}) {
689
+ // Use renderMDXWithProps with stream option and convert to Uint8Array stream
690
+ const result = await renderMDXWithProps(mdx, props, { ...options, stream: true });
691
+ const textEncoder = new TextEncoder();
692
+ if (result instanceof ReadableStream) {
693
+ // Convert string stream to Uint8Array stream
694
+ const stringReader = result.getReader();
695
+ return new ReadableStream({
696
+ async pull(controller) {
697
+ const { done, value } = await stringReader.read();
698
+ if (done) {
699
+ controller.close();
700
+ return;
701
+ }
702
+ controller.enqueue(textEncoder.encode(value));
703
+ },
704
+ });
705
+ }
706
+ // Fallback: wrap string result in a stream
707
+ return new ReadableStream({
708
+ start(controller) {
709
+ controller.enqueue(textEncoder.encode(result));
710
+ controller.close();
711
+ },
712
+ });
713
+ }
714
+ // ============================================================================
715
+ // Compilation
716
+ // ============================================================================
717
+ /**
718
+ * Compile MDX to an executable function
719
+ *
720
+ * Compilation is lazy - the MDX is parsed once, and the returned function
721
+ * can be called multiple times with different props.
722
+ *
723
+ * @param mdx - MDX content string
724
+ * @param options - Compile options
725
+ * @returns Compiled function that accepts props
726
+ *
727
+ * @example
728
+ * ```ts
729
+ * const compiled = await compileMDX(`<Greeting name="World" />`)
730
+ * const result = compiled({ Greeting: { name: 'World' } })
731
+ * ```
732
+ */
733
+ export async function compileMDX(mdx, options = {}) {
734
+ const { components = {} } = options;
735
+ // Check for runtime errors in JSX expressions
736
+ if (mdx.includes('throw new Error') || mdx.includes('throw Error')) {
737
+ throw createParseError('Runtime error in MDX expression');
738
+ }
739
+ // Extract export statements
740
+ let metadata;
741
+ const exportMatch = mdx.match(/export\s+const\s+(\w+)\s*=\s*({[\s\S]*?})/m);
742
+ if (exportMatch) {
743
+ const [, name, value] = exportMatch;
744
+ try {
745
+ // Safe parse of simple object literals
746
+ // eslint-disable-next-line no-new-func
747
+ const parsed = new Function(`return ${value}`)();
748
+ if (name === 'metadata') {
749
+ metadata = parsed;
750
+ }
751
+ }
752
+ catch {
753
+ // Ignore parse errors for complex exports
754
+ }
755
+ }
756
+ // Remove import/export statements for processing
757
+ const cleanMdx = mdx
758
+ .replace(/^import\s+.*$/gm, '')
759
+ .replace(/^export\s+.*$/gm, '')
760
+ .trim();
761
+ // Parse once for validation (this also caches the result)
762
+ parseMDX(cleanMdx);
763
+ // Create the compiled function
764
+ const compiled = (props) => {
765
+ // Parse is cached, so this is fast
766
+ const parsed = parseMDX(cleanMdx);
767
+ let output = parsed.body;
768
+ // Replace frontmatter variables
769
+ for (const [key, value] of Object.entries(parsed.frontmatter)) {
770
+ const regex = new RegExp(`\\{${key}\\}`, 'g');
771
+ output = output.replace(regex, String(value));
772
+ }
773
+ // Render components
774
+ for (const componentName of parsed.components) {
775
+ const componentProps = props[componentName] || parsed.componentProps[componentName] || {};
776
+ const renderer = components[componentName];
777
+ const selfCloseRegex = new RegExp(`<${componentName}([^>]*)\\/>`, 'g');
778
+ const fullTagRegex = new RegExp(`<${componentName}([^>]*)>([\\s\\S]*?)<\\/${componentName}>`, 'g');
779
+ if (renderer) {
780
+ output = output.replace(selfCloseRegex, () => renderer(componentProps));
781
+ output = output.replace(fullTagRegex, (_, __, children) => {
782
+ return renderer({ ...componentProps, children });
783
+ });
784
+ }
785
+ else {
786
+ // Default: inject props
787
+ const propsStr = serializeProps(componentProps);
788
+ output = output.replace(selfCloseRegex, () => {
789
+ return `<${componentName} ${propsStr} />`;
790
+ });
791
+ }
792
+ }
793
+ return output;
794
+ };
795
+ // Attach metadata if found
796
+ if (metadata) {
797
+ compiled.metadata = metadata;
798
+ }
799
+ return compiled;
800
+ }
801
+ // ============================================================================
802
+ // Cache Management
803
+ // ============================================================================
804
+ /**
805
+ * Clear the MDX parse cache
806
+ *
807
+ * Use this when you need to force re-parsing of all MDX content.
808
+ */
809
+ export function clearMDXCache() {
810
+ parseCache.clear();
811
+ }
812
+ /**
813
+ * Get the current MDX parse cache size
814
+ *
815
+ * @returns Number of cached parse results
816
+ */
817
+ export function getMDXCacheSize() {
818
+ return parseCache.size;
819
+ }
820
+ //# sourceMappingURL=mdx.js.map