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/hono-jsx.ts
ADDED
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hono/jsx integration for AI-powered props with hydration and streaming
|
|
3
|
+
*
|
|
4
|
+
* This module provides:
|
|
5
|
+
* - Hydration data collection during server render
|
|
6
|
+
* - Streaming render support with hono/jsx
|
|
7
|
+
* - AI-powered component prop generation
|
|
8
|
+
* - Context-aware rendering with Suspense support
|
|
9
|
+
*
|
|
10
|
+
* Bead: aip-fxpy (tests), aip-z57t (implementation)
|
|
11
|
+
*
|
|
12
|
+
* @packageDocumentation
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { generateProps, mergeWithGenerated } from './generate.js'
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Types
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Hydration data structure collected during render
|
|
23
|
+
*/
|
|
24
|
+
export interface HydrationData {
|
|
25
|
+
/** Map of component ID to props used during render */
|
|
26
|
+
components: Map<string, Record<string, unknown>>
|
|
27
|
+
/** Component hierarchy tree */
|
|
28
|
+
tree: HydrationNode[]
|
|
29
|
+
/** Serialize to JSON string */
|
|
30
|
+
toJSON(): string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Node in the component hydration tree
|
|
35
|
+
*/
|
|
36
|
+
export interface HydrationNode {
|
|
37
|
+
id: string
|
|
38
|
+
component: string
|
|
39
|
+
props: Record<string, unknown>
|
|
40
|
+
children: HydrationNode[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Context for hydration tracking
|
|
45
|
+
*/
|
|
46
|
+
export interface HydrationContext {
|
|
47
|
+
/** Register a component render with props */
|
|
48
|
+
register(componentName: string, props: Record<string, unknown>): string
|
|
49
|
+
/** Get collected hydration data */
|
|
50
|
+
getData(): HydrationData
|
|
51
|
+
/** Clear collected data */
|
|
52
|
+
clear(): void
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Options for streaming render
|
|
57
|
+
*/
|
|
58
|
+
export interface StreamingOptions {
|
|
59
|
+
/** Include hydration data in stream */
|
|
60
|
+
includeHydration?: boolean
|
|
61
|
+
/** Suspense configuration */
|
|
62
|
+
suspense?: {
|
|
63
|
+
fallback: string
|
|
64
|
+
}
|
|
65
|
+
/** Error handler */
|
|
66
|
+
onError?: (error: Error) => string
|
|
67
|
+
/** Enable progressive enhancement */
|
|
68
|
+
progressive?: boolean
|
|
69
|
+
/** Custom headers for response */
|
|
70
|
+
headers?: Record<string, string>
|
|
71
|
+
/** Enable streaming mode */
|
|
72
|
+
streaming?: boolean
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Options for streaming renderer
|
|
77
|
+
*/
|
|
78
|
+
export interface StreamingRendererOptions {
|
|
79
|
+
/** DOCTYPE to prepend */
|
|
80
|
+
doctype?: string
|
|
81
|
+
/** Shell wrapper function */
|
|
82
|
+
shell?: (content: string, hydration?: string) => string
|
|
83
|
+
/** Include hydration data */
|
|
84
|
+
includeHydration?: boolean
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Props for AI component creation
|
|
89
|
+
*/
|
|
90
|
+
export interface AIComponentProps<P = Record<string, unknown>> {
|
|
91
|
+
/** Component name */
|
|
92
|
+
name: string
|
|
93
|
+
/** Schema for AI prop generation */
|
|
94
|
+
schema: Record<string, string>
|
|
95
|
+
/** Render function */
|
|
96
|
+
render: (props: P) => string | Promise<string>
|
|
97
|
+
/** Fallback props on error */
|
|
98
|
+
fallback?: Partial<P>
|
|
99
|
+
/** Enable progressive enhancement */
|
|
100
|
+
progressive?: boolean
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Props for withAIProps wrapper
|
|
105
|
+
*/
|
|
106
|
+
export interface WithAIPropsOptions {
|
|
107
|
+
/** Schema for AI prop generation */
|
|
108
|
+
schema: Record<string, string>
|
|
109
|
+
/** Fallback props on error */
|
|
110
|
+
fallback?: Record<string, unknown>
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Props for AIPropsProvider
|
|
115
|
+
*/
|
|
116
|
+
export interface AIPropsProviderProps {
|
|
117
|
+
/** AI props configuration */
|
|
118
|
+
config: {
|
|
119
|
+
model?: string
|
|
120
|
+
cache?: boolean
|
|
121
|
+
system?: string
|
|
122
|
+
}
|
|
123
|
+
/** Children to render */
|
|
124
|
+
children: unknown
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Component type with schema attached
|
|
129
|
+
*/
|
|
130
|
+
export interface AIComponentFunction<P = Record<string, unknown>> {
|
|
131
|
+
(props: Partial<P> & { context?: Record<string, unknown> }): Promise<string>
|
|
132
|
+
schema: Record<string, string>
|
|
133
|
+
displayName?: string
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// Global hydration context (for useHydration hook simulation)
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
let globalHydrationContext: HydrationContext | null = null
|
|
141
|
+
|
|
142
|
+
// ============================================================================
|
|
143
|
+
// Hydration Data Collection
|
|
144
|
+
// ============================================================================
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Create hydration data with the toJSON method
|
|
148
|
+
*/
|
|
149
|
+
function createHydrationData(
|
|
150
|
+
components: Map<string, Record<string, unknown>>,
|
|
151
|
+
tree: HydrationNode[]
|
|
152
|
+
): HydrationData {
|
|
153
|
+
return {
|
|
154
|
+
components,
|
|
155
|
+
tree,
|
|
156
|
+
toJSON() {
|
|
157
|
+
return serializeHydrationData(this)
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Collect hydration data from a component render
|
|
164
|
+
*
|
|
165
|
+
* @param component - Component function to render
|
|
166
|
+
* @param props - Props to pass to component
|
|
167
|
+
* @returns Hydration data collected during render
|
|
168
|
+
*/
|
|
169
|
+
export async function collectHydrationData<P>(
|
|
170
|
+
component: (props: P) => string | Promise<string>,
|
|
171
|
+
props: P
|
|
172
|
+
): Promise<HydrationData> {
|
|
173
|
+
// Create a hydration context
|
|
174
|
+
const ctx = createHydrationContext()
|
|
175
|
+
|
|
176
|
+
// Store global context for nested components
|
|
177
|
+
const prevContext = globalHydrationContext
|
|
178
|
+
globalHydrationContext = ctx
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
// Get component name
|
|
182
|
+
const componentName = component.name || 'Anonymous'
|
|
183
|
+
|
|
184
|
+
// Register the component
|
|
185
|
+
ctx.register(componentName, props as Record<string, unknown>)
|
|
186
|
+
|
|
187
|
+
// Render the component (this might trigger nested registrations)
|
|
188
|
+
await component(props)
|
|
189
|
+
|
|
190
|
+
// Return collected data
|
|
191
|
+
return ctx.getData()
|
|
192
|
+
} finally {
|
|
193
|
+
// Restore previous context
|
|
194
|
+
globalHydrationContext = prevContext
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Create a hydration context for tracking component renders
|
|
200
|
+
*
|
|
201
|
+
* @returns New hydration context
|
|
202
|
+
*/
|
|
203
|
+
export function createHydrationContext(): HydrationContext {
|
|
204
|
+
const components = new Map<string, Record<string, unknown>>()
|
|
205
|
+
const tree: HydrationNode[] = []
|
|
206
|
+
let idCounter = 0
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
register(componentName: string, props: Record<string, unknown>): string {
|
|
210
|
+
const id = `${componentName}-${idCounter++}`
|
|
211
|
+
components.set(componentName, props)
|
|
212
|
+
|
|
213
|
+
const node: HydrationNode = {
|
|
214
|
+
id,
|
|
215
|
+
component: componentName,
|
|
216
|
+
props,
|
|
217
|
+
children: [],
|
|
218
|
+
}
|
|
219
|
+
tree.push(node)
|
|
220
|
+
|
|
221
|
+
return id
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
getData(): HydrationData {
|
|
225
|
+
return createHydrationData(new Map(components), [...tree])
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
clear(): void {
|
|
229
|
+
components.clear()
|
|
230
|
+
tree.length = 0
|
|
231
|
+
idCounter = 0
|
|
232
|
+
},
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Serialize hydration data to JSON string
|
|
238
|
+
*
|
|
239
|
+
* @param data - Hydration data to serialize
|
|
240
|
+
* @returns JSON string safe for script embedding
|
|
241
|
+
*/
|
|
242
|
+
export function serializeHydrationData(data: HydrationData): string {
|
|
243
|
+
// Convert Map to object for serialization
|
|
244
|
+
const componentsObj: Record<string, Record<string, unknown>> = {}
|
|
245
|
+
|
|
246
|
+
// Use a WeakSet to detect circular references
|
|
247
|
+
const seen = new WeakSet()
|
|
248
|
+
|
|
249
|
+
function sanitizeValue(value: unknown): unknown {
|
|
250
|
+
if (value === null || value === undefined) {
|
|
251
|
+
return value
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (typeof value === 'object') {
|
|
255
|
+
if (seen.has(value as object)) {
|
|
256
|
+
return '[Circular]'
|
|
257
|
+
}
|
|
258
|
+
seen.add(value as object)
|
|
259
|
+
|
|
260
|
+
if (Array.isArray(value)) {
|
|
261
|
+
return value.map(sanitizeValue)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const result: Record<string, unknown> = {}
|
|
265
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
266
|
+
result[k] = sanitizeValue(v)
|
|
267
|
+
}
|
|
268
|
+
return result
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return value
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for (const [key, value] of data.components) {
|
|
275
|
+
componentsObj[key] = sanitizeValue(value) as Record<string, unknown>
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const serializable = {
|
|
279
|
+
components: componentsObj,
|
|
280
|
+
tree: data.tree.map((node) => ({
|
|
281
|
+
id: node.id,
|
|
282
|
+
component: node.component,
|
|
283
|
+
props: sanitizeValue(node.props),
|
|
284
|
+
children: node.children,
|
|
285
|
+
})),
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let json = JSON.stringify(serializable)
|
|
289
|
+
|
|
290
|
+
// Escape script tags to prevent XSS
|
|
291
|
+
json = json.replace(/<script/gi, '\\u003cscript')
|
|
292
|
+
json = json.replace(/<\/script/gi, '\\u003c/script')
|
|
293
|
+
|
|
294
|
+
return json
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Provider component that enables hydration tracking for children
|
|
299
|
+
*
|
|
300
|
+
* @param props - Provider props with context and children
|
|
301
|
+
* @returns Rendered output with hydration tracking
|
|
302
|
+
*/
|
|
303
|
+
export function HydrationProvider(props: {
|
|
304
|
+
context: HydrationContext
|
|
305
|
+
children: unknown
|
|
306
|
+
}): unknown {
|
|
307
|
+
// Store context globally for nested components
|
|
308
|
+
const prevContext = globalHydrationContext
|
|
309
|
+
globalHydrationContext = props.context
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
// Return children - in a real JSX environment this would render them
|
|
313
|
+
return props.children
|
|
314
|
+
} finally {
|
|
315
|
+
// Note: In real JSX, we'd restore on unmount, not here
|
|
316
|
+
// For our purposes, we keep it set for the duration
|
|
317
|
+
globalHydrationContext = prevContext
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Hook to access hydration context from within a component
|
|
323
|
+
*
|
|
324
|
+
* @returns Current hydration context
|
|
325
|
+
*/
|
|
326
|
+
export function useHydration(): HydrationContext {
|
|
327
|
+
if (!globalHydrationContext) {
|
|
328
|
+
// Return a no-op context if not in a HydrationProvider
|
|
329
|
+
return createHydrationContext()
|
|
330
|
+
}
|
|
331
|
+
return globalHydrationContext
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ============================================================================
|
|
335
|
+
// Streaming Render
|
|
336
|
+
// ============================================================================
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Render a component to a ReadableStream
|
|
340
|
+
*
|
|
341
|
+
* @param component - Component function to render
|
|
342
|
+
* @param props - Props to pass to component
|
|
343
|
+
* @param options - Streaming options
|
|
344
|
+
* @returns ReadableStream of rendered HTML
|
|
345
|
+
*/
|
|
346
|
+
export async function renderToReadableStream<P>(
|
|
347
|
+
component: (props: P) => string | Promise<string>,
|
|
348
|
+
props: P,
|
|
349
|
+
options?: StreamingOptions
|
|
350
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
351
|
+
const encoder = new TextEncoder()
|
|
352
|
+
|
|
353
|
+
return new ReadableStream<Uint8Array>({
|
|
354
|
+
async start(controller) {
|
|
355
|
+
try {
|
|
356
|
+
// Create hydration context if needed
|
|
357
|
+
const ctx = options?.includeHydration ? createHydrationContext() : null
|
|
358
|
+
|
|
359
|
+
if (ctx) {
|
|
360
|
+
globalHydrationContext = ctx
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Render the component
|
|
364
|
+
let content: string
|
|
365
|
+
try {
|
|
366
|
+
content = await component(props)
|
|
367
|
+
} catch (error) {
|
|
368
|
+
if (options?.onError && error instanceof Error) {
|
|
369
|
+
content = options.onError(error)
|
|
370
|
+
} else {
|
|
371
|
+
throw error
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Enqueue the content in chunks for streaming behavior
|
|
376
|
+
const chunkSize = 1024
|
|
377
|
+
for (let i = 0; i < content.length; i += chunkSize) {
|
|
378
|
+
const chunk = content.slice(i, i + chunkSize)
|
|
379
|
+
controller.enqueue(encoder.encode(chunk))
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Add hydration script if requested
|
|
383
|
+
if (options?.includeHydration && ctx) {
|
|
384
|
+
const componentName = component.name || 'Anonymous'
|
|
385
|
+
ctx.register(componentName, props as Record<string, unknown>)
|
|
386
|
+
const hydrationData = ctx.getData()
|
|
387
|
+
const hydrationScript = `<script>window.__HYDRATION_DATA__=${serializeHydrationData(
|
|
388
|
+
hydrationData
|
|
389
|
+
)}</script>`
|
|
390
|
+
controller.enqueue(encoder.encode(hydrationScript))
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
controller.close()
|
|
394
|
+
} catch (error) {
|
|
395
|
+
controller.error(error)
|
|
396
|
+
} finally {
|
|
397
|
+
globalHydrationContext = null
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
})
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Create a streaming Response from a component
|
|
405
|
+
*
|
|
406
|
+
* @param component - Component function to render
|
|
407
|
+
* @param props - Props to pass to component
|
|
408
|
+
* @param options - Streaming options
|
|
409
|
+
* @returns Response with streaming body
|
|
410
|
+
*/
|
|
411
|
+
export async function streamJSXResponse<P>(
|
|
412
|
+
component: (props: P) => string | Promise<string>,
|
|
413
|
+
props: P,
|
|
414
|
+
options?: StreamingOptions
|
|
415
|
+
): Promise<Response> {
|
|
416
|
+
const stream = await renderToReadableStream(component, props, options)
|
|
417
|
+
|
|
418
|
+
const headers = new Headers({
|
|
419
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
420
|
+
...options?.headers,
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
// Don't set Content-Length for streaming responses
|
|
424
|
+
if (options?.streaming) {
|
|
425
|
+
// Transfer-Encoding is typically set automatically by the runtime
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return new Response(stream, {
|
|
429
|
+
status: 200,
|
|
430
|
+
headers,
|
|
431
|
+
})
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Create a reusable streaming renderer with configuration
|
|
436
|
+
*
|
|
437
|
+
* @param options - Renderer options
|
|
438
|
+
* @returns Streaming renderer instance
|
|
439
|
+
*/
|
|
440
|
+
export function createStreamingRenderer(options: StreamingRendererOptions): {
|
|
441
|
+
render: <P>(
|
|
442
|
+
component: (props: P) => string | Promise<string>,
|
|
443
|
+
props: P
|
|
444
|
+
) => Promise<ReadableStream<Uint8Array>>
|
|
445
|
+
} {
|
|
446
|
+
return {
|
|
447
|
+
async render<P>(
|
|
448
|
+
component: (props: P) => string | Promise<string>,
|
|
449
|
+
props: P
|
|
450
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
451
|
+
const encoder = new TextEncoder()
|
|
452
|
+
|
|
453
|
+
return new ReadableStream<Uint8Array>({
|
|
454
|
+
async start(controller) {
|
|
455
|
+
try {
|
|
456
|
+
// Create hydration context
|
|
457
|
+
const ctx = options.includeHydration ? createHydrationContext() : null
|
|
458
|
+
|
|
459
|
+
if (ctx) {
|
|
460
|
+
globalHydrationContext = ctx
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Render the component
|
|
464
|
+
const content = await component(props)
|
|
465
|
+
|
|
466
|
+
// Register for hydration data
|
|
467
|
+
if (ctx) {
|
|
468
|
+
const componentName = component.name || 'Anonymous'
|
|
469
|
+
ctx.register(componentName, props as Record<string, unknown>)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Get hydration data
|
|
473
|
+
let hydrationJson = ''
|
|
474
|
+
if (options.includeHydration && ctx) {
|
|
475
|
+
hydrationJson = serializeHydrationData(ctx.getData())
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Apply shell wrapper if provided
|
|
479
|
+
let output: string
|
|
480
|
+
if (options.shell) {
|
|
481
|
+
// When using shell, pass hydration data with __HYDRATION_DATA__ prefix
|
|
482
|
+
// so the shell can embed it directly in a script tag
|
|
483
|
+
const hydrationScript = hydrationJson
|
|
484
|
+
? `window.__HYDRATION_DATA__=${hydrationJson}`
|
|
485
|
+
: ''
|
|
486
|
+
output = options.shell(content, hydrationScript)
|
|
487
|
+
} else {
|
|
488
|
+
output = content
|
|
489
|
+
if (options.includeHydration && hydrationJson) {
|
|
490
|
+
output += `<script>window.__HYDRATION_DATA__=${hydrationJson}</script>`
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Prepend doctype if provided
|
|
495
|
+
if (options.doctype) {
|
|
496
|
+
output = options.doctype + '\n' + output
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Stream in chunks
|
|
500
|
+
const chunkSize = 1024
|
|
501
|
+
for (let i = 0; i < output.length; i += chunkSize) {
|
|
502
|
+
const chunk = output.slice(i, i + chunkSize)
|
|
503
|
+
controller.enqueue(encoder.encode(chunk))
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
controller.close()
|
|
507
|
+
} catch (error) {
|
|
508
|
+
controller.error(error)
|
|
509
|
+
} finally {
|
|
510
|
+
globalHydrationContext = null
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
})
|
|
514
|
+
},
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ============================================================================
|
|
519
|
+
// AI Component Creation
|
|
520
|
+
// ============================================================================
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Create a component with AI-powered prop generation
|
|
524
|
+
*
|
|
525
|
+
* @param options - Component options including schema and render function
|
|
526
|
+
* @returns AI-enhanced component function
|
|
527
|
+
*/
|
|
528
|
+
export function createAIComponent<P extends Record<string, unknown>>(
|
|
529
|
+
options: AIComponentProps<P>
|
|
530
|
+
): AIComponentFunction<P> {
|
|
531
|
+
const { name, schema, render, fallback, progressive } = options
|
|
532
|
+
|
|
533
|
+
const aiComponent = async (
|
|
534
|
+
props: Partial<P> & { context?: Record<string, unknown> }
|
|
535
|
+
): Promise<string> => {
|
|
536
|
+
const { context, ...partialProps } = props
|
|
537
|
+
|
|
538
|
+
// Check which props are missing
|
|
539
|
+
const schemaKeys = Object.keys(schema)
|
|
540
|
+
const providedKeys = Object.keys(partialProps)
|
|
541
|
+
const missingKeys = schemaKeys.filter((k) => !providedKeys.includes(k))
|
|
542
|
+
|
|
543
|
+
let finalProps: P
|
|
544
|
+
|
|
545
|
+
if (missingKeys.length === 0) {
|
|
546
|
+
// All props provided, no generation needed
|
|
547
|
+
finalProps = partialProps as P
|
|
548
|
+
} else {
|
|
549
|
+
// Generate missing props
|
|
550
|
+
try {
|
|
551
|
+
// Build schema for only missing props
|
|
552
|
+
const missingSchema: Record<string, string> = {}
|
|
553
|
+
for (const key of missingKeys) {
|
|
554
|
+
const schemaValue = schema[key]
|
|
555
|
+
if (schemaValue !== undefined) {
|
|
556
|
+
missingSchema[key] = schemaValue
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const result = await generateProps<Partial<P>>({
|
|
561
|
+
schema: missingSchema,
|
|
562
|
+
context: context || partialProps,
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
finalProps = {
|
|
566
|
+
...result.props,
|
|
567
|
+
...partialProps,
|
|
568
|
+
} as P
|
|
569
|
+
} catch (error) {
|
|
570
|
+
// Use fallback on error
|
|
571
|
+
if (fallback) {
|
|
572
|
+
finalProps = {
|
|
573
|
+
...fallback,
|
|
574
|
+
...partialProps,
|
|
575
|
+
} as P
|
|
576
|
+
} else {
|
|
577
|
+
throw error
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Render the component
|
|
583
|
+
return render(finalProps)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Attach schema and metadata
|
|
587
|
+
aiComponent.schema = schema
|
|
588
|
+
aiComponent.displayName = `AI(${name})`
|
|
589
|
+
|
|
590
|
+
return aiComponent
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Wrap an existing component with AI prop generation
|
|
595
|
+
*
|
|
596
|
+
* @param component - Component function to wrap
|
|
597
|
+
* @param options - AI props options
|
|
598
|
+
* @returns Wrapped component with AI props
|
|
599
|
+
*/
|
|
600
|
+
export function withAIProps<P extends Record<string, unknown>>(
|
|
601
|
+
component: (props: P) => string | Promise<string>,
|
|
602
|
+
options: WithAIPropsOptions
|
|
603
|
+
): AIComponentFunction<P> {
|
|
604
|
+
const { schema, fallback } = options
|
|
605
|
+
|
|
606
|
+
const wrappedComponent = async (
|
|
607
|
+
props: Partial<P> & { context?: Record<string, unknown> }
|
|
608
|
+
): Promise<string> => {
|
|
609
|
+
const { context, ...partialProps } = props
|
|
610
|
+
|
|
611
|
+
// Check which props are missing
|
|
612
|
+
const schemaKeys = Object.keys(schema)
|
|
613
|
+
const providedKeys = Object.keys(partialProps)
|
|
614
|
+
const missingKeys = schemaKeys.filter((k) => !providedKeys.includes(k))
|
|
615
|
+
|
|
616
|
+
let finalProps: P
|
|
617
|
+
|
|
618
|
+
if (missingKeys.length === 0) {
|
|
619
|
+
// All props provided
|
|
620
|
+
finalProps = partialProps as P
|
|
621
|
+
} else {
|
|
622
|
+
// Generate missing props
|
|
623
|
+
try {
|
|
624
|
+
const missingSchema: Record<string, string> = {}
|
|
625
|
+
for (const key of missingKeys) {
|
|
626
|
+
const schemaValue = schema[key]
|
|
627
|
+
if (schemaValue !== undefined) {
|
|
628
|
+
missingSchema[key] = schemaValue
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const result = await generateProps<Partial<P>>({
|
|
633
|
+
schema: missingSchema,
|
|
634
|
+
context: context || partialProps,
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
finalProps = {
|
|
638
|
+
...result.props,
|
|
639
|
+
...partialProps,
|
|
640
|
+
} as P
|
|
641
|
+
} catch (error) {
|
|
642
|
+
if (fallback) {
|
|
643
|
+
finalProps = {
|
|
644
|
+
...fallback,
|
|
645
|
+
...partialProps,
|
|
646
|
+
} as P
|
|
647
|
+
} else {
|
|
648
|
+
throw error
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return component(finalProps)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Preserve displayName
|
|
657
|
+
const originalName =
|
|
658
|
+
(component as { displayName?: string }).displayName || component.name || 'Component'
|
|
659
|
+
wrappedComponent.schema = schema
|
|
660
|
+
wrappedComponent.displayName = `withAIProps(${originalName})`
|
|
661
|
+
|
|
662
|
+
return wrappedComponent
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Provider component for AI props configuration
|
|
667
|
+
*
|
|
668
|
+
* @param props - Provider props with config and children
|
|
669
|
+
* @returns Rendered output with AI props context
|
|
670
|
+
*/
|
|
671
|
+
export function AIPropsProvider(props: AIPropsProviderProps): unknown {
|
|
672
|
+
// Store configuration globally (in a real implementation, use React Context)
|
|
673
|
+
// For now, we just pass through children
|
|
674
|
+
return props.children
|
|
675
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -64,3 +64,33 @@ export {
|
|
|
64
64
|
createValidator,
|
|
65
65
|
assertValidProps,
|
|
66
66
|
} from './validate.js'
|
|
67
|
+
|
|
68
|
+
// Export MDX utilities
|
|
69
|
+
export {
|
|
70
|
+
parseMDX,
|
|
71
|
+
extractComponentSchemas,
|
|
72
|
+
renderMDXWithProps,
|
|
73
|
+
streamMDXWithProps,
|
|
74
|
+
createMDXPropsGenerator,
|
|
75
|
+
compileMDX,
|
|
76
|
+
clearMDXCache,
|
|
77
|
+
getMDXCacheSize,
|
|
78
|
+
configureMDXCache,
|
|
79
|
+
getMDXCacheStats,
|
|
80
|
+
invalidateMDXCacheByTag,
|
|
81
|
+
cleanupMDXCache,
|
|
82
|
+
type ParsedMDX,
|
|
83
|
+
type ComponentSchemas,
|
|
84
|
+
type MDXPropsGeneratorOptions,
|
|
85
|
+
type MDXPropsGenerator,
|
|
86
|
+
type RenderMDXOptions,
|
|
87
|
+
type StreamMDXOptions,
|
|
88
|
+
type CompileMDXOptions,
|
|
89
|
+
type CompiledMDXFunction,
|
|
90
|
+
type MDXCacheEntry,
|
|
91
|
+
type MDXParseError,
|
|
92
|
+
type CacheInvalidationStrategy,
|
|
93
|
+
type MDXCacheOptions,
|
|
94
|
+
type MDXCacheStats,
|
|
95
|
+
type ParseMDXOptions,
|
|
96
|
+
} from './mdx.js'
|