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