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.
Files changed (73) hide show
  1. package/.dev.vars +2 -0
  2. package/CHANGELOG.md +11 -0
  3. package/README.md +2 -0
  4. package/package.json +39 -13
  5. package/src/ai.ts +12 -31
  6. package/src/cascade.ts +795 -0
  7. package/src/client.ts +440 -0
  8. package/src/durable-cascade.ts +743 -0
  9. package/src/event-bridge.ts +478 -0
  10. package/src/generate.ts +14 -12
  11. package/src/hoc.ts +15 -19
  12. package/src/hono-jsx.ts +675 -0
  13. package/src/index.ts +30 -0
  14. package/src/mdx-types.ts +169 -0
  15. package/src/mdx-utils.ts +437 -0
  16. package/src/mdx.ts +1008 -0
  17. package/src/rpc.ts +614 -0
  18. package/src/streaming.ts +618 -0
  19. package/src/validate.ts +15 -29
  20. package/src/worker.ts +547 -0
  21. package/test/cascade.test.ts +338 -0
  22. package/test/durable-cascade.test.ts +319 -0
  23. package/test/event-bridge.test.ts +351 -0
  24. package/test/generate.test.ts +6 -16
  25. package/test/mdx.test.ts +817 -0
  26. package/test/worker/capnweb-rpc.test.ts +1084 -0
  27. package/test/worker/full-flow.integration.test.ts +1463 -0
  28. package/test/worker/hono-jsx.test.ts +1258 -0
  29. package/test/worker/mdx-parsing.test.ts +1148 -0
  30. package/test/worker/setup.ts +56 -0
  31. package/test/worker.test.ts +595 -0
  32. package/tsconfig.json +2 -1
  33. package/vitest.config.js +6 -0
  34. package/vitest.config.ts +15 -1
  35. package/vitest.workers.config.ts +58 -0
  36. package/wrangler.jsonc +27 -0
  37. package/.turbo/turbo-build.log +0 -4
  38. package/LICENSE +0 -21
  39. package/dist/ai.d.ts +0 -125
  40. package/dist/ai.d.ts.map +0 -1
  41. package/dist/ai.js +0 -199
  42. package/dist/ai.js.map +0 -1
  43. package/dist/cache.d.ts +0 -66
  44. package/dist/cache.d.ts.map +0 -1
  45. package/dist/cache.js +0 -183
  46. package/dist/cache.js.map +0 -1
  47. package/dist/generate.d.ts +0 -69
  48. package/dist/generate.d.ts.map +0 -1
  49. package/dist/generate.js +0 -221
  50. package/dist/generate.js.map +0 -1
  51. package/dist/hoc.d.ts +0 -164
  52. package/dist/hoc.d.ts.map +0 -1
  53. package/dist/hoc.js +0 -236
  54. package/dist/hoc.js.map +0 -1
  55. package/dist/index.d.ts +0 -15
  56. package/dist/index.d.ts.map +0 -1
  57. package/dist/index.js +0 -21
  58. package/dist/index.js.map +0 -1
  59. package/dist/types.d.ts +0 -152
  60. package/dist/types.d.ts.map +0 -1
  61. package/dist/types.js +0 -7
  62. package/dist/types.js.map +0 -1
  63. package/dist/validate.d.ts +0 -58
  64. package/dist/validate.d.ts.map +0 -1
  65. package/dist/validate.js +0 -253
  66. package/dist/validate.js.map +0 -1
  67. package/src/ai.js +0 -198
  68. package/src/cache.js +0 -182
  69. package/src/generate.js +0 -220
  70. package/src/hoc.js +0 -235
  71. package/src/index.js +0 -20
  72. package/src/types.js +0 -6
  73. package/src/validate.js +0 -252
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Type definitions for MDX integration
3
+ *
4
+ * Shared types used across MDX parsing, props generation, and rendering.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+
9
+ /**
10
+ * Result of parsing MDX content
11
+ */
12
+ export interface ParsedMDX {
13
+ /** Original MDX content */
14
+ content: string
15
+ /** Body content without frontmatter */
16
+ body: string
17
+ /** Parsed frontmatter data */
18
+ frontmatter: Record<string, unknown>
19
+ /** List of component names found in MDX */
20
+ components: string[]
21
+ /** Props extracted from components */
22
+ componentProps: Record<string, Record<string, unknown>>
23
+ }
24
+
25
+ /**
26
+ * Component schema definitions
27
+ * Each component maps to an object schema (key -> description string)
28
+ */
29
+ export type ComponentSchemas = Record<string, Record<string, string>>
30
+
31
+ /**
32
+ * Options for creating an MDX props generator
33
+ */
34
+ export interface MDXPropsGeneratorOptions {
35
+ /** Schemas for components */
36
+ schemas: ComponentSchemas
37
+ /** Whether to cache generated props */
38
+ cache?: boolean
39
+ /** Model to use for generation */
40
+ model?: string
41
+ /** Maximum parallel generation requests (default: 3) */
42
+ maxParallel?: number
43
+ }
44
+
45
+ /**
46
+ * MDX props generator instance
47
+ */
48
+ export interface MDXPropsGenerator {
49
+ /** Generate props for components in MDX */
50
+ generate: (mdx: string) => Promise<Record<string, Record<string, unknown>>>
51
+ /** Clear the generator's cache */
52
+ clearCache?: () => void
53
+ }
54
+
55
+ /**
56
+ * Options for rendering MDX with props
57
+ */
58
+ export interface RenderMDXOptions {
59
+ /** Custom component renderers */
60
+ components?: Record<string, (props: Record<string, unknown>) => string>
61
+ /** Enable streaming render */
62
+ stream?: boolean
63
+ }
64
+
65
+ /**
66
+ * Options for compiling MDX
67
+ */
68
+ export interface CompileMDXOptions {
69
+ /** Custom component map */
70
+ components?: Record<string, (props: Record<string, unknown>) => string>
71
+ }
72
+
73
+ /**
74
+ * Compiled MDX function type
75
+ */
76
+ export interface CompiledMDXFunction {
77
+ (props: Record<string, Record<string, unknown>>): string
78
+ /** Exported metadata from MDX */
79
+ metadata?: Record<string, unknown>
80
+ }
81
+
82
+ /**
83
+ * Options for streaming MDX rendering
84
+ */
85
+ export interface StreamMDXOptions {
86
+ /** Custom component renderers */
87
+ components?: Record<string, (props: Record<string, unknown>) => string>
88
+ }
89
+
90
+ /**
91
+ * Cache entry for parsed MDX
92
+ */
93
+ export interface MDXCacheEntry {
94
+ /** Content hash */
95
+ hash: string
96
+ /** Parsed MDX result */
97
+ parsed: ParsedMDX
98
+ /** Timestamp of cache entry */
99
+ timestamp: number
100
+ }
101
+
102
+ /**
103
+ * Cache entry for generated props
104
+ */
105
+ export interface PropsCacheEntry {
106
+ /** Cache key */
107
+ key: string
108
+ /** Generated props */
109
+ props: Record<string, unknown>
110
+ /** Timestamp of cache entry */
111
+ timestamp: number
112
+ /** Content hash used for generation */
113
+ contentHash?: string
114
+ /** Tags for invalidation */
115
+ tags?: string[]
116
+ }
117
+
118
+ /**
119
+ * MDX parse error with location information
120
+ */
121
+ export interface MDXParseError extends Error {
122
+ /** Line number where error occurred (1-indexed) */
123
+ line?: number
124
+ /** Column number where error occurred (1-indexed) */
125
+ column?: number
126
+ /** The problematic content */
127
+ source?: string
128
+ }
129
+
130
+ /**
131
+ * Cache invalidation strategy type
132
+ */
133
+ export type CacheInvalidationStrategy =
134
+ | 'ttl' // Time-based invalidation
135
+ | 'lru' // Least recently used
136
+ | 'tag' // Tag-based invalidation
137
+ | 'manual' // Manual invalidation only
138
+
139
+ /**
140
+ * Options for MDX cache configuration
141
+ */
142
+ export interface MDXCacheOptions {
143
+ /** Maximum cache size (default: 100) */
144
+ maxSize?: number
145
+ /** Time-to-live in milliseconds (default: 5 minutes) */
146
+ ttl?: number
147
+ /** Invalidation strategy (default: 'lru') */
148
+ strategy?: CacheInvalidationStrategy
149
+ /** Enable automatic cleanup interval */
150
+ autoCleanup?: boolean
151
+ /** Cleanup interval in milliseconds (default: 60 seconds) */
152
+ cleanupInterval?: number
153
+ }
154
+
155
+ /**
156
+ * MDX cache statistics
157
+ */
158
+ export interface MDXCacheStats {
159
+ /** Number of cache hits */
160
+ hits: number
161
+ /** Number of cache misses */
162
+ misses: number
163
+ /** Current cache size */
164
+ size: number
165
+ /** Maximum cache size */
166
+ maxSize: number
167
+ /** Cache hit ratio */
168
+ hitRatio: number
169
+ }
@@ -0,0 +1,437 @@
1
+ /**
2
+ * MDX utility functions
3
+ *
4
+ * Internal utilities for MDX parsing, component extraction, and content hashing.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+
9
+ import type { ParsedMDX, MDXParseError } from './mdx-types.js'
10
+
11
+ /**
12
+ * Default cache TTL for MDX parsing (5 minutes)
13
+ */
14
+ export const MDX_CACHE_TTL = 5 * 60 * 1000
15
+
16
+ /**
17
+ * Create a content hash for cache keys
18
+ *
19
+ * Uses a simple but fast hash algorithm suitable for cache keys.
20
+ *
21
+ * @param content - Content to hash
22
+ * @returns Hash string
23
+ */
24
+ export function hashContent(content: string): string {
25
+ let hash = 0
26
+ for (let i = 0; i < content.length; i++) {
27
+ const char = content.charCodeAt(i)
28
+ hash = (hash << 5) - hash + char
29
+ hash = hash & hash // Convert to 32-bit integer
30
+ }
31
+ return hash.toString(36)
32
+ }
33
+
34
+ /**
35
+ * Create an MDX parse error with location information
36
+ *
37
+ * @param message - Error message
38
+ * @param source - Source content where error occurred
39
+ * @param position - Position in source (optional)
40
+ * @returns MDXParseError
41
+ */
42
+ export function createParseError(
43
+ message: string,
44
+ source?: string,
45
+ position?: number
46
+ ): MDXParseError {
47
+ const error = new Error(message) as MDXParseError
48
+ if (source !== undefined) {
49
+ error.source = source
50
+ }
51
+
52
+ if (source && position !== undefined) {
53
+ const lines = source.slice(0, position).split('\n')
54
+ error.line = lines.length
55
+ error.column = (lines[lines.length - 1]?.length || 0) + 1
56
+ }
57
+
58
+ return error
59
+ }
60
+
61
+ /**
62
+ * Parse YAML frontmatter
63
+ *
64
+ * Handles basic YAML types: strings, numbers, booleans, arrays, and nested objects.
65
+ *
66
+ * @param yaml - YAML content string
67
+ * @returns Parsed key-value pairs
68
+ * @throws Error if YAML is invalid
69
+ */
70
+ export function parseYAML(yaml: string): Record<string, unknown> {
71
+ const result: Record<string, unknown> = {}
72
+ const lines = yaml.trim().split('\n')
73
+
74
+ // Stack for tracking nested objects
75
+ // Each entry stores: the parent object, the parent indent level, and the current object's indent
76
+ const stack: Array<{
77
+ parentObj: Record<string, unknown>
78
+ parentIndent: number
79
+ thisIndent: number
80
+ }> = []
81
+ let currentObj: Record<string, unknown> = result
82
+ let currentIndent = -1 // Use -1 for root level
83
+ let currentArray: unknown[] | null = null
84
+
85
+ for (const line of lines) {
86
+ // Skip empty lines
87
+ if (!line.trim()) continue
88
+
89
+ // Calculate indentation
90
+ const indentMatch = line.match(/^(\s*)/)
91
+ const indent = indentMatch && indentMatch[1] !== undefined ? indentMatch[1].length : 0
92
+
93
+ // Pop stack if we've dedented (back to a parent level or sibling)
94
+ while (stack.length > 0 && indent <= stack[stack.length - 1]!.thisIndent) {
95
+ const popped = stack.pop()!
96
+ currentObj = popped.parentObj
97
+ currentIndent = popped.parentIndent
98
+ currentArray = null
99
+ }
100
+
101
+ // Check for array item
102
+ const arrayMatch = line.match(/^(\s*)-\s*(.*)$/)
103
+ if (arrayMatch && arrayMatch[2] !== undefined && currentArray !== null) {
104
+ const value = parseYAMLValue(arrayMatch[2].trim())
105
+ currentArray.push(value)
106
+ continue
107
+ }
108
+
109
+ // Check for key-value pair (supporting special chars like $)
110
+ const keyMatch = line.match(/^(\s*)([\w$]+):\s*(.*)$/)
111
+ if (keyMatch && keyMatch[2] !== undefined && keyMatch[3] !== undefined) {
112
+ const key = keyMatch[2]
113
+ const value = keyMatch[3].trim()
114
+
115
+ // If the value is empty, this might be a nested object or array
116
+ if (value === '') {
117
+ // Check if this is an array or nested object by looking at the next line
118
+ const lineIndex = lines.indexOf(line)
119
+ const nextLine = lines[lineIndex + 1]
120
+
121
+ if (nextLine && nextLine.trim().startsWith('-')) {
122
+ // It's an array
123
+ currentArray = []
124
+ currentObj[key] = currentArray
125
+ } else {
126
+ // It's a nested object
127
+ const nestedObj: Record<string, unknown> = {}
128
+ currentObj[key] = nestedObj
129
+ stack.push({
130
+ parentObj: currentObj,
131
+ parentIndent: currentIndent,
132
+ thisIndent: indent,
133
+ })
134
+ currentObj = nestedObj
135
+ currentIndent = indent
136
+ currentArray = null
137
+ }
138
+ } else {
139
+ currentObj[key] = parseYAMLValue(value)
140
+ currentArray = null
141
+ }
142
+ }
143
+ }
144
+
145
+ return result
146
+ }
147
+
148
+ /**
149
+ * Parse a YAML value to its appropriate type
150
+ *
151
+ * @param value - YAML value string
152
+ * @returns Parsed value
153
+ * @throws Error if value is malformed
154
+ */
155
+ export function parseYAMLValue(value: string): unknown {
156
+ // Boolean
157
+ if (value === 'true') return true
158
+ if (value === 'false') return false
159
+
160
+ // Number
161
+ const num = Number(value)
162
+ if (!isNaN(num) && value !== '') return num
163
+
164
+ // String (remove quotes if present)
165
+ if (
166
+ (value.startsWith('"') && value.endsWith('"')) ||
167
+ (value.startsWith("'") && value.endsWith("'"))
168
+ ) {
169
+ return value.slice(1, -1)
170
+ }
171
+
172
+ // Check for invalid YAML (incomplete objects/arrays)
173
+ if (value.startsWith('[') && !value.endsWith(']')) {
174
+ throw createParseError('Invalid YAML: unclosed array')
175
+ }
176
+ if (value.startsWith('{') && !value.endsWith('}')) {
177
+ throw createParseError('Invalid YAML: unclosed object')
178
+ }
179
+
180
+ return value
181
+ }
182
+
183
+ /**
184
+ * Extract component names from MDX content
185
+ *
186
+ * Finds all PascalCase JSX component tags.
187
+ *
188
+ * @param content - MDX body content
189
+ * @returns Array of unique component names
190
+ */
191
+ export function extractComponents(content: string): string[] {
192
+ const components = new Set<string>()
193
+
194
+ // Match JSX component tags (PascalCase)
195
+ // Self-closing: <Component />
196
+ // Opening: <Component>
197
+ const tagRegex = /<([A-Z][a-zA-Z0-9]*)(?:\s|>|\/)/g
198
+ let match
199
+
200
+ while ((match = tagRegex.exec(content)) !== null) {
201
+ if (match[1]) {
202
+ components.add(match[1])
203
+ }
204
+ }
205
+
206
+ return Array.from(components)
207
+ }
208
+
209
+ /**
210
+ * Extract props from a component tag string
211
+ *
212
+ * Parses string props, expression props, and boolean props.
213
+ *
214
+ * @param tag - Component tag content (attributes portion)
215
+ * @returns Props object
216
+ */
217
+ export function extractPropsFromTag(tag: string): Record<string, unknown> {
218
+ const props: Record<string, unknown> = {}
219
+
220
+ // Extract string props: name="value"
221
+ const stringPropRegex = /(\w+)="([^"]*)"/g
222
+ let match
223
+
224
+ while ((match = stringPropRegex.exec(tag)) !== null) {
225
+ const name = match[1]
226
+ const value = match[2]
227
+ if (name !== undefined && value !== undefined) {
228
+ props[name] = value
229
+ }
230
+ }
231
+
232
+ // Extract expression props: name={expression}
233
+ const exprPropRegex = /(\w+)=\{([^}]*)\}/g
234
+ while ((match = exprPropRegex.exec(tag)) !== null) {
235
+ const name = match[1]
236
+ const exprValue = match[2]
237
+ if (name !== undefined && exprValue !== undefined) {
238
+ try {
239
+ // Try to parse as JSON
240
+ props[name] = JSON.parse(exprValue)
241
+ } catch {
242
+ // Try to evaluate simple expressions
243
+ if (exprValue === 'true') {
244
+ props[name] = true
245
+ } else if (exprValue === 'false') {
246
+ props[name] = false
247
+ } else if (!isNaN(Number(exprValue))) {
248
+ props[name] = Number(exprValue)
249
+ } else {
250
+ // Keep as expression string
251
+ props[name] = exprValue
252
+ }
253
+ }
254
+ }
255
+ }
256
+
257
+ // Extract boolean props (word not followed by =)
258
+ const booleanPropRegex = /\s([a-z][a-zA-Z0-9]*)(?=\s|>|\/|$)/g
259
+ while ((match = booleanPropRegex.exec(tag)) !== null) {
260
+ const name = match[1]
261
+ // Only add if not already defined
262
+ if (name !== undefined && !(name in props)) {
263
+ props[name] = true
264
+ }
265
+ }
266
+
267
+ return props
268
+ }
269
+
270
+ /**
271
+ * Extract component props from all components in MDX content
272
+ *
273
+ * @param content - MDX body content
274
+ * @returns Map of component names to their props
275
+ */
276
+ export function extractComponentProps(content: string): Record<string, Record<string, unknown>> {
277
+ const componentProps: Record<string, Record<string, unknown>> = {}
278
+
279
+ // Match full component tags (including multi-line)
280
+ const tagRegex = /<([A-Z][a-zA-Z0-9]*)([\s\S]*?)(?:\/>|>)/g
281
+ let match
282
+
283
+ while ((match = tagRegex.exec(content)) !== null) {
284
+ const componentName = match[1]
285
+ const propsStr = match[2]
286
+ if (componentName === undefined || propsStr === undefined) continue
287
+
288
+ const props = extractPropsFromTag(propsStr)
289
+
290
+ if (Object.keys(props).length > 0) {
291
+ // Merge with existing props for this component
292
+ componentProps[componentName] = {
293
+ ...componentProps[componentName],
294
+ ...props,
295
+ }
296
+ }
297
+ }
298
+
299
+ return componentProps
300
+ }
301
+
302
+ /**
303
+ * Validate MDX syntax
304
+ *
305
+ * Checks for unclosed tags and invalid prop syntax.
306
+ *
307
+ * @param content - MDX body content
308
+ * @throws MDXParseError if syntax is invalid
309
+ */
310
+ export function validateMDX(content: string): void {
311
+ // Check for incomplete tags at the end of content
312
+ const tagStarts: Array<{ name: string; index: number }> = []
313
+ const tagStartRegex = /<([A-Z][a-zA-Z0-9]*)/g
314
+ let match
315
+
316
+ while ((match = tagStartRegex.exec(content)) !== null) {
317
+ const name = match[1]
318
+ if (name !== undefined) {
319
+ tagStarts.push({ name, index: match.index })
320
+ }
321
+ }
322
+
323
+ // For each tag start, check if there's a closing >
324
+ for (let i = 0; i < tagStarts.length; i++) {
325
+ const start = tagStarts[i]
326
+ if (!start) continue
327
+ const endBound = tagStarts[i + 1]?.index ?? content.length
328
+ const tagContent = content.slice(start.index, endBound)
329
+
330
+ if (!tagContent.includes('>')) {
331
+ throw createParseError(
332
+ `Invalid MDX syntax: incomplete tag <${start.name}`,
333
+ content,
334
+ start.index
335
+ )
336
+ }
337
+ }
338
+
339
+ // Check for invalid prop syntax: prop=>
340
+ const invalidPropMatch = content.match(/\w+=\s*>/)
341
+ if (invalidPropMatch) {
342
+ const position = content.indexOf(invalidPropMatch[0])
343
+ throw createParseError('Invalid MDX syntax: incomplete prop value', content, position)
344
+ }
345
+
346
+ // Validate matching open/close tags
347
+ const allTagsRegex = /<\/?([A-Z][a-zA-Z0-9]*)[\s\S]*?>/g
348
+ const tagMatches: Array<{ name: string; type: 'open' | 'close' | 'selfClose'; full: string }> = []
349
+
350
+ while ((match = allTagsRegex.exec(content)) !== null) {
351
+ const full = match[0]
352
+ const name = match[1]
353
+ if (name === undefined) continue
354
+
355
+ if (full.startsWith('</')) {
356
+ tagMatches.push({ name, type: 'close', full })
357
+ } else if (full.trimEnd().endsWith('/>')) {
358
+ tagMatches.push({ name, type: 'selfClose', full })
359
+ } else {
360
+ tagMatches.push({ name, type: 'open', full })
361
+ }
362
+ }
363
+
364
+ // Count opens and closes per tag name
365
+ const openCount: Record<string, number> = {}
366
+ const closeCount: Record<string, number> = {}
367
+
368
+ for (const tag of tagMatches) {
369
+ if (tag.type === 'open') {
370
+ openCount[tag.name] = (openCount[tag.name] || 0) + 1
371
+ } else if (tag.type === 'close') {
372
+ closeCount[tag.name] = (closeCount[tag.name] || 0) + 1
373
+ }
374
+ }
375
+
376
+ // Each open tag should have a matching close tag
377
+ for (const name of Object.keys(openCount)) {
378
+ const opens = openCount[name] || 0
379
+ const closes = closeCount[name] || 0
380
+ if (opens > closes) {
381
+ throw createParseError(`Invalid MDX syntax: unclosed <${name}> tag`, content)
382
+ }
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Serialize props to JSX attribute string
388
+ *
389
+ * @param props - Props object
390
+ * @returns JSX attribute string
391
+ */
392
+ export function serializeProps(props: Record<string, unknown>): string {
393
+ return Object.entries(props)
394
+ .map(([k, v]) => {
395
+ if (typeof v === 'string') {
396
+ return `${k}="${v}"`
397
+ }
398
+ return `${k}={${JSON.stringify(v)}}`
399
+ })
400
+ .join(' ')
401
+ }
402
+
403
+ /**
404
+ * Sort object keys for consistent hashing
405
+ *
406
+ * @param obj - Object to sort
407
+ * @returns Object with sorted keys
408
+ */
409
+ export function sortObject(obj: Record<string, unknown>): Record<string, unknown> {
410
+ const sorted: Record<string, unknown> = {}
411
+ for (const key of Object.keys(obj).sort()) {
412
+ const value = obj[key]
413
+ sorted[key] =
414
+ value && typeof value === 'object' && !Array.isArray(value)
415
+ ? sortObject(value as Record<string, unknown>)
416
+ : value
417
+ }
418
+ return sorted
419
+ }
420
+
421
+ /**
422
+ * Create a cache key for MDX props generation
423
+ *
424
+ * @param componentName - Component name
425
+ * @param schema - Component schema
426
+ * @param context - Frontmatter context
427
+ * @returns Cache key string
428
+ */
429
+ export function createMDXCacheKey(
430
+ componentName: string,
431
+ schema: Record<string, string>,
432
+ context?: Record<string, unknown>
433
+ ): string {
434
+ const schemaHash = hashContent(JSON.stringify(sortObject(schema)))
435
+ const contextHash = context ? hashContent(JSON.stringify(sortObject(context))) : ''
436
+ return `mdx:${componentName}:${schemaHash}:${contextHash}`
437
+ }