ai-props 0.1.2 → 2.0.2

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 (69) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/CHANGELOG.md +15 -0
  3. package/README.md +345 -352
  4. package/dist/ai.d.ts +125 -0
  5. package/dist/ai.d.ts.map +1 -0
  6. package/dist/ai.js +199 -0
  7. package/dist/ai.js.map +1 -0
  8. package/dist/cache.d.ts +66 -0
  9. package/dist/cache.d.ts.map +1 -0
  10. package/dist/cache.js +183 -0
  11. package/dist/cache.js.map +1 -0
  12. package/dist/generate.d.ts +69 -0
  13. package/dist/generate.d.ts.map +1 -0
  14. package/dist/generate.js +221 -0
  15. package/dist/generate.js.map +1 -0
  16. package/dist/hoc.d.ts +164 -0
  17. package/dist/hoc.d.ts.map +1 -0
  18. package/dist/hoc.js +236 -0
  19. package/dist/hoc.js.map +1 -0
  20. package/dist/index.d.ts +14 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +21 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/types.d.ts +152 -0
  25. package/dist/types.d.ts.map +1 -0
  26. package/dist/types.js +7 -0
  27. package/dist/types.js.map +1 -0
  28. package/dist/validate.d.ts +58 -0
  29. package/dist/validate.d.ts.map +1 -0
  30. package/dist/validate.js +253 -0
  31. package/dist/validate.js.map +1 -0
  32. package/package.json +16 -63
  33. package/src/ai.ts +264 -0
  34. package/src/cache.ts +216 -0
  35. package/src/generate.ts +276 -0
  36. package/src/hoc.ts +309 -0
  37. package/src/index.ts +66 -0
  38. package/src/types.ts +167 -0
  39. package/src/validate.ts +333 -0
  40. package/test/ai.test.ts +327 -0
  41. package/test/cache.test.ts +236 -0
  42. package/test/generate.test.ts +406 -0
  43. package/test/hoc.test.ts +411 -0
  44. package/test/validate.test.ts +324 -0
  45. package/tsconfig.json +9 -0
  46. package/vitest.config.ts +15 -0
  47. package/LICENSE +0 -21
  48. package/dist/AI.d.ts +0 -27
  49. package/dist/AI.d.ts.map +0 -1
  50. package/dist/AI.test.d.ts +0 -2
  51. package/dist/AI.test.d.ts.map +0 -1
  52. package/dist/ai-props.es.js +0 -3697
  53. package/dist/ai-props.umd.js +0 -30
  54. package/dist/components/ErrorBoundary.d.ts +0 -17
  55. package/dist/components/ErrorBoundary.d.ts.map +0 -1
  56. package/dist/examples/BlogList.d.ts +0 -2
  57. package/dist/examples/BlogList.d.ts.map +0 -1
  58. package/dist/examples/BlogList.fixture.d.ts +0 -5
  59. package/dist/examples/BlogList.fixture.d.ts.map +0 -1
  60. package/dist/examples/HeroSection.d.ts +0 -2
  61. package/dist/examples/HeroSection.d.ts.map +0 -1
  62. package/dist/examples/HeroSection.fixture.d.ts +0 -5
  63. package/dist/examples/HeroSection.fixture.d.ts.map +0 -1
  64. package/dist/test/setup.d.ts +0 -2
  65. package/dist/test/setup.d.ts.map +0 -1
  66. package/dist/utils/schema.d.ts +0 -28
  67. package/dist/utils/schema.d.ts.map +0 -1
  68. package/dist/utils/styles.d.ts +0 -3
  69. package/dist/utils/styles.d.ts.map +0 -1
@@ -0,0 +1,327 @@
1
+ /**
2
+ * Tests for AI() wrapper and component functions
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
6
+ import {
7
+ AI,
8
+ createAIComponent,
9
+ definePropsSchema,
10
+ createComponentFactory,
11
+ composeAIComponents,
12
+ } from '../src/ai.js'
13
+ import { configureAIProps, resetConfig, clearCache } from '../src/index.js'
14
+
15
+ // Mock the generateObject function
16
+ vi.mock('ai-functions', () => ({
17
+ generateObject: vi.fn().mockImplementation(async ({ schema }) => {
18
+ // Generate mock data based on schema
19
+ const mockData: Record<string, unknown> = {}
20
+ for (const [key, value] of Object.entries(schema)) {
21
+ if (typeof value === 'string') {
22
+ if (value.includes('(number)')) {
23
+ mockData[key] = 42
24
+ } else if (value.includes('(boolean)')) {
25
+ mockData[key] = true
26
+ } else {
27
+ mockData[key] = `generated-${key}`
28
+ }
29
+ } else if (Array.isArray(value)) {
30
+ mockData[key] = ['item1', 'item2']
31
+ } else if (typeof value === 'object') {
32
+ mockData[key] = { nested: 'value' }
33
+ }
34
+ }
35
+ return { object: mockData }
36
+ }),
37
+ schema: vi.fn((s) => s),
38
+ }))
39
+
40
+ describe('AI()', () => {
41
+ beforeEach(() => {
42
+ resetConfig()
43
+ clearCache()
44
+ vi.clearAllMocks()
45
+ })
46
+
47
+ it('creates an AI component wrapper', () => {
48
+ const UserCard = AI({
49
+ schema: {
50
+ name: 'User name',
51
+ bio: 'User biography',
52
+ },
53
+ })
54
+
55
+ expect(typeof UserCard).toBe('function')
56
+ expect(UserCard.schema).toBeDefined()
57
+ expect(UserCard.generateProps).toBeDefined()
58
+ })
59
+
60
+ it('generates missing props', async () => {
61
+ const UserCard = AI({
62
+ schema: {
63
+ name: 'User name',
64
+ bio: 'User biography',
65
+ },
66
+ })
67
+
68
+ const props = await UserCard({})
69
+
70
+ expect(props.name).toBe('generated-name')
71
+ expect(props.bio).toBe('generated-bio')
72
+ })
73
+
74
+ it('preserves provided props', async () => {
75
+ const UserCard = AI({
76
+ schema: {
77
+ name: 'User name',
78
+ bio: 'User biography',
79
+ },
80
+ })
81
+
82
+ const props = await UserCard({ name: 'John Doe' })
83
+
84
+ expect(props.name).toBe('John Doe')
85
+ expect(props.bio).toBe('generated-bio')
86
+ })
87
+
88
+ it('applies defaults', async () => {
89
+ const UserCard = AI({
90
+ schema: {
91
+ name: 'User name',
92
+ role: 'User role',
93
+ },
94
+ defaults: {
95
+ role: 'member',
96
+ },
97
+ })
98
+
99
+ const props = await UserCard({})
100
+
101
+ expect(props.role).toBe('member')
102
+ })
103
+
104
+ it('throws for missing required props', async () => {
105
+ const UserCard = AI({
106
+ schema: {
107
+ name: 'User name',
108
+ email: 'Email address',
109
+ },
110
+ required: ['email'],
111
+ })
112
+
113
+ await expect(UserCard({ name: 'John' })).rejects.toThrow('Missing required props')
114
+ })
115
+
116
+ it('excludes specified props from generation', async () => {
117
+ const UserCard = AI({
118
+ schema: {
119
+ name: 'User name',
120
+ avatar: 'Avatar URL',
121
+ internal: 'Internal data',
122
+ },
123
+ exclude: ['internal'],
124
+ })
125
+
126
+ const props = await UserCard({})
127
+
128
+ expect(props.name).toBe('generated-name')
129
+ expect(props.avatar).toBe('generated-avatar')
130
+ // internal should not be generated
131
+ expect(props.internal).toBeUndefined()
132
+ })
133
+
134
+ it('exposes generateProps method', async () => {
135
+ const UserCard = AI({
136
+ schema: {
137
+ name: 'User name',
138
+ },
139
+ })
140
+
141
+ const props = await UserCard.generateProps({ name: 'Context Name' })
142
+
143
+ expect(props).toBeDefined()
144
+ expect(props.name).toBe('Context Name')
145
+ })
146
+ })
147
+
148
+ describe('createAIComponent', () => {
149
+ beforeEach(() => {
150
+ resetConfig()
151
+ clearCache()
152
+ vi.clearAllMocks()
153
+ })
154
+
155
+ it('creates typed AI component', async () => {
156
+ const ProductCard = createAIComponent<{
157
+ title: string
158
+ price: number
159
+ description: string
160
+ }>({
161
+ schema: {
162
+ title: 'Product title',
163
+ price: 'Price (number)',
164
+ description: 'Description',
165
+ },
166
+ })
167
+
168
+ const props = await ProductCard({})
169
+
170
+ expect(typeof props.title).toBe('string')
171
+ expect(typeof props.price).toBe('number')
172
+ })
173
+ })
174
+
175
+ describe('definePropsSchema', () => {
176
+ it('returns schema unchanged', () => {
177
+ const schema = definePropsSchema({
178
+ name: 'User name',
179
+ email: 'Email address',
180
+ })
181
+
182
+ expect(schema).toEqual({
183
+ name: 'User name',
184
+ email: 'Email address',
185
+ })
186
+ })
187
+
188
+ it('preserves complex schemas', () => {
189
+ const schema = definePropsSchema({
190
+ user: {
191
+ name: 'Name',
192
+ profile: {
193
+ bio: 'Biography',
194
+ },
195
+ },
196
+ tags: ['Tag list'],
197
+ })
198
+
199
+ expect(schema.user).toBeDefined()
200
+ expect(schema.tags).toBeDefined()
201
+ })
202
+ })
203
+
204
+ describe('createComponentFactory', () => {
205
+ beforeEach(() => {
206
+ resetConfig()
207
+ clearCache()
208
+ vi.clearAllMocks()
209
+ })
210
+
211
+ it('creates component factory with generate method', async () => {
212
+ const factory = createComponentFactory({
213
+ schema: {
214
+ name: 'Product name',
215
+ price: 'Price (number)',
216
+ },
217
+ })
218
+
219
+ expect(factory.component).toBeDefined()
220
+ expect(factory.generate).toBeDefined()
221
+ expect(factory.generateMany).toBeDefined()
222
+ expect(factory.generateWith).toBeDefined()
223
+ })
224
+
225
+ it('generates single instance', async () => {
226
+ const factory = createComponentFactory({
227
+ schema: {
228
+ name: 'Product name',
229
+ },
230
+ })
231
+
232
+ const product = await factory.generate({ category: 'electronics' })
233
+
234
+ expect(product.name).toBeDefined()
235
+ })
236
+
237
+ it('generates multiple instances', async () => {
238
+ const factory = createComponentFactory({
239
+ schema: {
240
+ name: 'Product name',
241
+ },
242
+ })
243
+
244
+ const products = await factory.generateMany([
245
+ { category: 'electronics' },
246
+ { category: 'clothing' },
247
+ { category: 'food' },
248
+ ])
249
+
250
+ expect(products).toHaveLength(3)
251
+ expect(products[0]?.name).toBeDefined()
252
+ })
253
+
254
+ it('generates with overrides', async () => {
255
+ const factory = createComponentFactory({
256
+ schema: {
257
+ name: 'Product name',
258
+ price: 'Price (number)',
259
+ },
260
+ })
261
+
262
+ const product = await factory.generateWith(
263
+ { category: 'tech' },
264
+ { price: 99 }
265
+ )
266
+
267
+ expect(product.price).toBe(99)
268
+ expect(product.name).toBeDefined()
269
+ })
270
+ })
271
+
272
+ describe('composeAIComponents', () => {
273
+ beforeEach(() => {
274
+ resetConfig()
275
+ clearCache()
276
+ vi.clearAllMocks()
277
+ })
278
+
279
+ it('composes multiple schemas', async () => {
280
+ const FullProfile = composeAIComponents({
281
+ user: {
282
+ schema: { name: 'User name' },
283
+ },
284
+ settings: {
285
+ schema: { theme: 'Theme preference' },
286
+ },
287
+ })
288
+
289
+ expect(FullProfile.schema).toBeDefined()
290
+ expect(typeof FullProfile).toBe('function')
291
+ })
292
+
293
+ it('generates composed props', async () => {
294
+ const FullProfile = composeAIComponents({
295
+ user: {
296
+ schema: { name: 'User name' },
297
+ },
298
+ settings: {
299
+ schema: { theme: 'Theme preference' },
300
+ },
301
+ })
302
+
303
+ const profile = await FullProfile({
304
+ user: { name: 'John' },
305
+ settings: {},
306
+ })
307
+
308
+ expect(profile.user.name).toBe('John')
309
+ expect(profile.settings.theme).toBeDefined()
310
+ })
311
+
312
+ it('generates all missing sections', async () => {
313
+ const FullProfile = composeAIComponents({
314
+ user: {
315
+ schema: { name: 'User name' },
316
+ },
317
+ prefs: {
318
+ schema: { notifications: 'Notifications (boolean)' },
319
+ },
320
+ })
321
+
322
+ const profile = await FullProfile({})
323
+
324
+ expect(profile.user).toBeDefined()
325
+ expect(profile.prefs).toBeDefined()
326
+ })
327
+ })
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Tests for ai-props cache
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
6
+ import {
7
+ MemoryPropsCache,
8
+ LRUPropsCache,
9
+ createCacheKey,
10
+ getDefaultCache,
11
+ configureCache,
12
+ clearCache,
13
+ DEFAULT_CACHE_TTL,
14
+ } from '../src/cache.js'
15
+
16
+ describe('createCacheKey', () => {
17
+ it('creates consistent keys for same inputs', () => {
18
+ const key1 = createCacheKey({ name: 'string' }, { id: 1 })
19
+ const key2 = createCacheKey({ name: 'string' }, { id: 1 })
20
+ expect(key1).toBe(key2)
21
+ })
22
+
23
+ it('creates different keys for different schemas', () => {
24
+ const key1 = createCacheKey({ name: 'string' })
25
+ const key2 = createCacheKey({ name: 'string', age: 'number' })
26
+ expect(key1).not.toBe(key2)
27
+ })
28
+
29
+ it('creates different keys for different contexts', () => {
30
+ const key1 = createCacheKey({ name: 'string' }, { id: 1 })
31
+ const key2 = createCacheKey({ name: 'string' }, { id: 2 })
32
+ expect(key1).not.toBe(key2)
33
+ })
34
+
35
+ it('handles undefined context', () => {
36
+ const key1 = createCacheKey({ name: 'string' })
37
+ const key2 = createCacheKey({ name: 'string' }, undefined)
38
+ expect(key1).toBe(key2)
39
+ })
40
+
41
+ it('handles string schemas', () => {
42
+ const key = createCacheKey('user name')
43
+ expect(typeof key).toBe('string')
44
+ expect(key.length).toBeGreaterThan(0)
45
+ })
46
+
47
+ it('creates same key regardless of object key order', () => {
48
+ const key1 = createCacheKey({ a: '1' }, { x: 1, y: 2 })
49
+ const key2 = createCacheKey({ a: '1' }, { y: 2, x: 1 })
50
+ expect(key1).toBe(key2)
51
+ })
52
+ })
53
+
54
+ describe('MemoryPropsCache', () => {
55
+ let cache: MemoryPropsCache
56
+
57
+ beforeEach(() => {
58
+ cache = new MemoryPropsCache()
59
+ })
60
+
61
+ it('stores and retrieves values', () => {
62
+ cache.set('key1', { name: 'test' })
63
+ const entry = cache.get<{ name: string }>('key1')
64
+
65
+ expect(entry).toBeDefined()
66
+ expect(entry?.props.name).toBe('test')
67
+ })
68
+
69
+ it('returns undefined for missing keys', () => {
70
+ const entry = cache.get('nonexistent')
71
+ expect(entry).toBeUndefined()
72
+ })
73
+
74
+ it('deletes entries', () => {
75
+ cache.set('key1', { value: 1 })
76
+ expect(cache.delete('key1')).toBe(true)
77
+ expect(cache.get('key1')).toBeUndefined()
78
+ })
79
+
80
+ it('clears all entries', () => {
81
+ cache.set('key1', { value: 1 })
82
+ cache.set('key2', { value: 2 })
83
+ cache.clear()
84
+
85
+ expect(cache.size).toBe(0)
86
+ expect(cache.get('key1')).toBeUndefined()
87
+ expect(cache.get('key2')).toBeUndefined()
88
+ })
89
+
90
+ it('tracks size correctly', () => {
91
+ expect(cache.size).toBe(0)
92
+
93
+ cache.set('key1', { value: 1 })
94
+ expect(cache.size).toBe(1)
95
+
96
+ cache.set('key2', { value: 2 })
97
+ expect(cache.size).toBe(2)
98
+
99
+ cache.delete('key1')
100
+ expect(cache.size).toBe(1)
101
+ })
102
+
103
+ it('respects TTL', async () => {
104
+ const shortCache = new MemoryPropsCache(50) // 50ms TTL
105
+ shortCache.set('key1', { value: 1 })
106
+
107
+ expect(shortCache.get('key1')).toBeDefined()
108
+
109
+ // Wait for expiration
110
+ await new Promise(resolve => setTimeout(resolve, 60))
111
+
112
+ expect(shortCache.get('key1')).toBeUndefined()
113
+ })
114
+
115
+ it('cleans up expired entries', async () => {
116
+ const shortCache = new MemoryPropsCache(50)
117
+ shortCache.set('key1', { value: 1 })
118
+ shortCache.set('key2', { value: 2 })
119
+
120
+ await new Promise(resolve => setTimeout(resolve, 60))
121
+
122
+ const removed = shortCache.cleanup()
123
+ expect(removed).toBe(2)
124
+ expect(shortCache.size).toBe(0)
125
+ })
126
+
127
+ it('stores entry metadata', () => {
128
+ cache.set('key1', { value: 1 })
129
+ const entry = cache.get('key1')
130
+
131
+ expect(entry).toBeDefined()
132
+ expect(entry?.key).toBe('key1')
133
+ expect(entry?.timestamp).toBeDefined()
134
+ expect(typeof entry?.timestamp).toBe('number')
135
+ })
136
+ })
137
+
138
+ describe('LRUPropsCache', () => {
139
+ let cache: LRUPropsCache
140
+
141
+ beforeEach(() => {
142
+ cache = new LRUPropsCache(3) // Max 3 entries
143
+ })
144
+
145
+ it('stores and retrieves values', () => {
146
+ cache.set('key1', { value: 1 })
147
+ const entry = cache.get<{ value: number }>('key1')
148
+
149
+ expect(entry?.props.value).toBe(1)
150
+ })
151
+
152
+ it('evicts oldest entries when at capacity', () => {
153
+ cache.set('key1', { value: 1 })
154
+ cache.set('key2', { value: 2 })
155
+ cache.set('key3', { value: 3 })
156
+ cache.set('key4', { value: 4 }) // Should evict key1
157
+
158
+ expect(cache.get('key1')).toBeUndefined()
159
+ expect(cache.get('key2')).toBeDefined()
160
+ expect(cache.get('key3')).toBeDefined()
161
+ expect(cache.get('key4')).toBeDefined()
162
+ })
163
+
164
+ it('updates LRU order on access', () => {
165
+ cache.set('key1', { value: 1 })
166
+ cache.set('key2', { value: 2 })
167
+ cache.set('key3', { value: 3 })
168
+
169
+ // Access key1, making it most recently used
170
+ cache.get('key1')
171
+
172
+ // Add key4, should evict key2 (oldest now)
173
+ cache.set('key4', { value: 4 })
174
+
175
+ expect(cache.get('key1')).toBeDefined() // Still there
176
+ expect(cache.get('key2')).toBeUndefined() // Evicted
177
+ expect(cache.get('key3')).toBeDefined()
178
+ expect(cache.get('key4')).toBeDefined()
179
+ })
180
+
181
+ it('respects TTL in addition to capacity', async () => {
182
+ const shortCache = new LRUPropsCache(10, 50) // 50ms TTL
183
+ shortCache.set('key1', { value: 1 })
184
+
185
+ await new Promise(resolve => setTimeout(resolve, 60))
186
+
187
+ expect(shortCache.get('key1')).toBeUndefined()
188
+ })
189
+
190
+ it('maintains correct size', () => {
191
+ cache.set('key1', { value: 1 })
192
+ cache.set('key2', { value: 2 })
193
+ cache.set('key3', { value: 3 })
194
+
195
+ expect(cache.size).toBe(3)
196
+
197
+ cache.set('key4', { value: 4 })
198
+ expect(cache.size).toBe(3) // Still 3 after eviction
199
+ })
200
+ })
201
+
202
+ describe('Default cache management', () => {
203
+ beforeEach(() => {
204
+ clearCache()
205
+ })
206
+
207
+ it('getDefaultCache returns singleton', () => {
208
+ const cache1 = getDefaultCache()
209
+ const cache2 = getDefaultCache()
210
+ expect(cache1).toBe(cache2)
211
+ })
212
+
213
+ it('configureCache creates new instance with TTL', () => {
214
+ const oldCache = getDefaultCache()
215
+ configureCache(1000)
216
+ const newCache = getDefaultCache()
217
+
218
+ // Should be a different instance
219
+ expect(newCache).not.toBe(oldCache)
220
+ })
221
+
222
+ it('clearCache empties the cache', () => {
223
+ const cache = getDefaultCache()
224
+ cache.set('key1', { value: 1 })
225
+
226
+ clearCache()
227
+
228
+ expect(cache.size).toBe(0)
229
+ })
230
+ })
231
+
232
+ describe('DEFAULT_CACHE_TTL', () => {
233
+ it('is 5 minutes in milliseconds', () => {
234
+ expect(DEFAULT_CACHE_TTL).toBe(5 * 60 * 1000)
235
+ })
236
+ })