ai-props 2.1.1 → 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 +24 -0
- package/README.md +131 -118
- package/package.json +30 -4
- 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 -5
- 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-types.ts
ADDED
|
@@ -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
|
+
}
|
package/src/mdx-utils.ts
ADDED
|
@@ -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
|
+
}
|