ai-evaluate 2.1.7 → 2.2.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 (102) hide show
  1. package/README.md +61 -4
  2. package/dist/evaluate.d.ts.map +1 -1
  3. package/dist/evaluate.js +18 -16
  4. package/dist/evaluate.js.map +1 -1
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/miniflare-pool.d.ts +109 -0
  10. package/dist/miniflare-pool.d.ts.map +1 -0
  11. package/dist/miniflare-pool.js +308 -0
  12. package/dist/miniflare-pool.js.map +1 -0
  13. package/dist/node.d.ts.map +1 -1
  14. package/dist/node.js +42 -10
  15. package/dist/node.js.map +1 -1
  16. package/dist/shared.d.ts +66 -0
  17. package/dist/shared.d.ts.map +1 -0
  18. package/dist/shared.js +169 -0
  19. package/dist/shared.js.map +1 -0
  20. package/dist/static/index.d.ts +111 -0
  21. package/dist/static/index.d.ts.map +1 -0
  22. package/dist/static/index.js +347 -0
  23. package/dist/static/index.js.map +1 -0
  24. package/dist/type-guards.d.ts +21 -0
  25. package/dist/type-guards.d.ts.map +1 -0
  26. package/dist/type-guards.js +216 -0
  27. package/dist/type-guards.js.map +1 -0
  28. package/dist/types.d.ts +17 -2
  29. package/dist/types.d.ts.map +1 -1
  30. package/dist/validation.d.ts +26 -0
  31. package/dist/validation.d.ts.map +1 -0
  32. package/dist/validation.js +104 -0
  33. package/dist/validation.js.map +1 -0
  34. package/dist/worker-template/code-transforms.d.ts +9 -0
  35. package/dist/worker-template/code-transforms.d.ts.map +1 -0
  36. package/dist/worker-template/code-transforms.js +28 -0
  37. package/dist/worker-template/code-transforms.js.map +1 -0
  38. package/{src/worker-template.d.ts → dist/worker-template/core.d.ts} +7 -15
  39. package/dist/worker-template/core.d.ts.map +1 -0
  40. package/dist/worker-template/core.js +502 -0
  41. package/dist/worker-template/core.js.map +1 -0
  42. package/dist/worker-template/helpers.d.ts +14 -0
  43. package/dist/worker-template/helpers.d.ts.map +1 -0
  44. package/dist/worker-template/helpers.js +79 -0
  45. package/dist/worker-template/helpers.js.map +1 -0
  46. package/dist/worker-template/index.d.ts +14 -0
  47. package/dist/worker-template/index.d.ts.map +1 -0
  48. package/dist/worker-template/index.js +19 -0
  49. package/dist/worker-template/index.js.map +1 -0
  50. package/dist/worker-template/sdk-generator.d.ts +17 -0
  51. package/dist/worker-template/sdk-generator.d.ts.map +1 -0
  52. package/{src/worker-template.js → dist/worker-template/sdk-generator.js} +377 -1506
  53. package/dist/worker-template/sdk-generator.js.map +1 -0
  54. package/dist/worker-template/test-generator.d.ts +16 -0
  55. package/dist/worker-template/test-generator.d.ts.map +1 -0
  56. package/dist/worker-template/test-generator.js +357 -0
  57. package/dist/worker-template/test-generator.js.map +1 -0
  58. package/dist/worker-template.d.ts +2 -2
  59. package/dist/worker-template.d.ts.map +1 -1
  60. package/dist/worker-template.js +22 -10
  61. package/dist/worker-template.js.map +1 -1
  62. package/package.json +17 -6
  63. package/public/capnweb.mjs +220 -0
  64. package/public/index.mjs +426 -0
  65. package/public/scaffold.mjs +198 -0
  66. package/.turbo/turbo-build.log +0 -4
  67. package/.turbo/turbo-test.log +0 -54
  68. package/.turbo/turbo-typecheck.log +0 -4
  69. package/CHANGELOG.md +0 -48
  70. package/example/package.json +0 -20
  71. package/example/src/index.ts +0 -214
  72. package/example/wrangler.jsonc +0 -25
  73. package/src/capnweb-bundle.ts +0 -2596
  74. package/src/evaluate.js +0 -187
  75. package/src/evaluate.ts +0 -325
  76. package/src/index.js +0 -10
  77. package/src/index.ts +0 -21
  78. package/src/node.d.ts +0 -17
  79. package/src/node.d.ts.map +0 -1
  80. package/src/node.js +0 -168
  81. package/src/node.js.map +0 -1
  82. package/src/node.ts +0 -200
  83. package/src/repl.ts +0 -228
  84. package/src/types.d.ts +0 -172
  85. package/src/types.d.ts.map +0 -1
  86. package/src/types.js +0 -4
  87. package/src/types.js.map +0 -1
  88. package/src/types.ts +0 -180
  89. package/src/worker-template.d.ts.map +0 -1
  90. package/src/worker-template.js.map +0 -1
  91. package/src/worker-template.ts +0 -3806
  92. package/test/evaluate-extended.test.js +0 -429
  93. package/test/evaluate-extended.test.ts +0 -469
  94. package/test/evaluate.test.js +0 -235
  95. package/test/evaluate.test.ts +0 -253
  96. package/test/index.test.js +0 -77
  97. package/test/index.test.ts +0 -95
  98. package/test/worker-template.test.js +0 -365
  99. package/test/worker-template.test.ts +0 -430
  100. package/tsconfig.json +0 -22
  101. package/vitest.config.js +0 -21
  102. package/vitest.config.ts +0 -28
@@ -1,3806 +0,0 @@
1
- /**
2
- * Worker template for sandbox execution
3
- *
4
- * This code is stringified and sent to the worker loader.
5
- * It uses the TEST service binding (ai-tests) for assertions and test running.
6
- *
7
- * The user's code (module, tests, script) is embedded directly into
8
- * the worker source - no eval() or new Function() needed. The security
9
- * comes from running in an isolated V8 context via worker_loaders.
10
- *
11
- * Routes:
12
- * - POST /execute - Run tests and scripts, return results
13
- * - POST /rpc or WebSocket upgrade - capnweb RPC to module exports
14
- * - GET / - Return info about available exports
15
- */
16
-
17
- import type { SDKConfig } from './types.js'
18
-
19
- /**
20
- * Generate SDK code for injection into sandbox
21
- *
22
- * Supports two modes:
23
- * - local: In-memory implementations (for testing without network)
24
- * - remote: RPC-based implementations (for production/integration tests)
25
- */
26
- function generateSDKCode(config: SDKConfig = {}): string {
27
- // Use local mode by default for sandboxed execution
28
- if (config.context === 'remote') {
29
- return generateRemoteSDKCode(config)
30
- }
31
- return generateLocalSDKCode(config)
32
- }
33
-
34
- /**
35
- * Generate local SDK code with in-memory implementations
36
- *
37
- * Implements APIs that align with ai-database (MemoryDB) and ai-workflows:
38
- * - MongoDB-style query operators ($gt, $gte, $lt, $in, $regex, etc.)
39
- * - URL resolution and identifier parsing
40
- * - upsert, generate, forEach methods
41
- * - Typed collection accessors (db.Users.find(), etc.)
42
- * - Workflow event/schedule patterns
43
- */
44
- function generateLocalSDKCode(config: SDKConfig = {}): string {
45
- const ns = config.ns || 'default'
46
- const aiGatewayUrl = config.aiGatewayUrl || ''
47
- const aiGatewayToken = config.aiGatewayToken || ''
48
-
49
- return `
50
- // ============================================================
51
- // Local SDK - In-memory implementation (aligned with ai-database/ai-workflows)
52
- // ============================================================
53
-
54
- const __SDK_CONFIG__ = {
55
- ns: '${ns}',
56
- aiGatewayUrl: '${aiGatewayUrl}',
57
- aiGatewayToken: '${aiGatewayToken}'
58
- };
59
-
60
- // In-memory database storage (mirrors MemoryDB structure)
61
- const __db_things__ = new Map();
62
- const __db_relationships__ = new Map();
63
- // Indexes for efficient lookups
64
- const __db_byUrl__ = new Map();
65
- const __db_byNsType__ = new Map();
66
- const __db_relFrom__ = new Map();
67
- const __db_relTo__ = new Map();
68
-
69
- // ID generator (crypto.randomUUID not available in all environments)
70
- const __generateId__ = () => {
71
- const bytes = new Uint8Array(16);
72
- if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
73
- crypto.getRandomValues(bytes);
74
- } else {
75
- for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
76
- }
77
- bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
78
- bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant
79
- const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
80
- return hex.slice(0, 8) + '-' + hex.slice(8, 12) + '-' + hex.slice(12, 16) + '-' + hex.slice(16, 20) + '-' + hex.slice(20);
81
- };
82
-
83
- // URL resolution (mirrors ai-database resolveUrl)
84
- const __resolveUrl__ = (entity) => {
85
- if (entity.url) return entity.url;
86
- return 'https://' + entity.ns + '/' + entity.type + '/' + entity.id;
87
- };
88
-
89
- // Parse identifier (mirrors ai-database parseIdentifier)
90
- const __parseIdentifier__ = (identifier, defaults = {}) => {
91
- if (identifier.includes('://')) {
92
- try {
93
- const parsed = new URL(identifier);
94
- const parts = parsed.pathname.split('/').filter(Boolean);
95
- return {
96
- ns: parsed.host,
97
- type: parts[0] || '',
98
- id: parts.slice(1).join('/') || '',
99
- url: identifier
100
- };
101
- } catch { return { ns: defaults.ns, id: identifier }; }
102
- }
103
- if (identifier.includes('/')) {
104
- const parts = identifier.split('/');
105
- return { ns: defaults.ns, type: parts[0], id: parts.slice(1).join('/') };
106
- }
107
- return { ns: defaults.ns, type: defaults.type, id: identifier };
108
- };
109
-
110
- // Extract mdxld metadata from data object
111
- const __extractType__ = (data) => data && (data.$type || data['@type']);
112
- const __extractId__ = (data) => data && (data.$id || data['@id']);
113
- const __extractContext__ = (data) => data && (data.$context || data['@context']);
114
-
115
- // Index management
116
- const __indexThing__ = (thing) => {
117
- const url = __resolveUrl__(thing);
118
- __db_byUrl__.set(url, thing);
119
- const nsTypeKey = thing.ns + '/' + thing.type;
120
- if (!__db_byNsType__.has(nsTypeKey)) __db_byNsType__.set(nsTypeKey, new Set());
121
- __db_byNsType__.get(nsTypeKey).add(url);
122
- };
123
- const __unindexThing__ = (thing) => {
124
- const url = __resolveUrl__(thing);
125
- __db_byUrl__.delete(url);
126
- const nsTypeKey = thing.ns + '/' + thing.type;
127
- const set = __db_byNsType__.get(nsTypeKey);
128
- if (set) set.delete(url);
129
- };
130
- const __indexRelationship__ = (rel) => {
131
- if (!__db_relFrom__.has(rel.from)) __db_relFrom__.set(rel.from, new Set());
132
- __db_relFrom__.get(rel.from).add(rel.id);
133
- if (!__db_relTo__.has(rel.to)) __db_relTo__.set(rel.to, new Set());
134
- __db_relTo__.get(rel.to).add(rel.id);
135
- };
136
- const __unindexRelationship__ = (rel) => {
137
- const fromSet = __db_relFrom__.get(rel.from);
138
- if (fromSet) fromSet.delete(rel.id);
139
- const toSet = __db_relTo__.get(rel.to);
140
- if (toSet) toSet.delete(rel.id);
141
- };
142
-
143
- // MongoDB-style query matching (mirrors MemoryDB.matchesQuery)
144
- const __matchesQuery__ = (thing, options) => {
145
- if (options.ns && thing.ns !== options.ns) return false;
146
- if (options.type && thing.type !== options.type) return false;
147
- if (options.where) {
148
- for (const [key, value] of Object.entries(options.where)) {
149
- const thingValue = thing.data[key];
150
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
151
- for (const [op, opVal] of Object.entries(value)) {
152
- switch (op) {
153
- case '$gt': if (!(thingValue > opVal)) return false; break;
154
- case '$gte': if (!(thingValue >= opVal)) return false; break;
155
- case '$lt': if (!(thingValue < opVal)) return false; break;
156
- case '$lte': if (!(thingValue <= opVal)) return false; break;
157
- case '$ne': if (thingValue === opVal) return false; break;
158
- case '$in': if (!Array.isArray(opVal) || !opVal.includes(thingValue)) return false; break;
159
- case '$nin': if (Array.isArray(opVal) && opVal.includes(thingValue)) return false; break;
160
- case '$exists': if (opVal && thingValue === undefined) return false; if (!opVal && thingValue !== undefined) return false; break;
161
- case '$regex': if (typeof thingValue !== 'string') return false; const regex = typeof opVal === 'string' ? new RegExp(opVal) : opVal; if (!regex.test(thingValue)) return false; break;
162
- default: if (thingValue !== value) return false;
163
- }
164
- }
165
- } else if (thingValue !== value) return false;
166
- }
167
- }
168
- return true;
169
- };
170
-
171
- // Apply query options (sort, limit, offset)
172
- const __applyQueryOptions__ = (items, options) => {
173
- let result = [...items];
174
- if (options.orderBy) {
175
- const field = options.orderBy;
176
- const dir = options.order === 'desc' ? -1 : 1;
177
- result.sort((a, b) => {
178
- const aVal = a[field] ?? a.data?.[field];
179
- const bVal = b[field] ?? b.data?.[field];
180
- if (aVal === undefined && bVal === undefined) return 0;
181
- if (aVal === undefined) return dir;
182
- if (bVal === undefined) return -dir;
183
- if (aVal < bVal) return -dir;
184
- if (aVal > bVal) return dir;
185
- return 0;
186
- });
187
- }
188
- if (options.offset) result = result.slice(options.offset);
189
- if (options.limit) result = result.slice(0, options.limit);
190
- return result;
191
- };
192
-
193
- // Cosine similarity helper for semantic search
194
- const __cosineSimilarity__ = (a, b) => {
195
- if (!a || !b || a.length !== b.length || a.length === 0) return 0;
196
- let dotProduct = 0, normA = 0, normB = 0;
197
- for (let i = 0; i < a.length; i++) {
198
- dotProduct += a[i] * b[i];
199
- normA += a[i] * a[i];
200
- normB += b[i] * b[i];
201
- }
202
- const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
203
- return magnitude === 0 ? 0 : dotProduct / magnitude;
204
- };
205
-
206
- // Embedding cache for semantic search (stores URL -> embedding)
207
- const __embeddings__ = new Map();
208
-
209
- // AI embed helper for semantic search - defined early so __db_core__.search can use it
210
- // Uses Gemini embedding model (768 dimensions) through AI Gateway
211
- const __aiEmbed__ = async (text) => {
212
- if (!__SDK_CONFIG__.aiGatewayUrl) return [];
213
- try {
214
- const url = __SDK_CONFIG__.aiGatewayUrl + '/google-ai-studio/v1beta/models/gemini-embedding-001:embedContent';
215
- const headers = { 'Content-Type': 'application/json' };
216
- if (__SDK_CONFIG__.aiGatewayToken) {
217
- headers['cf-aig-authorization'] = 'Bearer ' + __SDK_CONFIG__.aiGatewayToken;
218
- }
219
- const response = await fetch(url, {
220
- method: 'POST',
221
- headers,
222
- body: JSON.stringify({
223
- content: { parts: [{ text }] },
224
- outputDimensionality: 768
225
- })
226
- });
227
- if (!response.ok) return [];
228
- const result = await response.json();
229
- return result.embedding?.values || [];
230
- } catch (err) {
231
- console.warn('Embedding failed:', err.message);
232
- return [];
233
- }
234
- };
235
-
236
- // Local DB implementation (aligned with ai-database DBClient interface)
237
- const __db_core__ = {
238
- ns: __SDK_CONFIG__.ns,
239
-
240
- async list(options = {}) {
241
- const results = [];
242
- for (const thing of __db_things__.values()) {
243
- if (__matchesQuery__(thing, options)) results.push(thing);
244
- }
245
- return __applyQueryOptions__(results, options);
246
- },
247
-
248
- async find(options = {}) {
249
- return this.list(options);
250
- },
251
-
252
- async search(options = {}) {
253
- const query = (options.query || '').toLowerCase();
254
- const fields = options.fields || ['data'];
255
- const minScore = options.minScore || 0;
256
- const results = [];
257
-
258
- // Semantic search using embeddings
259
- if (options.semantic && typeof __aiEmbed__ === 'function') {
260
- const queryEmbedding = await __aiEmbed__(options.query || '');
261
- if (!queryEmbedding || queryEmbedding.length === 0) {
262
- // Embedding failed - return text-based results sorted by relevance
263
- // This handles cases where AI Gateway auth isn't configured
264
- console.warn('Semantic search: embeddings unavailable, using fuzzy text matching');
265
- const queryTerms = (options.query || '').toLowerCase().split(/\\s+/);
266
- const textResults = [];
267
- for (const thing of __db_things__.values()) {
268
- if (!__matchesQuery__(thing, options)) continue;
269
- const content = JSON.stringify(thing.data).toLowerCase();
270
- // Score based on how many query terms appear in the content
271
- let score = 0;
272
- for (const term of queryTerms) {
273
- if (content.includes(term)) score += 1;
274
- // Bonus for partial matches
275
- for (const word of content.split(/[\\s\\W]+/)) {
276
- if (word.includes(term) || term.includes(word)) score += 0.1;
277
- }
278
- }
279
- if (score > 0) textResults.push({ thing, score });
280
- }
281
- textResults.sort((a, b) => b.score - a.score);
282
- return __applyQueryOptions__(textResults.map(r => r.thing), options);
283
- }
284
-
285
- for (const thing of __db_things__.values()) {
286
- if (!__matchesQuery__(thing, options)) continue;
287
-
288
- // Get or compute embedding for this thing
289
- const thingUrl = thing.url || thing.id;
290
- let thingEmbedding = __embeddings__.get(thingUrl);
291
-
292
- if (!thingEmbedding) {
293
- const textContent = JSON.stringify(thing.data);
294
- thingEmbedding = await __aiEmbed__(textContent);
295
- if (thingEmbedding && thingEmbedding.length > 0) {
296
- __embeddings__.set(thingUrl, thingEmbedding);
297
- }
298
- }
299
-
300
- if (thingEmbedding && thingEmbedding.length > 0) {
301
- const score = __cosineSimilarity__(queryEmbedding, thingEmbedding);
302
- if (score >= minScore) results.push({ thing, score });
303
- }
304
- }
305
-
306
- results.sort((a, b) => b.score - a.score);
307
- return __applyQueryOptions__(results.map(r => r.thing), options);
308
- }
309
-
310
- // Text-based search
311
- for (const thing of __db_things__.values()) {
312
- if (!__matchesQuery__(thing, options)) continue;
313
- const searchIn = fields.includes('data')
314
- ? JSON.stringify(thing.data).toLowerCase()
315
- : fields.map(f => String(thing[f] || '')).join(' ').toLowerCase();
316
- if (searchIn.includes(query)) {
317
- const index = searchIn.indexOf(query);
318
- const score = 1 - (index / searchIn.length);
319
- if (score >= minScore) results.push({ thing, score });
320
- }
321
- }
322
- results.sort((a, b) => b.score - a.score);
323
- return __applyQueryOptions__(results.map(r => r.thing), options);
324
- },
325
-
326
- async get(identifier, options = {}) {
327
- let url;
328
- try {
329
- const parsed = __parseIdentifier__(identifier, { ns: __SDK_CONFIG__.ns });
330
- if (parsed.url) url = parsed.url;
331
- else if (parsed.ns && parsed.type && parsed.id) url = 'https://' + parsed.ns + '/' + parsed.type + '/' + parsed.id;
332
- else if (parsed.ns && parsed.id) {
333
- for (const [thingUrl, thing] of __db_byUrl__) {
334
- if (thing.ns === parsed.ns && thing.id === parsed.id) return thing;
335
- }
336
- }
337
- } catch { }
338
- if (url) {
339
- const thing = __db_byUrl__.get(url);
340
- if (thing) return thing;
341
- }
342
- // Try by ID across all things
343
- for (const thing of __db_things__.values()) {
344
- if (thing.id === identifier || thing.url === identifier) return thing;
345
- }
346
- // Handle create/generate options
347
- if (options.create || options.generate) {
348
- const parsed = __parseIdentifier__(identifier, { ns: __SDK_CONFIG__.ns });
349
- if (options.generate) return this.generate(identifier, typeof options.generate === 'object' ? options.generate : {});
350
- const data = typeof options.create === 'object' ? options.create : {};
351
- // Prioritize $type from data over URL-derived type
352
- const type = __extractType__(data) || parsed.type || 'Thing';
353
- const id = parsed.id || __extractId__(data) || __generateId__();
354
- return this.create({ ns: parsed.ns || __SDK_CONFIG__.ns, type, id, data });
355
- }
356
- return null;
357
- },
358
-
359
- async set(url, data) {
360
- const existing = __db_byUrl__.get(url);
361
- if (existing) {
362
- existing.data = data;
363
- existing.updatedAt = new Date();
364
- return existing;
365
- }
366
- const parsed = __parseIdentifier__(url, { ns: __SDK_CONFIG__.ns });
367
- const thing = {
368
- ns: parsed.ns || __SDK_CONFIG__.ns,
369
- type: parsed.type || '',
370
- id: parsed.id || __generateId__(),
371
- url,
372
- createdAt: new Date(),
373
- updatedAt: new Date(),
374
- data
375
- };
376
- __db_things__.set(url, thing);
377
- __indexThing__(thing);
378
- return thing;
379
- },
380
-
381
- async create(urlOrOptions, dataArg) {
382
- // URL-first syntax: create('https://...', { $type: 'Post', ... })
383
- if (typeof urlOrOptions === 'string') {
384
- const url = urlOrOptions;
385
- const data = dataArg || {};
386
- const parsed = __parseIdentifier__(url, { ns: __SDK_CONFIG__.ns });
387
- // Prioritize $type from data over URL-derived type
388
- const type = __extractType__(data) || parsed.type || 'Thing';
389
- const id = parsed.id || __extractId__(data) || __generateId__();
390
- const context = __extractContext__(data);
391
- const ns = parsed.ns || __SDK_CONFIG__.ns;
392
- const cleanData = { ...data };
393
- delete cleanData.$type; delete cleanData.$id; delete cleanData.$context;
394
- delete cleanData['@type']; delete cleanData['@id']; delete cleanData['@context'];
395
- const thingUrl = parsed.url || 'https://' + ns + '/' + type + '/' + id;
396
- if (__db_byUrl__.has(thingUrl)) throw new Error('Thing already exists: ' + thingUrl);
397
- const thing = { ns, type, id, url: thingUrl, createdAt: new Date(), updatedAt: new Date(), data: cleanData };
398
- if (context) thing['@context'] = context;
399
- __db_things__.set(thingUrl, thing);
400
- __indexThing__(thing);
401
- return thing;
402
- }
403
-
404
- const options = urlOrOptions;
405
-
406
- // Data-first syntax with $type: create({ $type: 'User', name: 'Alice', ... })
407
- // Detect by presence of $type or @type and absence of 'data' property
408
- if ((__extractType__(options) && !('data' in options)) || ('$type' in options) || ('@type' in options)) {
409
- const type = __extractType__(options) || 'Thing';
410
- const id = __extractId__(options) || __generateId__();
411
- const context = __extractContext__(options);
412
- const ns = __SDK_CONFIG__.ns;
413
- const cleanData = { ...options };
414
- delete cleanData.$type; delete cleanData.$id; delete cleanData.$context;
415
- delete cleanData['@type']; delete cleanData['@id']; delete cleanData['@context'];
416
- const thingUrl = 'https://' + ns + '/' + type + '/' + id;
417
- if (__db_byUrl__.has(thingUrl)) throw new Error('Thing already exists: ' + thingUrl);
418
- const thing = { ns, type, id, url: thingUrl, createdAt: new Date(), updatedAt: new Date(), data: cleanData };
419
- if (context) thing['@context'] = context;
420
- __db_things__.set(thingUrl, thing);
421
- __indexThing__(thing);
422
- return thing;
423
- }
424
-
425
- // Options syntax: create({ ns, type, data })
426
- const id = options.id || __generateId__();
427
- const thingUrl = options.url || 'https://' + options.ns + '/' + options.type + '/' + id;
428
- if (__db_byUrl__.has(thingUrl)) throw new Error('Thing already exists: ' + thingUrl);
429
- const thing = { ns: options.ns, type: options.type, id, url: thingUrl, createdAt: new Date(), updatedAt: new Date(), data: options.data };
430
- if (options['@context']) thing['@context'] = options['@context'];
431
- __db_things__.set(thingUrl, thing);
432
- __indexThing__(thing);
433
- return thing;
434
- },
435
-
436
- async update(url, options) {
437
- const parsed = __parseIdentifier__(url, { ns: __SDK_CONFIG__.ns });
438
- const resolvedUrl = parsed.url || 'https://' + parsed.ns + '/' + parsed.type + '/' + parsed.id;
439
- const existing = __db_byUrl__.get(resolvedUrl);
440
- if (!existing) throw new Error('Thing not found: ' + resolvedUrl);
441
- existing.data = { ...existing.data, ...options.data };
442
- existing.updatedAt = new Date();
443
- return existing;
444
- },
445
-
446
- async upsert(urlOrOptions, dataArg) {
447
- if (typeof urlOrOptions === 'string') {
448
- const parsed = __parseIdentifier__(urlOrOptions, { ns: __SDK_CONFIG__.ns });
449
- const resolvedUrl = parsed.url || (parsed.ns && parsed.type && parsed.id ? 'https://' + parsed.ns + '/' + parsed.type + '/' + parsed.id : null);
450
- if (resolvedUrl && __db_byUrl__.has(resolvedUrl)) {
451
- return this.update(resolvedUrl, { data: dataArg || {} });
452
- }
453
- return this.create(urlOrOptions, dataArg);
454
- }
455
- const options = urlOrOptions;
456
- const url = options.url || 'https://' + options.ns + '/' + options.type + '/' + (options.id || __generateId__());
457
- if (__db_byUrl__.has(url)) return this.update(url, { data: options.data });
458
- return this.create({ ...options, url });
459
- },
460
-
461
- async delete(url) {
462
- const parsed = __parseIdentifier__(url, { ns: __SDK_CONFIG__.ns });
463
- let resolvedUrl = parsed.url;
464
- if (!resolvedUrl && parsed.ns && parsed.type && parsed.id) {
465
- resolvedUrl = 'https://' + parsed.ns + '/' + parsed.type + '/' + parsed.id;
466
- }
467
- if (!resolvedUrl) return false;
468
- const thing = __db_byUrl__.get(resolvedUrl);
469
- if (!thing) return false;
470
- __unindexThing__(thing);
471
- __db_things__.delete(resolvedUrl);
472
- // Delete related relationships
473
- const relIds = new Set([...(__db_relFrom__.get(resolvedUrl) || []), ...(__db_relTo__.get(resolvedUrl) || [])]);
474
- for (const relId of relIds) {
475
- const rel = __db_relationships__.get(relId);
476
- if (rel) { __unindexRelationship__(rel); __db_relationships__.delete(relId); }
477
- }
478
- return true;
479
- },
480
-
481
- async generate(identifier, options = {}) {
482
- const parsed = __parseIdentifier__(identifier, { ns: __SDK_CONFIG__.ns });
483
- const type = parsed.type || 'Thing';
484
- const id = parsed.id || __generateId__();
485
- const thing = {
486
- ns: parsed.ns || __SDK_CONFIG__.ns, type, id,
487
- url: parsed.url || 'https://' + (parsed.ns || __SDK_CONFIG__.ns) + '/' + type + '/' + id,
488
- createdAt: new Date(), updatedAt: new Date(),
489
- data: { _generated: true, _prompt: options.prompt, _model: options.model }
490
- };
491
- __db_things__.set(thing.url, thing);
492
- __indexThing__(thing);
493
- return thing;
494
- },
495
-
496
- async forEach(options, callback) {
497
- const things = await this.list(options);
498
- for (const thing of things) await callback(thing);
499
- },
500
-
501
- async relate(options) {
502
- const id = __generateId__();
503
- const rel = { id, type: options.type, from: options.from, to: options.to, createdAt: new Date(), data: options.data };
504
- __db_relationships__.set(id, rel);
505
- __indexRelationship__(rel);
506
- return rel;
507
- },
508
-
509
- async unrelate(from, type, to) {
510
- for (const [id, rel] of __db_relationships__) {
511
- if (rel.from === from && rel.type === type && rel.to === to) {
512
- __unindexRelationship__(rel);
513
- __db_relationships__.delete(id);
514
- return true;
515
- }
516
- }
517
- return false;
518
- },
519
-
520
- async related(url, relationshipType, direction = 'both') {
521
- const parsed = __parseIdentifier__(url, { ns: __SDK_CONFIG__.ns });
522
- const resolvedUrl = parsed.url || 'https://' + parsed.ns + '/' + parsed.type + '/' + parsed.id;
523
- const relatedUrls = new Set();
524
- if (direction === 'from' || direction === 'both') {
525
- const fromRels = __db_relFrom__.get(resolvedUrl);
526
- if (fromRels) {
527
- for (const relId of fromRels) {
528
- const rel = __db_relationships__.get(relId);
529
- if (rel && (!relationshipType || rel.type === relationshipType)) relatedUrls.add(rel.to);
530
- }
531
- }
532
- }
533
- if (direction === 'to' || direction === 'both') {
534
- const toRels = __db_relTo__.get(resolvedUrl);
535
- if (toRels) {
536
- for (const relId of toRels) {
537
- const rel = __db_relationships__.get(relId);
538
- if (rel && (!relationshipType || rel.type === relationshipType)) relatedUrls.add(rel.from);
539
- }
540
- }
541
- }
542
- const results = [];
543
- for (const relatedUrl of relatedUrls) {
544
- const thing = __db_byUrl__.get(relatedUrl);
545
- if (thing) results.push(thing);
546
- }
547
- return results;
548
- },
549
-
550
- async relationships(url, type, direction = 'both') {
551
- const parsed = __parseIdentifier__(url, { ns: __SDK_CONFIG__.ns });
552
- const resolvedUrl = parsed.url || 'https://' + parsed.ns + '/' + parsed.type + '/' + parsed.id;
553
- const results = [];
554
- if (direction === 'from' || direction === 'both') {
555
- const fromRels = __db_relFrom__.get(resolvedUrl);
556
- if (fromRels) {
557
- for (const relId of fromRels) {
558
- const rel = __db_relationships__.get(relId);
559
- if (rel && (!type || rel.type === type)) results.push(rel);
560
- }
561
- }
562
- }
563
- if (direction === 'to' || direction === 'both') {
564
- const toRels = __db_relTo__.get(resolvedUrl);
565
- if (toRels) {
566
- for (const relId of toRels) {
567
- const rel = __db_relationships__.get(relId);
568
- if (rel && (!type || rel.type === type)) results.push(rel);
569
- }
570
- }
571
- }
572
- return results;
573
- },
574
-
575
- clear() {
576
- __db_things__.clear(); __db_relationships__.clear();
577
- __db_byUrl__.clear(); __db_byNsType__.clear();
578
- __db_relFrom__.clear(); __db_relTo__.clear();
579
- },
580
-
581
- stats() {
582
- return { things: __db_things__.size, relationships: __db_relationships__.size };
583
- }
584
- };
585
-
586
- // Typed collection accessor (db.Users, db.Posts, etc.) - mirrors ai-database TypedDBOperations
587
- const db = new Proxy(__db_core__, {
588
- get: (target, prop) => {
589
- if (prop in target) return target[prop];
590
- if (prop === 'then' || prop === 'catch' || prop === 'finally') return undefined;
591
- // Return a collection accessor for the type
592
- const type = String(prop);
593
- const collectionNs = __SDK_CONFIG__.ns;
594
- const makeUrl = (id) => 'https://' + collectionNs + '/' + type + '/' + id;
595
- return {
596
- async list(options = {}) {
597
- return __db_core__.list({ ...options, type, ns: collectionNs });
598
- },
599
- async find(options = {}) {
600
- return __db_core__.find({ ...options, type, ns: collectionNs });
601
- },
602
- async search(options) {
603
- return __db_core__.search({ ...options, type, ns: collectionNs });
604
- },
605
- async get(id, options = {}) {
606
- return __db_core__.get(makeUrl(id), options);
607
- },
608
- async create(idOrData, data) {
609
- if (typeof idOrData === 'string') {
610
- return __db_core__.create({ ns: collectionNs, type, id: idOrData, data: data || {} });
611
- }
612
- const extractedId = __extractId__(idOrData);
613
- return __db_core__.create({ ns: collectionNs, type, id: extractedId || __generateId__(), data: idOrData });
614
- },
615
- async update(id, data) {
616
- return __db_core__.update(makeUrl(id), { data });
617
- },
618
- async upsert(idOrData, data) {
619
- if (typeof idOrData === 'string') {
620
- return __db_core__.upsert({ ns: collectionNs, type, id: idOrData, data: data || {} });
621
- }
622
- const extractedId = __extractId__(idOrData);
623
- return __db_core__.upsert({ ns: collectionNs, type, id: extractedId || __generateId__(), data: idOrData });
624
- },
625
- async delete(id) {
626
- return __db_core__.delete(makeUrl(id));
627
- },
628
- async forEach(optionsOrCallback, callback) {
629
- if (typeof optionsOrCallback === 'function') {
630
- return __db_core__.forEach({ type, ns: collectionNs }, optionsOrCallback);
631
- }
632
- return __db_core__.forEach({ ...optionsOrCallback, type, ns: collectionNs }, callback);
633
- }
634
- };
635
- }
636
- });
637
-
638
- // AI Gateway client - makes real API calls through Cloudflare AI Gateway
639
- // When not configured, returns mock data for testing
640
- const __aiGateway__ = {
641
- async fetch(provider, endpoint, body, extraHeaders = {}) {
642
- // Mock mode when AI Gateway not configured (for testing)
643
- if (!__SDK_CONFIG__.aiGatewayUrl) {
644
- // Extract prompt from various request formats
645
- let prompt = '';
646
- if (body?.messages?.[0]?.content?.[0]?.text) {
647
- prompt = body.messages[0].content[0].text;
648
- } else if (body?.content?.parts?.[0]?.text) {
649
- prompt = body.content.parts[0].text;
650
- }
651
-
652
- // Return mock responses based on endpoint type
653
- if (endpoint.includes('converse')) {
654
- // Mock text generation response
655
- return {
656
- output: {
657
- message: {
658
- content: [{ text: 'Mock AI response to: ' + prompt.slice(0, 50) }]
659
- }
660
- },
661
- usage: { inputTokens: 10, outputTokens: 20 }
662
- };
663
- }
664
- if (endpoint.includes('embed')) {
665
- // Mock embedding response
666
- return {
667
- embedding: {
668
- values: new Array(body.outputDimensionality || 768).fill(0).map(() => Math.random())
669
- }
670
- };
671
- }
672
- // Default mock response
673
- return { mock: true, prompt };
674
- }
675
-
676
- const url = __SDK_CONFIG__.aiGatewayUrl + '/' + provider + endpoint;
677
- const headers = {
678
- 'Content-Type': 'application/json',
679
- ...extraHeaders
680
- };
681
- if (__SDK_CONFIG__.aiGatewayToken) {
682
- // Use cf-aig-authorization header for AI Gateway with stored credentials
683
- headers['cf-aig-authorization'] = 'Bearer ' + __SDK_CONFIG__.aiGatewayToken;
684
- }
685
- const response = await fetch(url, {
686
- method: 'POST',
687
- headers,
688
- body: JSON.stringify(body)
689
- });
690
- if (!response.ok) {
691
- const error = await response.text();
692
- throw new Error('AI Gateway error: ' + response.status + ' ' + error);
693
- }
694
- return response.json();
695
- }
696
- };
697
-
698
- // AI implementation - callable as function or via methods
699
- // Supports: ai('prompt'), ai\`template\`, ai.generate(), ai.embed(), etc.
700
- const __aiMethods__ = {
701
- async generate(prompt, options = {}) {
702
- // Default to Claude 4.5 Opus via AWS Bedrock
703
- const model = options.model || 'anthropic.claude-opus-4-5-20251101-v1:0';
704
- const result = await __aiGateway__.fetch('aws-bedrock', '/model/' + model + '/converse', {
705
- messages: [{ role: 'user', content: [{ text: prompt }] }],
706
- inferenceConfig: { maxTokens: options.maxTokens || 1024 }
707
- });
708
- const text = result.output?.message?.content?.[0]?.text || '';
709
- return { text, model, usage: result.usage };
710
- },
711
- async embed(text, options = {}) {
712
- // Use Gemini embedding model (768 dimensions) via AI Gateway
713
- const dimensions = options.dimensions || 768;
714
- const result = await __aiGateway__.fetch('google-ai-studio', '/v1beta/models/gemini-embedding-001:embedContent', {
715
- content: { parts: [{ text }] },
716
- outputDimensionality: dimensions
717
- });
718
- return result.embedding?.values || [];
719
- },
720
- async embedMany(texts, options = {}) {
721
- const dimensions = options.dimensions || 768;
722
- const embeddings = [];
723
- for (const text of texts) {
724
- const result = await __aiGateway__.fetch('google-ai-studio', '/v1beta/models/gemini-embedding-001:embedContent', {
725
- content: { parts: [{ text }] },
726
- outputDimensionality: dimensions
727
- });
728
- embeddings.push(result.embedding?.values || []);
729
- }
730
- return embeddings;
731
- },
732
- async chat(messages, options = {}) {
733
- // Default to Claude 4.5 Opus via AWS Bedrock
734
- const model = options.model || 'anthropic.claude-opus-4-5-20251101-v1:0';
735
- const result = await __aiGateway__.fetch('aws-bedrock', '/model/' + model + '/converse', {
736
- messages: messages.map(m => ({ role: m.role, content: [{ text: m.content }] })),
737
- inferenceConfig: { maxTokens: options.maxTokens || 1024 }
738
- });
739
- const content = result.output?.message?.content?.[0]?.text || '';
740
- return { role: 'assistant', content, usage: result.usage };
741
- },
742
- async complete(prompt, options = {}) {
743
- const result = await ai.generate(prompt, options);
744
- return result.text;
745
- },
746
- async classify(text, labels, options = {}) {
747
- const prompt = 'Classify the following text into one of these categories: ' + labels.join(', ') + '\\n\\nText: ' + text + '\\n\\nRespond with just the category name.';
748
- const result = await ai.generate(prompt, { ...options, maxTokens: 50 });
749
- const label = labels.find(l => result.text.toLowerCase().includes(l.toLowerCase())) || labels[0];
750
- return { label, confidence: 0.9 };
751
- },
752
- async extract(text, schema, options = {}) {
753
- const prompt = 'Extract the following information from the text and return as JSON:\\n\\nSchema: ' + JSON.stringify(schema) + '\\n\\nText: ' + text + '\\n\\nRespond with valid JSON only.';
754
- const result = await ai.generate(prompt, options);
755
- try {
756
- return JSON.parse(result.text);
757
- } catch {
758
- return { _extracted: true, raw: result.text };
759
- }
760
- },
761
- async summarize(text, options = {}) {
762
- const prompt = 'Summarize the following text concisely:\\n\\n' + text;
763
- const result = await ai.generate(prompt, { ...options, maxTokens: options.maxTokens || 256 });
764
- return result.text;
765
- },
766
- // Create database-aware AI tools (returns array for Claude SDK compatibility)
767
- createDatabaseTools(database) {
768
- const dbInstance = database || __db_core__;
769
-
770
- // Helper for success response in Claude SDK format
771
- const success = (data) => ({
772
- content: [{ type: 'text', text: JSON.stringify(data) }]
773
- });
774
-
775
- // Helper for error response
776
- const error = (message) => ({
777
- content: [{ type: 'text', text: message }],
778
- isError: true
779
- });
780
-
781
- return [
782
- {
783
- name: 'mdxdb_list',
784
- description: 'List documents from the database by type. Returns an array of documents.',
785
- handler: async (args) => {
786
- try {
787
- const { type, prefix, limit = 100 } = args || {};
788
- const result = await dbInstance.list({ type, prefix, limit });
789
- return success(result);
790
- } catch (err) {
791
- return error('Failed to list documents: ' + (err.message || String(err)));
792
- }
793
- }
794
- },
795
- {
796
- name: 'mdxdb_search',
797
- description: 'Search for documents by query. Supports semantic search when enabled.',
798
- handler: async (args) => {
799
- try {
800
- const { query, type, limit = 10, semantic = false } = args || {};
801
- const result = await dbInstance.search({ query, type, limit, semantic });
802
- return success(result);
803
- } catch (err) {
804
- return error('Failed to search documents: ' + (err.message || String(err)));
805
- }
806
- }
807
- },
808
- {
809
- name: 'mdxdb_get',
810
- description: 'Get a specific document by ID. Returns the document or null if not found.',
811
- handler: async (args) => {
812
- try {
813
- const { id, url } = args || {};
814
- const identifier = url || id;
815
- if (!identifier) {
816
- return error('Either id or url is required');
817
- }
818
- const doc = await dbInstance.get(identifier);
819
- if (!doc) {
820
- return error('Document not found: ' + identifier);
821
- }
822
- return success(doc);
823
- } catch (err) {
824
- return error('Failed to get document: ' + (err.message || String(err)));
825
- }
826
- }
827
- },
828
- {
829
- name: 'mdxdb_set',
830
- description: 'Create or update a document. Returns success status.',
831
- handler: async (args) => {
832
- try {
833
- const { id, url, data, content, type } = args || {};
834
- const identifier = url || id;
835
- if (!identifier) {
836
- return error('Either id or url is required');
837
- }
838
- // Set data directly - the db.set wraps it in a thing.data property
839
- // Also include type metadata if provided
840
- const docData = { ...(data || {}), ...(type ? { $type: type } : {}) };
841
- await dbInstance.set(identifier, docData);
842
- return success({ success: true, id: identifier });
843
- } catch (err) {
844
- return error('Failed to set document: ' + (err.message || String(err)));
845
- }
846
- }
847
- },
848
- {
849
- name: 'mdxdb_delete',
850
- description: 'Delete a document by ID. Returns deletion status.',
851
- handler: async (args) => {
852
- try {
853
- const { id, url } = args || {};
854
- const identifier = url || id;
855
- if (!identifier) {
856
- return error('Either id or url is required');
857
- }
858
- const result = await dbInstance.delete(identifier);
859
- return success({ deleted: result.deleted !== false });
860
- } catch (err) {
861
- return error('Failed to delete document: ' + (err.message || String(err)));
862
- }
863
- }
864
- }
865
- ];
866
- }
867
- };
868
-
869
- // Create callable ai function that also has methods as properties
870
- // Supports: ai('prompt'), ai\`template\`, ai.generate(), ai.embed(), etc.
871
- const ai = Object.assign(
872
- async function ai(promptOrStrings, ...values) {
873
- // Handle template literal: ai\`Hello \${name}\`
874
- if (Array.isArray(promptOrStrings) && 'raw' in promptOrStrings) {
875
- const prompt = promptOrStrings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '');
876
- const result = await __aiMethods__.generate(prompt);
877
- return result.text || result;
878
- }
879
- // Handle direct call: ai('prompt') or ai('prompt', options)
880
- const prompt = promptOrStrings;
881
- const options = values[0] || {};
882
- const result = await __aiMethods__.generate(prompt, options);
883
- return result.text || result;
884
- },
885
- __aiMethods__
886
- );
887
-
888
- // Add references method to db_core
889
- __db_core__.references = async function(url, direction = 'both') {
890
- const rels = await this.relationships(url, undefined, direction);
891
- const refs = [];
892
- for (const rel of rels) {
893
- const targetUrl = direction === 'from' ? rel.to : direction === 'to' ? rel.from : (rel.from === url ? rel.to : rel.from);
894
- const target = await this.get(targetUrl);
895
- if (target) refs.push({ ...target, relationship: rel });
896
- }
897
- return refs;
898
- };
899
-
900
- // ============================================================
901
- // Hono-compatible HTTP App (for testing)
902
- // ============================================================
903
-
904
- // Cache for client component detection to avoid recursion
905
- const __clientComponentCache__ = new WeakMap();
906
-
907
- // Check if a function is a client component
908
- const __isClientComponent__ = (fn) => {
909
- if (typeof fn !== 'function') return false;
910
- // Check cache first
911
- if (__clientComponentCache__.has(fn)) {
912
- return __clientComponentCache__.get(fn);
913
- }
914
- // Mark as processing to prevent recursion
915
- __clientComponentCache__.set(fn, false);
916
-
917
- const source = fn.toString();
918
- let result = false;
919
-
920
- // Check for 'use client' directive
921
- if (source.includes("'use client'") || source.includes('"use client"')) {
922
- result = true;
923
- }
924
- // Check for 'use server' directive (explicitly server)
925
- else if (source.includes("'use server'") || source.includes('"use server"')) {
926
- result = false;
927
- }
928
- // Auto-detect: functions using useState are client components
929
- else if (source.includes('useState(') || source.includes('useEffect(') || source.includes('useRef(')) {
930
- result = true;
931
- }
932
-
933
- __clientComponentCache__.set(fn, result);
934
- return result;
935
- };
936
-
937
- // Simple JSX renderer for Hono JSX components
938
- const __renderJsx__ = (element) => {
939
- if (element === null || element === undefined) return '';
940
- if (typeof element === 'string' || typeof element === 'number') return String(element);
941
- if (Array.isArray(element)) return element.map(__renderJsx__).join('');
942
- if (typeof element !== 'object') return String(element);
943
-
944
- const { type, props } = element;
945
- if (!type) return '';
946
-
947
- // Handle function components
948
- if (typeof type === 'function') {
949
- const isClient = __isClientComponent__(type);
950
- try {
951
- const result = type(props || {});
952
- const rendered = __renderJsx__(result);
953
- // Wrap client components with hydration marker
954
- if (isClient) {
955
- const componentName = type.name || 'Component';
956
- return '<div data-hono-hydrate="' + componentName + '">' + rendered + '</div><script>/* hydrate: ' + componentName + ' */</script>';
957
- }
958
- return rendered;
959
- } catch (e) {
960
- return '<!-- Error: ' + e.message + ' -->';
961
- }
962
- }
963
-
964
- // Handle HTML elements
965
- const tag = String(type);
966
- const attrs = Object.entries(props || {})
967
- .filter(([k, v]) => k !== 'children' && v !== undefined && v !== null && v !== false)
968
- .map(([k, v]) => {
969
- if (v === true) return k;
970
- const attrName = k === 'className' ? 'class' : k.replace(/([A-Z])/g, '-$1').toLowerCase();
971
- return attrName + '="' + String(v).replace(/"/g, '&quot;') + '"';
972
- })
973
- .join(' ');
974
-
975
- const children = props?.children;
976
- const childContent = Array.isArray(children)
977
- ? children.map(__renderJsx__).join('')
978
- : children !== undefined
979
- ? __renderJsx__(children)
980
- : '';
981
-
982
- const voidElements = new Set(['br', 'hr', 'img', 'input', 'link', 'meta', 'area', 'base', 'col', 'embed', 'param', 'source', 'track', 'wbr']);
983
- if (voidElements.has(tag.toLowerCase())) {
984
- return '<' + tag + (attrs ? ' ' + attrs : '') + ' />';
985
- }
986
- return '<' + tag + (attrs ? ' ' + attrs : '') + '>' + childContent + '</' + tag + '>';
987
- };
988
-
989
- const __createHonoApp__ = () => {
990
- const routes = [];
991
- const middleware = [];
992
- let notFoundHandler = null;
993
- let errorHandler = null;
994
-
995
- // Parse path pattern into regex and param names
996
- const parsePattern = (pattern) => {
997
- const params = [];
998
- // Normalize: remove trailing slash for consistent matching
999
- let normalizedPattern = pattern.replace(/\\/+$/, '') || '/';
1000
- let regexStr = normalizedPattern
1001
- .replace(/\\*$/, '(?<wildcard>.*)') // Wildcard at end
1002
- .replace(/\\/:([^/]+)\\?/g, (_, name) => { params.push(name); return '(?:/(?<' + name + '>[^/]*))?'; }) // Optional param (makes /segment optional)
1003
- .replace(/:([^/]+)/g, (_, name) => { params.push(name); return '(?<' + name + '>[^/]+)'; }); // Required param
1004
- // Handle trailing slash optionally
1005
- if (!regexStr.endsWith('.*') && !regexStr.endsWith(')?')) {
1006
- regexStr = regexStr + '/?';
1007
- }
1008
- return { regex: new RegExp('^' + regexStr + '(?:\\\\?.*)?$'), params };
1009
- };
1010
-
1011
- // Create context object
1012
- const createContext = (req, pathParams, store) => {
1013
- const url = new URL(req.url, 'http://localhost');
1014
- return {
1015
- req: {
1016
- raw: req,
1017
- url: req.url,
1018
- method: req.method,
1019
- path: url.pathname,
1020
- param: (name) => name === '*' ? pathParams.wildcard : pathParams[name],
1021
- query: (name) => url.searchParams.get(name),
1022
- queries: () => {
1023
- const result = {};
1024
- for (const [key, value] of url.searchParams) {
1025
- if (result[key]) {
1026
- if (Array.isArray(result[key])) result[key].push(value);
1027
- else result[key] = [result[key], value];
1028
- } else {
1029
- result[key] = value;
1030
- }
1031
- }
1032
- return result;
1033
- },
1034
- header: (name) => req.headers.get(name),
1035
- json: () => req.json(),
1036
- text: () => req.text(),
1037
- arrayBuffer: () => req.arrayBuffer(),
1038
- parseBody: async () => {
1039
- const contentType = req.headers.get('Content-Type') || '';
1040
- if (contentType.includes('application/json')) {
1041
- return req.json();
1042
- }
1043
- if (contentType.includes('multipart/form-data') || contentType.includes('application/x-www-form-urlencoded')) {
1044
- const formData = await req.formData();
1045
- const result = {};
1046
- for (const [key, value] of formData.entries()) {
1047
- result[key] = value;
1048
- }
1049
- return result;
1050
- }
1051
- return req.text();
1052
- },
1053
- },
1054
- // Response methods - text supports optional headers as 3rd param
1055
- text: (body, status = 200, headers = {}) => {
1056
- const h = { 'Content-Type': 'text/plain', ...headers };
1057
- return new Response(body, { status, headers: h });
1058
- },
1059
- json: (data, status = 200) => new Response(JSON.stringify(data), { status, headers: { 'Content-Type': 'application/json' } }),
1060
- html: (body, status = 200) => {
1061
- const rendered = typeof body === 'object' ? __renderJsx__(body) : body;
1062
- return new Response(rendered, { status, headers: { 'Content-Type': 'text/html' } });
1063
- },
1064
- body: (data, status = 200) => new Response(data, { status }),
1065
- redirect: (url, status = 302) => new Response(null, { status, headers: { 'Location': url } }),
1066
- notFound: () => new Response('Not Found', { status: 404 }),
1067
- // Streaming helper - returns Response immediately, callback runs async
1068
- stream: (callback) => {
1069
- const { readable, writable } = new TransformStream();
1070
- const writer = writable.getWriter();
1071
- const streamApi = {
1072
- write: async (chunk) => {
1073
- const data = typeof chunk === 'string' ? new TextEncoder().encode(chunk) : chunk;
1074
- await writer.write(data);
1075
- },
1076
- close: async () => await writer.close(),
1077
- pipe: async (rs) => {
1078
- const reader = rs.getReader();
1079
- while (true) {
1080
- const { done, value } = await reader.read();
1081
- if (done) break;
1082
- await writer.write(value);
1083
- }
1084
- await writer.close();
1085
- }
1086
- };
1087
- // Run callback async, close stream when done
1088
- Promise.resolve(callback(streamApi)).then(() => writer.close()).catch(() => writer.close());
1089
- return new Response(readable, { headers: { 'Content-Type': 'text/html; charset=utf-8' } });
1090
- },
1091
- // Status helper
1092
- status: (code) => {
1093
- const ctx = createContext(req, pathParams, store);
1094
- const originalJson = ctx.json;
1095
- const originalText = ctx.text;
1096
- const originalHtml = ctx.html;
1097
- ctx.json = (data) => originalJson(data, code);
1098
- ctx.text = (body) => originalText(body, code);
1099
- ctx.html = (body) => originalHtml(body, code);
1100
- return ctx;
1101
- },
1102
- // Header helper
1103
- header: (name, value) => {
1104
- store._headers = store._headers || {};
1105
- store._headers[name] = value;
1106
- },
1107
- // Store for middleware
1108
- set: (key, value) => { store[key] = value; },
1109
- get: (key) => store[key],
1110
- };
1111
- };
1112
-
1113
- // Register routes
1114
- const addRoute = (method, path, ...handlers) => {
1115
- const { regex, params } = parsePattern(path);
1116
- routes.push({ method: method.toUpperCase(), pattern: path, regex, params, handlers });
1117
- };
1118
-
1119
- // Process request through middleware and routes
1120
- const handleRequest = async (req) => {
1121
- const url = new URL(req.url, 'http://localhost');
1122
- const path = url.pathname;
1123
- const method = req.method;
1124
- const store = {};
1125
-
1126
- // Find matching route
1127
- let matchedRoute = null;
1128
- let routeParams = {};
1129
- for (const route of routes) {
1130
- if (route.method !== method && route.method !== 'ALL') continue;
1131
- const match = path.match(route.regex);
1132
- if (match) {
1133
- matchedRoute = route;
1134
- routeParams = match.groups || {};
1135
- break;
1136
- }
1137
- }
1138
-
1139
- // Collect matching middleware
1140
- const matchingMiddleware = middleware.filter(mw => {
1141
- const prefix = mw.prefix.replace('/*', '').replace('*', '');
1142
- return path.startsWith(prefix) || prefix === '';
1143
- });
1144
-
1145
- // Track the response through the chain so middleware can modify headers after next()
1146
- let chainResponse = null;
1147
-
1148
- // Create a proper execution chain: middleware -> route handlers
1149
- const executeChain = async (index) => {
1150
- // First, run through middleware
1151
- if (index < matchingMiddleware.length) {
1152
- const mw = matchingMiddleware[index];
1153
- const ctx = createContext(req, routeParams, store);
1154
- const next = async () => {
1155
- const downstreamResult = await executeChain(index + 1);
1156
- if (downstreamResult) chainResponse = downstreamResult;
1157
- return downstreamResult;
1158
- };
1159
- const result = await mw.handler(ctx, next);
1160
- // If middleware returns a response, use it; otherwise use chainResponse
1161
- if (result) return result;
1162
- return chainResponse;
1163
- }
1164
-
1165
- // Then run the route handler(s)
1166
- if (!matchedRoute) {
1167
- // No route matched - use 404 handler
1168
- if (notFoundHandler) {
1169
- const ctx = createContext(req, {}, store);
1170
- return notFoundHandler(ctx);
1171
- }
1172
- return new Response('Not Found', { status: 404 });
1173
- }
1174
-
1175
- const ctx = createContext(req, routeParams, store);
1176
- for (let i = 0; i < matchedRoute.handlers.length; i++) {
1177
- const handler = matchedRoute.handlers[i];
1178
- const isLast = i === matchedRoute.handlers.length - 1;
1179
- let nextCalled = false;
1180
- const next = async () => { nextCalled = true; };
1181
- const result = await handler(ctx, next);
1182
- if (result) return result;
1183
- if (!isLast && !nextCalled) break;
1184
- }
1185
- return null;
1186
- };
1187
-
1188
- // Helper to apply stored headers to response
1189
- const applyHeaders = (response) => {
1190
- if (store._headers && response instanceof Response) {
1191
- for (const [name, value] of Object.entries(store._headers)) {
1192
- response.headers.set(name, value);
1193
- }
1194
- }
1195
- return response;
1196
- };
1197
-
1198
- // Execute the chain with error handling
1199
- try {
1200
- const result = await executeChain(0);
1201
- if (result) return applyHeaders(result);
1202
- // If no result, return 404
1203
- if (notFoundHandler) {
1204
- const ctx = createContext(req, {}, store);
1205
- return applyHeaders(notFoundHandler(ctx));
1206
- }
1207
- return new Response('Not Found', { status: 404 });
1208
- } catch (err) {
1209
- if (errorHandler) {
1210
- const ctx = createContext(req, {}, store);
1211
- return applyHeaders(errorHandler(err, ctx));
1212
- }
1213
- throw err;
1214
- }
1215
- };
1216
-
1217
- const appObj = {
1218
- get: (path, ...handlers) => addRoute('GET', path, ...handlers),
1219
- post: (path, ...handlers) => addRoute('POST', path, ...handlers),
1220
- put: (path, ...handlers) => addRoute('PUT', path, ...handlers),
1221
- delete: (path, ...handlers) => addRoute('DELETE', path, ...handlers),
1222
- patch: (path, ...handlers) => addRoute('PATCH', path, ...handlers),
1223
- all: (path, ...handlers) => addRoute('ALL', path, ...handlers),
1224
- use: (pathOrHandler, handler) => {
1225
- const path = typeof pathOrHandler === 'string' ? pathOrHandler : '/*';
1226
- const h = typeof pathOrHandler === 'function' ? pathOrHandler : handler;
1227
- const { regex, params } = parsePattern(path);
1228
- middleware.push({ prefix: path, regex, handler: h });
1229
- },
1230
- route: (basePath, subApp) => {
1231
- // Mount sub-app routes
1232
- for (const route of subApp._routes || []) {
1233
- addRoute(route.method, basePath + route.pattern, ...route.handlers);
1234
- }
1235
- },
1236
- // Create a scoped sub-app with a base path
1237
- basePath: (prefix) => {
1238
- const subApp = {
1239
- get: (path, ...handlers) => { addRoute('GET', prefix + path, ...handlers); return subApp; },
1240
- post: (path, ...handlers) => { addRoute('POST', prefix + path, ...handlers); return subApp; },
1241
- put: (path, ...handlers) => { addRoute('PUT', prefix + path, ...handlers); return subApp; },
1242
- delete: (path, ...handlers) => { addRoute('DELETE', prefix + path, ...handlers); return subApp; },
1243
- patch: (path, ...handlers) => { addRoute('PATCH', prefix + path, ...handlers); return subApp; },
1244
- all: (path, ...handlers) => { addRoute('ALL', prefix + path, ...handlers); return subApp; },
1245
- basePath: (subPrefix) => appObj.basePath(prefix + subPrefix),
1246
- _routes: routes,
1247
- };
1248
- return subApp;
1249
- },
1250
- // Global 404 handler
1251
- notFound: (handler) => { notFoundHandler = handler; },
1252
- // Global error handler
1253
- onError: (handler) => { errorHandler = handler; },
1254
- request: async (path, options = {}) => {
1255
- const url = path.startsWith('http') ? path : 'http://localhost' + path;
1256
- const req = new Request(url, {
1257
- method: options.method || 'GET',
1258
- headers: options.headers || {},
1259
- body: options.body,
1260
- });
1261
- return handleRequest(req);
1262
- },
1263
- fetch: handleRequest,
1264
- _routes: routes,
1265
- };
1266
- return appObj;
1267
- };
1268
-
1269
- // Create global app instance
1270
- const app = __createHonoApp__();
1271
-
1272
- // ============================================================
1273
- // MDX Rendering Utilities
1274
- // ============================================================
1275
-
1276
- // Extract frontmatter from MDX content
1277
- const __extractFrontmatter__ = (content) => {
1278
- const match = content.match(/^---\\n([\\s\\S]*?)\\n---\\n?([\\s\\S]*)$/);
1279
- if (!match) return { frontmatter: {}, body: content };
1280
- try {
1281
- const fm = {};
1282
- match[1].split('\\n').forEach(line => {
1283
- const [key, ...vals] = line.split(':');
1284
- if (key && vals.length) fm[key.trim()] = vals.join(':').trim();
1285
- });
1286
- return { frontmatter: fm, body: match[2] };
1287
- } catch {
1288
- return { frontmatter: {}, body: content };
1289
- }
1290
- };
1291
-
1292
- // render object for MDX
1293
- const render = {
1294
- // Render MDX to markdown (strip frontmatter by default)
1295
- markdown: (content, options = {}) => {
1296
- const { frontmatter, body } = __extractFrontmatter__(content);
1297
- if (options.includeFrontmatter) {
1298
- return content;
1299
- }
1300
- return body.trim();
1301
- },
1302
- // Extract table of contents from markdown headings
1303
- toc: (content) => {
1304
- const { body } = __extractFrontmatter__(content);
1305
- const headings = [];
1306
- const regex = /^(#{1,6})\\s+(.+)$/gm;
1307
- let match;
1308
- while ((match = regex.exec(body)) !== null) {
1309
- headings.push({
1310
- level: match[1].length,
1311
- text: match[2].trim(),
1312
- slug: match[2].trim().toLowerCase().replace(/[^a-z0-9]+/g, '-')
1313
- });
1314
- }
1315
- return headings;
1316
- },
1317
- // Render MDX to HTML (simplified)
1318
- html: (content) => {
1319
- const { body } = __extractFrontmatter__(content);
1320
- // Basic markdown to HTML
1321
- return body
1322
- .replace(/^### (.+)$/gm, '<h3>$1</h3>')
1323
- .replace(/^## (.+)$/gm, '<h2>$1</h2>')
1324
- .replace(/^# (.+)$/gm, '<h1>$1</h1>')
1325
- .replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>')
1326
- .replace(/\\*(.+?)\\*/g, '<em>$1</em>')
1327
- .replace(/\\[(.+?)\\]\\((.+?)\\)/g, '<a href="$2">$1</a>')
1328
- .replace(/^- (.+)$/gm, '<li>$1</li>')
1329
- .replace(/(<li>.*<\\/li>\\n?)+/g, '<ul>$&</ul>')
1330
- .replace(/^\\n+|\\n+$/g, '');
1331
- }
1332
- };
1333
-
1334
- // React-like hooks stubs
1335
- let __hook_state__ = [];
1336
- let __hook_index__ = 0;
1337
-
1338
- const useState = (initial) => {
1339
- const idx = __hook_index__++;
1340
- if (__hook_state__[idx] === undefined) {
1341
- __hook_state__[idx] = initial;
1342
- }
1343
- const setState = (newVal) => {
1344
- __hook_state__[idx] = typeof newVal === 'function' ? newVal(__hook_state__[idx]) : newVal;
1345
- };
1346
- return [__hook_state__[idx], setState];
1347
- };
1348
-
1349
- const useEffect = (fn, deps) => { /* No-op in server context */ };
1350
- const useRef = (initial) => ({ current: initial });
1351
- const useMemo = (fn, deps) => fn();
1352
- const useCallback = (fn, deps) => fn;
1353
-
1354
- // Suspense placeholder
1355
- const Suspense = ({ children, fallback }) => children;
1356
-
1357
- // Streaming render function
1358
- const renderToStream = async (element, stream) => {
1359
- const html = __renderJsx__(element);
1360
- await stream.write(html);
1361
- };
1362
-
1363
- // Serialization utilities for client props
1364
- const serialize = {
1365
- clientProps: (props) => {
1366
- const result = {};
1367
- for (const [key, value] of Object.entries(props || {})) {
1368
- if (typeof value === 'function') {
1369
- result[key] = { __rpc: true, name: value.name || 'anonymous' };
1370
- } else {
1371
- result[key] = value;
1372
- }
1373
- }
1374
- return result;
1375
- },
1376
- json: JSON.stringify,
1377
- parse: JSON.parse
1378
- };
1379
-
1380
- // Add .isClient getter to Function.prototype for component detection
1381
- Object.defineProperty(Function.prototype, 'isClient', {
1382
- get: function() {
1383
- return __isClientComponent__(this);
1384
- },
1385
- configurable: true,
1386
- enumerable: false
1387
- });
1388
-
1389
- // ============================================================
1390
- // Additional Parsing Functions
1391
- // ============================================================
1392
-
1393
- // Parse URL into components
1394
- const parseUrl = (urlString) => {
1395
- try {
1396
- const url = new URL(urlString);
1397
- const pathParts = url.pathname.split('/').filter(Boolean);
1398
- return {
1399
- protocol: url.protocol.replace(':', ''),
1400
- host: url.host,
1401
- pathname: url.pathname,
1402
- path: pathParts,
1403
- search: url.search,
1404
- hash: url.hash,
1405
- origin: url.origin
1406
- };
1407
- } catch {
1408
- return { pathname: urlString, path: urlString.split('/').filter(Boolean) };
1409
- }
1410
- };
1411
-
1412
- // ============================================================
1413
- // Workflow/Event System (aligned with ai-workflows)
1414
- // ============================================================
1415
-
1416
- // Event handler registry
1417
- const __event_handlers__ = new Map();
1418
- // Schedule handler registry
1419
- const __schedule_handlers__ = [];
1420
- // Workflow history for tracking
1421
- const __workflow_history__ = [];
1422
-
1423
- // Known cron patterns for schedules
1424
- const __KNOWN_PATTERNS__ = {
1425
- second: '* * * * * *', minute: '* * * * *', hour: '0 * * * *',
1426
- day: '0 0 * * *', week: '0 0 * * 0', month: '0 0 1 * *', year: '0 0 1 1 *',
1427
- Monday: '0 0 * * 1', Tuesday: '0 0 * * 2', Wednesday: '0 0 * * 3',
1428
- Thursday: '0 0 * * 4', Friday: '0 0 * * 5', Saturday: '0 0 * * 6', Sunday: '0 0 * * 0',
1429
- weekday: '0 0 * * 1-5', weekend: '0 0 * * 0,6', midnight: '0 0 * * *', noon: '0 12 * * *'
1430
- };
1431
-
1432
- // Time patterns for schedule modifiers
1433
- const __TIME_PATTERNS__ = {
1434
- at6am: { hour: 6, minute: 0 }, at7am: { hour: 7, minute: 0 }, at8am: { hour: 8, minute: 0 },
1435
- at9am: { hour: 9, minute: 0 }, at10am: { hour: 10, minute: 0 }, at11am: { hour: 11, minute: 0 },
1436
- at12pm: { hour: 12, minute: 0 }, atnoon: { hour: 12, minute: 0 }, at1pm: { hour: 13, minute: 0 },
1437
- at2pm: { hour: 14, minute: 0 }, at3pm: { hour: 15, minute: 0 }, at4pm: { hour: 16, minute: 0 },
1438
- at5pm: { hour: 17, minute: 0 }, at6pm: { hour: 18, minute: 0 }, at7pm: { hour: 19, minute: 0 },
1439
- at8pm: { hour: 20, minute: 0 }, at9pm: { hour: 21, minute: 0 }, atmidnight: { hour: 0, minute: 0 }
1440
- };
1441
-
1442
- // Parse event string (Noun.event)
1443
- const __parseEvent__ = (event) => {
1444
- const parts = event.split('.');
1445
- if (parts.length !== 2) return null;
1446
- return { noun: parts[0], event: parts[1] };
1447
- };
1448
-
1449
- // Register event handler
1450
- const __registerEventHandler__ = (noun, event, handler) => {
1451
- const key = noun + '.' + event;
1452
- if (!__event_handlers__.has(key)) __event_handlers__.set(key, []);
1453
- __event_handlers__.get(key).push({ handler, source: handler.toString() });
1454
- };
1455
-
1456
- // Register schedule handler
1457
- const __registerScheduleHandler__ = (interval, handler) => {
1458
- __schedule_handlers__.push({ interval, handler, source: handler.toString() });
1459
- };
1460
-
1461
- // Create $.on - supports both on('event', handler) and on.Entity.event(handler)
1462
- const on = new Proxy(function(event, filterOrHandler, handler) {
1463
- // on('user.created', handler) or on('user.created', { where: ... }, handler)
1464
- if (typeof event === 'string') {
1465
- const actualHandler = typeof filterOrHandler === 'function' ? filterOrHandler : handler;
1466
- const filter = typeof filterOrHandler === 'object' ? filterOrHandler : null;
1467
- const parts = event.split('.');
1468
- const key = event; // Use full event string as key
1469
- if (!__event_handlers__.has(key)) __event_handlers__.set(key, []);
1470
- __event_handlers__.get(key).push({ handler: actualHandler, filter, source: actualHandler.toString() });
1471
- return {
1472
- off: () => {
1473
- const handlers = __event_handlers__.get(key);
1474
- if (handlers) {
1475
- const idx = handlers.findIndex(h => h.handler === actualHandler);
1476
- if (idx > -1) handlers.splice(idx, 1);
1477
- }
1478
- }
1479
- };
1480
- }
1481
- }, {
1482
- get: (target, prop) => {
1483
- if (prop === 'once') {
1484
- // on.once('event', handler) - one-time handler
1485
- return (event, handler) => {
1486
- const wrapper = async (data, $) => {
1487
- const key = event;
1488
- const handlers = __event_handlers__.get(key);
1489
- if (handlers) {
1490
- const idx = handlers.findIndex(h => h.handler === wrapper);
1491
- if (idx > -1) handlers.splice(idx, 1);
1492
- }
1493
- return handler(data, $);
1494
- };
1495
- const key = event;
1496
- if (!__event_handlers__.has(key)) __event_handlers__.set(key, []);
1497
- __event_handlers__.get(key).push({ handler: wrapper, source: handler.toString() });
1498
- return { off: () => {} };
1499
- };
1500
- }
1501
- // on.Entity.event(handler) pattern
1502
- const noun = String(prop);
1503
- return new Proxy({}, {
1504
- get: (_, eventName) => (handler) => {
1505
- const key = noun + '.' + String(eventName);
1506
- __registerEventHandler__(noun, String(eventName), handler);
1507
- return {
1508
- off: () => {
1509
- const handlers = __event_handlers__.get(key);
1510
- if (handlers) {
1511
- const idx = handlers.findIndex(h => h.handler === handler);
1512
- if (idx > -1) handlers.splice(idx, 1);
1513
- }
1514
- },
1515
- unsubscribe: () => {
1516
- const handlers = __event_handlers__.get(key);
1517
- if (handlers) {
1518
- const idx = handlers.findIndex(h => h.handler === handler);
1519
- if (idx > -1) handlers.splice(idx, 1);
1520
- }
1521
- }
1522
- };
1523
- }
1524
- });
1525
- },
1526
- apply: (target, thisArg, args) => target(...args)
1527
- });
1528
-
1529
- // Parse duration string (100ms, 1s, 5m, etc.)
1530
- const __parseDuration__ = (str) => {
1531
- if (!str || typeof str !== 'string') return null;
1532
- const match = str.match(/^(\\d+)(ms|s|m|h|d|w)?$/);
1533
- if (!match) return null;
1534
- const value = parseInt(match[1], 10);
1535
- const unit = match[2] || 'ms';
1536
- switch (unit) {
1537
- case 'ms': return value;
1538
- case 's': return value * 1000;
1539
- case 'm': return value * 60 * 1000;
1540
- case 'h': return value * 60 * 60 * 1000;
1541
- case 'd': return value * 24 * 60 * 60 * 1000;
1542
- case 'w': return value * 7 * 24 * 60 * 60 * 1000;
1543
- default: return value;
1544
- }
1545
- };
1546
-
1547
- // Check if string looks like a cron expression
1548
- const __isCronExpression__ = (str) => {
1549
- if (!str || typeof str !== 'string') return false;
1550
- const parts = str.trim().split(/\\s+/);
1551
- return parts.length >= 5 && parts.length <= 6;
1552
- };
1553
-
1554
- // Active schedule timers
1555
- const __schedule_timers__ = [];
1556
-
1557
- // Create $.every - supports every('100ms', handler), every('name', '* * * *', handler), and every.Monday.at9am(handler)
1558
- const every = new Proxy(function(intervalOrName, handlerOrCronOrOptions, handlerArg) {
1559
- // Determine format: every('100ms', handler) or every('name', '* * * *', handler) or every('name', opts, handler)
1560
- let name = null;
1561
- let interval = null;
1562
- let handler = null;
1563
- let options = {};
1564
-
1565
- if (typeof handlerOrCronOrOptions === 'function') {
1566
- // every('100ms', handler) or every('* * * *', handler)
1567
- interval = intervalOrName;
1568
- handler = handlerOrCronOrOptions;
1569
- } else if (typeof handlerArg === 'function') {
1570
- // every('name', '* * * *', handler) or every('name', { immediate: true }, handler)
1571
- name = intervalOrName;
1572
- if (typeof handlerOrCronOrOptions === 'string') {
1573
- interval = handlerOrCronOrOptions;
1574
- } else {
1575
- options = handlerOrCronOrOptions || {};
1576
- interval = options.interval;
1577
- }
1578
- handler = handlerArg;
1579
- }
1580
-
1581
- // Determine if it's a duration or cron
1582
- const isCron = __isCronExpression__(interval);
1583
- const durationMs = isCron ? null : __parseDuration__(interval);
1584
-
1585
- let stopped = false;
1586
- let timer = null;
1587
- let runCount = 0;
1588
-
1589
- const job = {
1590
- name: name || interval,
1591
- cron: isCron ? interval : null,
1592
- stopped: false,
1593
- stop: () => {
1594
- stopped = true;
1595
- job.stopped = true;
1596
- if (timer) clearInterval(timer);
1597
- },
1598
- next: () => new Date(Date.now() + (durationMs || 60000))
1599
- };
1600
-
1601
- if (durationMs) {
1602
- // Duration-based schedule
1603
- if (options.immediate) {
1604
- Promise.resolve().then(() => handler());
1605
- }
1606
-
1607
- timer = setInterval(async () => {
1608
- if (stopped) return;
1609
- if (options.until && options.until()) {
1610
- job.stop();
1611
- return;
1612
- }
1613
- runCount++;
1614
- try {
1615
- await handler();
1616
- } catch (e) {
1617
- console.error('[every] Handler error:', e);
1618
- }
1619
- }, durationMs);
1620
-
1621
- __schedule_timers__.push(timer);
1622
- }
1623
-
1624
- __registerScheduleHandler__({
1625
- type: isCron ? 'cron' : 'duration',
1626
- expression: isCron ? interval : null,
1627
- ms: durationMs,
1628
- natural: name || interval
1629
- }, handler);
1630
-
1631
- return job;
1632
- }, {
1633
- get: (target, prop) => {
1634
- const propStr = String(prop);
1635
- const pattern = __KNOWN_PATTERNS__[propStr];
1636
- if (pattern) {
1637
- const result = (handler) => {
1638
- __registerScheduleHandler__({ type: 'cron', expression: pattern, natural: propStr }, handler);
1639
- return { stop: () => {}, cancel: () => {}, name: propStr, cron: pattern, stopped: false };
1640
- };
1641
- // Support time modifiers: every.Monday.at9am(handler)
1642
- return new Proxy(result, {
1643
- get: (_, timeKey) => {
1644
- const time = __TIME_PATTERNS__[String(timeKey)];
1645
- if (time) {
1646
- const parts = pattern.split(' ');
1647
- parts[0] = String(time.minute);
1648
- parts[1] = String(time.hour);
1649
- const cron = parts.join(' ');
1650
- return (handler) => {
1651
- __registerScheduleHandler__({ type: 'cron', expression: cron, natural: propStr + '.' + String(timeKey) }, handler);
1652
- return { stop: () => {}, cancel: () => {}, name: propStr + '.' + String(timeKey), cron, stopped: false };
1653
- };
1654
- }
1655
- return undefined;
1656
- },
1657
- apply: (_, __, args) => {
1658
- __registerScheduleHandler__({ type: 'cron', expression: pattern, natural: propStr }, args[0]);
1659
- return { stop: () => {}, cancel: () => {}, name: propStr, cron: pattern, stopped: false };
1660
- }
1661
- });
1662
- }
1663
- // Plural units: every.seconds(5), every.minutes(10), etc.
1664
- const pluralUnits = { seconds: 'second', minutes: 'minute', hours: 'hour', days: 'day', weeks: 'week' };
1665
- if (pluralUnits[propStr]) {
1666
- return (value) => (handler) => {
1667
- __registerScheduleHandler__({ type: pluralUnits[propStr], value, natural: value + ' ' + propStr }, handler);
1668
- return { stop: () => {}, cancel: () => {}, name: value + ' ' + propStr, stopped: false };
1669
- };
1670
- }
1671
- return undefined;
1672
- },
1673
- apply: (target, thisArg, args) => target(...args)
1674
- });
1675
-
1676
- // Send event - returns event info, supports options (delay, correlationId, channel, wait)
1677
- const send = async (event, data, options = {}) => {
1678
- const eventId = __generateId__();
1679
- const timestamp = Date.now();
1680
- const eventObj = {
1681
- id: eventId,
1682
- type: event,
1683
- data,
1684
- timestamp,
1685
- correlationId: options.correlationId
1686
- };
1687
-
1688
- __workflow_history__.push({ type: 'event', name: event, data, timestamp });
1689
-
1690
- // Handle delay
1691
- if (options.delay) {
1692
- const delayMs = typeof options.delay === 'string' ? __parseDuration__(options.delay) : options.delay;
1693
- if (delayMs) {
1694
- await new Promise(r => setTimeout(r, delayMs));
1695
- }
1696
- }
1697
-
1698
- // Find matching handlers (supports wildcard like 'user.*')
1699
- const matchingHandlers = [];
1700
- for (const [key, handlers] of __event_handlers__) {
1701
- const keyPattern = key.replace(/\\.\\*/g, '\\\\.[^.]+');
1702
- const regex = new RegExp('^' + keyPattern + '$');
1703
- if (key === event || regex.test(event) || (key.endsWith('.*') && event.startsWith(key.slice(0, -1)))) {
1704
- for (const h of handlers) {
1705
- // Check channel filter
1706
- if (h.filter?.channel && h.filter.channel !== options.channel) continue;
1707
- // Check where filter
1708
- if (h.filter?.where) {
1709
- let match = true;
1710
- for (const [k, v] of Object.entries(h.filter.where)) {
1711
- if (data[k] !== v) { match = false; break; }
1712
- }
1713
- if (!match) continue;
1714
- }
1715
- matchingHandlers.push(h);
1716
- }
1717
- }
1718
- }
1719
-
1720
- // If wait option, call first handler and return response
1721
- if (options.wait && matchingHandlers.length > 0) {
1722
- let response = null;
1723
- const reply = (data) => { response = { data }; };
1724
- await matchingHandlers[0].handler({ type: event, data, correlationId: options.correlationId }, reply);
1725
- return response || eventObj;
1726
- }
1727
-
1728
- // Fire all handlers
1729
- await Promise.all(matchingHandlers.map(async ({ handler }) => {
1730
- try {
1731
- await handler({ type: event, data, correlationId: options.correlationId }, $);
1732
- } catch (error) {
1733
- console.error('Error in handler for ' + event + ':', error);
1734
- }
1735
- }));
1736
-
1737
- return eventObj;
1738
- };
1739
-
1740
- // Add broadcast method to send
1741
- send.broadcast = async (event, data) => {
1742
- return send(event, data);
1743
- };
1744
-
1745
- // Delay helper
1746
- const delay = (ms) => {
1747
- if (typeof ms === 'string') ms = __parseDuration__(ms) || 0;
1748
- return new Promise(r => setTimeout(r, ms));
1749
- };
1750
-
1751
- // Decide helper - pattern matching decision
1752
- const decide = (subject) => {
1753
- const conditions = [];
1754
- let defaultValue = null;
1755
-
1756
- const chain = {
1757
- when: (conditionOrFn, result) => {
1758
- conditions.push({ condition: conditionOrFn, result });
1759
- return chain;
1760
- },
1761
- otherwise: (result) => {
1762
- defaultValue = result;
1763
- // Execute decision
1764
- for (const { condition, result } of conditions) {
1765
- let matches = false;
1766
- if (typeof condition === 'function') {
1767
- matches = condition(subject);
1768
- } else if (typeof condition === 'object') {
1769
- matches = true;
1770
- for (const [key, value] of Object.entries(condition)) {
1771
- const subjectValue = subject[key];
1772
- if (typeof value === 'object' && value !== null) {
1773
- for (const [op, opVal] of Object.entries(value)) {
1774
- switch (op) {
1775
- case '$gte': if (!(subjectValue >= opVal)) matches = false; break;
1776
- case '$gt': if (!(subjectValue > opVal)) matches = false; break;
1777
- case '$lte': if (!(subjectValue <= opVal)) matches = false; break;
1778
- case '$lt': if (!(subjectValue < opVal)) matches = false; break;
1779
- case '$ne': if (subjectValue === opVal) matches = false; break;
1780
- default: if (subjectValue !== value) matches = false;
1781
- }
1782
- }
1783
- } else if (subjectValue !== value) {
1784
- matches = false;
1785
- }
1786
- if (!matches) break;
1787
- }
1788
- }
1789
- if (matches) {
1790
- return typeof result === 'function' ? result() : result;
1791
- }
1792
- }
1793
- return typeof defaultValue === 'function' ? defaultValue() : defaultValue;
1794
- }
1795
- };
1796
-
1797
- return chain;
1798
- };
1799
-
1800
- // Async decide
1801
- decide.async = (subject) => {
1802
- const conditions = [];
1803
-
1804
- const chain = {
1805
- when: (conditionFn, result) => {
1806
- conditions.push({ condition: conditionFn, result });
1807
- return chain;
1808
- },
1809
- otherwise: async (defaultResult) => {
1810
- for (const { condition, result } of conditions) {
1811
- const matches = await condition(subject);
1812
- if (matches) return result;
1813
- }
1814
- return defaultResult;
1815
- }
1816
- };
1817
-
1818
- return chain;
1819
- };
1820
-
1821
- // Track helper - analytics tracking
1822
- const __tracked_events__ = [];
1823
- const track = async (event, data, metadata = {}) => {
1824
- const entry = { type: event, data, metadata, timestamp: Date.now(), userId: metadata.userId };
1825
- __tracked_events__.push(entry);
1826
- return entry;
1827
- };
1828
-
1829
- track.user = async (userId, event, data) => {
1830
- const entry = { type: event, data, userId, timestamp: Date.now() };
1831
- __tracked_events__.push(entry);
1832
- return entry;
1833
- };
1834
-
1835
- track.query = async (options = {}) => {
1836
- let results = [...__tracked_events__];
1837
- if (options.type) results = results.filter(e => e.type === options.type);
1838
- if (options.userId) results = results.filter(e => e.userId === options.userId);
1839
- if (options['data.buttonId']) results = results.filter(e => e.data?.buttonId === options['data.buttonId']);
1840
- if (options.orderBy) {
1841
- results.sort((a, b) => {
1842
- const aVal = a[options.orderBy];
1843
- const bVal = b[options.orderBy];
1844
- return options.order === 'desc' ? bVal - aVal : aVal - bVal;
1845
- });
1846
- }
1847
- if (options.limit) results = results.slice(0, options.limit);
1848
- return results;
1849
- };
1850
-
1851
- track.funnel = async (steps, options = {}) => {
1852
- const events = await track.query(options);
1853
- const completed = steps.every(step => events.some(e => e.type === step));
1854
- return {
1855
- steps: steps.map(step => ({ step, completed: events.some(e => e.type === step) })),
1856
- completed,
1857
- conversionRate: completed ? 1 : 0
1858
- };
1859
- };
1860
-
1861
- track.aggregate = async (event, options = {}) => {
1862
- const events = __tracked_events__.filter(e => e.type === event);
1863
- const groups = {};
1864
- for (const e of events) {
1865
- const key = options.groupBy ? e.data[options.groupBy] : '_all';
1866
- if (!groups[key]) groups[key] = { sum: 0, count: 0, values: [] };
1867
- groups[key].count++;
1868
- if (options.sum) groups[key].sum += e.data[options.sum] || 0;
1869
- groups[key].values.push(e);
1870
- }
1871
- return groups;
1872
- };
1873
-
1874
- track.timeseries = async (event, options = {}) => {
1875
- const events = __tracked_events__.filter(e => e.type === event);
1876
- return events.map(e => ({
1877
- timestamp: e.timestamp,
1878
- value: options.field ? e.data[options.field] : 1
1879
- }));
1880
- };
1881
-
1882
- // Experiment helper - A/B testing
1883
- const experiment = (name) => {
1884
- const variants = [];
1885
- let eligibilityFn = null;
1886
- const overrides = new Map();
1887
- let rolloutConfig = null;
1888
-
1889
- const exp = {
1890
- variant: (variantName, config, options = {}) => {
1891
- variants.push({ name: variantName, config, weight: options.weight || 1 });
1892
- return exp;
1893
- },
1894
- eligible: (fn) => {
1895
- eligibilityFn = fn;
1896
- return exp;
1897
- },
1898
- override: (userId, variantName) => {
1899
- overrides.set(userId, variantName);
1900
- return exp;
1901
- },
1902
- rollout: (variantName, percentage) => {
1903
- rolloutConfig = { variant: variantName, percentage };
1904
- return exp;
1905
- },
1906
- assign: (userId) => {
1907
- // Check override
1908
- if (overrides.has(userId)) {
1909
- const overrideVariant = variants.find(v => v.name === overrides.get(userId));
1910
- if (overrideVariant) return { name: overrideVariant.name, ...overrideVariant.config };
1911
- }
1912
- // Check eligibility
1913
- if (eligibilityFn && typeof userId === 'object' && !eligibilityFn(userId)) {
1914
- return variants[0] ? { name: variants[0].name, ...variants[0].config } : {};
1915
- }
1916
- // Consistent assignment based on userId hash
1917
- const userIdStr = typeof userId === 'object' ? userId.id || JSON.stringify(userId) : String(userId);
1918
- let hash = 0;
1919
- for (let i = 0; i < userIdStr.length; i++) {
1920
- hash = ((hash << 5) - hash) + userIdStr.charCodeAt(i);
1921
- hash = hash & hash;
1922
- }
1923
- const normalized = Math.abs(hash % 100) / 100;
1924
-
1925
- // Check rollout
1926
- if (rolloutConfig && normalized < rolloutConfig.percentage) {
1927
- const rolloutVariant = variants.find(v => v.name === rolloutConfig.variant);
1928
- if (rolloutVariant) return { name: rolloutVariant.name, ...rolloutVariant.config };
1929
- }
1930
-
1931
- // Weight-based selection
1932
- const totalWeight = variants.reduce((sum, v) => sum + v.weight, 0);
1933
- let cumulative = 0;
1934
- for (const v of variants) {
1935
- cumulative += v.weight / totalWeight;
1936
- if (normalized < cumulative) return { name: v.name, ...v.config };
1937
- }
1938
- return variants[0] ? { name: variants[0].name, ...variants[0].config } : {};
1939
- },
1940
- track: {
1941
- exposure: async (userId) => {
1942
- await track('experiment.exposure', { experiment: name, userId });
1943
- },
1944
- conversion: async (userId, data = {}) => {
1945
- await track('experiment.conversion', { experiment: name, userId, ...data });
1946
- }
1947
- },
1948
- results: async () => {
1949
- const exposures = __tracked_events__.filter(e => e.type === 'experiment.exposure' && e.data.experiment === name);
1950
- const conversions = __tracked_events__.filter(e => e.type === 'experiment.conversion' && e.data.experiment === name);
1951
- const result = {};
1952
- for (const v of variants) {
1953
- result[v.name] = { exposures: 0, conversions: 0 };
1954
- }
1955
- for (const e of exposures) {
1956
- const variantName = exp.assign(e.data.userId).name;
1957
- if (result[variantName]) result[variantName].exposures++;
1958
- }
1959
- for (const e of conversions) {
1960
- const variantName = exp.assign(e.data.userId).name;
1961
- if (result[variantName]) result[variantName].conversions++;
1962
- }
1963
- return result;
1964
- }
1965
- };
1966
-
1967
- return exp;
1968
- };
1969
-
1970
- // Execute event and wait for result ($.do) - mirrors ai-workflows do
1971
- const __doEvent__ = async (event, data) => {
1972
- __workflow_history__.push({ type: 'action', name: 'do:' + event, data, timestamp: Date.now() });
1973
-
1974
- const parsed = __parseEvent__(event);
1975
- if (!parsed) throw new Error('Invalid event format: ' + event + '. Expected Noun.event');
1976
-
1977
- const key = parsed.noun + '.' + parsed.event;
1978
- const handlers = __event_handlers__.get(key) || [];
1979
- if (handlers.length === 0) throw new Error('No handler registered for ' + event);
1980
-
1981
- return await handlers[0].handler(data, $);
1982
- };
1983
-
1984
- // Try event (non-durable) - mirrors ai-workflows try
1985
- const __tryEvent__ = async (event, data) => {
1986
- __workflow_history__.push({ type: 'action', name: 'try:' + event, data, timestamp: Date.now() });
1987
-
1988
- const parsed = __parseEvent__(event);
1989
- if (!parsed) throw new Error('Invalid event format: ' + event + '. Expected Noun.event');
1990
-
1991
- const key = parsed.noun + '.' + parsed.event;
1992
- const handlers = __event_handlers__.get(key) || [];
1993
- if (handlers.length === 0) throw new Error('No handler registered for ' + event);
1994
-
1995
- return await handlers[0].handler(data, $);
1996
- };
1997
-
1998
- // Queue implementation
1999
- const __queues__ = new Map();
2000
- const __queue_stats__ = new Map();
2001
- const queue = (name) => {
2002
- if (!__queues__.has(name)) {
2003
- __queues__.set(name, []);
2004
- __queue_stats__.set(name, { added: 0, processed: 0, failed: 0, retried: 0 });
2005
- }
2006
- const q = __queues__.get(name);
2007
- const stats = __queue_stats__.get(name);
2008
- return {
2009
- add: async (item, options = {}) => {
2010
- const job = { id: __generateId__(), item, options, addedAt: new Date(), priority: options.priority || 'normal', attempts: 0 };
2011
- q.push(job);
2012
- stats.added++;
2013
- return job;
2014
- },
2015
- process: async (handler) => {
2016
- while (q.length) {
2017
- const job = q.shift();
2018
- try {
2019
- job.attempts++;
2020
- await handler(job.item);
2021
- stats.processed++;
2022
- } catch (e) {
2023
- stats.failed++;
2024
- if (job.attempts < (job.options.maxRetries || 3)) {
2025
- q.push(job); // Retry
2026
- stats.retried++;
2027
- }
2028
- }
2029
- }
2030
- },
2031
- processBatch: async (handler, batchSize = 10) => {
2032
- const batch = q.splice(0, batchSize);
2033
- if (batch.length === 0) return;
2034
- try {
2035
- await handler(batch.map(j => j.item));
2036
- stats.processed += batch.length;
2037
- } catch (e) {
2038
- stats.failed += batch.length;
2039
- // Put back for retry
2040
- for (const job of batch) {
2041
- if (job.attempts < (job.options.maxRetries || 3)) {
2042
- job.attempts++;
2043
- q.push(job);
2044
- stats.retried++;
2045
- }
2046
- }
2047
- }
2048
- },
2049
- size: () => q.length,
2050
- clear: () => { q.length = 0; },
2051
- stats: () => ({ ...stats, pending: q.length }),
2052
- peek: (n = 1) => q.slice(0, n).map(j => j.item),
2053
- getByPriority: (priority) => q.filter(j => j.priority === priority).map(j => j.item)
2054
- };
2055
- };
2056
-
2057
- // Actor pattern implementation
2058
- const __actors__ = new Map();
2059
- const actor = (type) => ({
2060
- register: (id, state = {}) => {
2061
- const key = type + ':' + id;
2062
- __actors__.set(key, { id, type, state, createdAt: new Date() });
2063
- return __actors__.get(key);
2064
- },
2065
- get: (id) => __actors__.get(type + ':' + id),
2066
- send: async (id, message) => {
2067
- const a = __actors__.get(type + ':' + id);
2068
- if (!a) throw new Error('Actor not found: ' + type + ':' + id);
2069
- // Handle message (stub - in real impl would dispatch to actor handler)
2070
- return { delivered: true };
2071
- }
2072
- });
2073
-
2074
- // Actions API (workflow actions)
2075
- const __actions__ = new Map();
2076
- const actions = {
2077
- async create(options) {
2078
- const id = __generateId__();
2079
- const action = {
2080
- id,
2081
- actor: options.actor,
2082
- object: options.object,
2083
- action: options.action,
2084
- metadata: options.metadata || {},
2085
- status: 'pending',
2086
- createdAt: new Date(),
2087
- updatedAt: new Date()
2088
- };
2089
- __actions__.set(id, action);
2090
- return action;
2091
- },
2092
- async get(id) {
2093
- return __actions__.get(id) || null;
2094
- },
2095
- async start(id) {
2096
- const action = __actions__.get(id);
2097
- if (!action) throw new Error('Action not found: ' + id);
2098
- action.status = 'active';
2099
- action.startedAt = new Date();
2100
- action.updatedAt = new Date();
2101
- return action;
2102
- },
2103
- async complete(id, result = {}) {
2104
- const action = __actions__.get(id);
2105
- if (!action) throw new Error('Action not found: ' + id);
2106
- action.status = 'completed';
2107
- action.result = result;
2108
- action.completedAt = new Date();
2109
- action.updatedAt = new Date();
2110
- return action;
2111
- },
2112
- async fail(id, error) {
2113
- const action = __actions__.get(id);
2114
- if (!action) throw new Error('Action not found: ' + id);
2115
- action.status = 'failed';
2116
- action.error = error;
2117
- action.failedAt = new Date();
2118
- action.updatedAt = new Date();
2119
- return action;
2120
- },
2121
- async list(options = {}) {
2122
- let results = Array.from(__actions__.values());
2123
- if (options.status) results = results.filter(a => a.status === options.status);
2124
- if (options.actor) results = results.filter(a => a.actor === options.actor);
2125
- if (options.limit) results = results.slice(0, options.limit);
2126
- return results;
2127
- },
2128
- async retry(id) {
2129
- const action = __actions__.get(id);
2130
- if (!action) throw new Error('Action not found: ' + id);
2131
- action.status = 'pending';
2132
- action.retryCount = (action.retryCount || 0) + 1;
2133
- action.updatedAt = new Date();
2134
- return action;
2135
- },
2136
- async cancel(id) {
2137
- const action = __actions__.get(id);
2138
- if (!action) throw new Error('Action not found: ' + id);
2139
- action.status = 'cancelled';
2140
- action.cancelledAt = new Date();
2141
- action.updatedAt = new Date();
2142
- return action;
2143
- }
2144
- };
2145
-
2146
- // Artifacts API (for storing results)
2147
- const __artifacts__ = new Map();
2148
- const artifacts = {
2149
- async store(options) {
2150
- const id = __generateId__();
2151
- const artifact = {
2152
- id,
2153
- type: options.type,
2154
- data: options.data,
2155
- metadata: options.metadata || {},
2156
- createdAt: new Date()
2157
- };
2158
- __artifacts__.set(id, artifact);
2159
- return artifact;
2160
- },
2161
- async get(id) {
2162
- return __artifacts__.get(id) || null;
2163
- },
2164
- async list(options = {}) {
2165
- let results = Array.from(__artifacts__.values());
2166
- if (options.type) results = results.filter(a => a.type === options.type);
2167
- if (options.limit) results = results.slice(0, options.limit);
2168
- return results;
2169
- }
2170
- };
2171
-
2172
- // Events API (for event sourcing)
2173
- const __events__ = [];
2174
- const events = {
2175
- async record(options) {
2176
- const event = {
2177
- id: __generateId__(),
2178
- type: options.type,
2179
- subject: options.subject,
2180
- data: options.data,
2181
- metadata: options.metadata || {},
2182
- timestamp: new Date()
2183
- };
2184
- __events__.push(event);
2185
- return event;
2186
- },
2187
- async list(options = {}) {
2188
- let results = [...__events__];
2189
- if (options.type) results = results.filter(e => e.type === options.type);
2190
- if (options.subject) results = results.filter(e => e.subject === options.subject);
2191
- if (options.limit) results = results.slice(0, options.limit);
2192
- return results;
2193
- },
2194
- // Query is an alias for list with more flexible filtering
2195
- async query(options = {}) {
2196
- let results = [...__events__];
2197
- if (options.type) results = results.filter(e => e.type === options.type);
2198
- if (options.subject) results = results.filter(e => e.subject === options.subject);
2199
- if (options.actor) results = results.filter(e => e.metadata?.actor === options.actor);
2200
- if (options.from) results = results.filter(e => new Date(e.timestamp) >= new Date(options.from));
2201
- if (options.to) results = results.filter(e => new Date(e.timestamp) <= new Date(options.to));
2202
- if (options.orderBy === 'timestamp') {
2203
- results.sort((a, b) => options.order === 'desc' ? b.timestamp - a.timestamp : a.timestamp - b.timestamp);
2204
- }
2205
- if (options.limit) results = results.slice(0, options.limit);
2206
- return results;
2207
- },
2208
- async replay(handler, options = {}) {
2209
- const evts = await events.list(options);
2210
- for (const evt of evts) {
2211
- await handler(evt);
2212
- }
2213
- },
2214
- // Subscribe to events
2215
- subscribe(type, handler) {
2216
- on(type, handler);
2217
- return { unsubscribe: () => {} };
2218
- },
2219
- // Emit an event (alias for record + send)
2220
- async emit(type, data, metadata = {}) {
2221
- const event = await events.record({ type, data, metadata });
2222
- await send(type, data);
2223
- return event;
2224
- }
2225
- };
2226
-
2227
- // MDX Parsing helpers (basic implementation)
2228
- const parse = (content) => {
2229
- const frontmatterMatch = content.match(/^---\\n([\\s\\S]*?)\\n---\\n?([\\s\\S]*)$/);
2230
- if (!frontmatterMatch) {
2231
- return { data: {}, content: content.trim(), type: null, id: null };
2232
- }
2233
- const [, frontmatter, body] = frontmatterMatch;
2234
- const data = {};
2235
- let currentKey = null;
2236
- let inArray = false;
2237
- const lines = frontmatter.split('\\n');
2238
- for (const line of lines) {
2239
- const keyMatch = line.match(/^(\\w+):\\s*(.*)$/);
2240
- if (keyMatch) {
2241
- currentKey = keyMatch[1];
2242
- const value = keyMatch[2].trim();
2243
- if (value === '') {
2244
- data[currentKey] = [];
2245
- inArray = true;
2246
- } else {
2247
- data[currentKey] = value;
2248
- inArray = false;
2249
- }
2250
- } else if (inArray && currentKey && line.match(/^\\s+-\\s+(.+)$/)) {
2251
- const itemMatch = line.match(/^\\s+-\\s+(.+)$/);
2252
- if (itemMatch) {
2253
- if (!Array.isArray(data[currentKey])) data[currentKey] = [];
2254
- data[currentKey].push(itemMatch[1].trim());
2255
- }
2256
- }
2257
- }
2258
- return {
2259
- data,
2260
- content: body.trim(),
2261
- type: data.$type || data['@type'] || null,
2262
- id: data.$id || data['@id'] || null
2263
- };
2264
- };
2265
-
2266
- const stringify = (doc) => {
2267
- const lines = ['---'];
2268
- if (doc.type) lines.push('$type: ' + doc.type);
2269
- if (doc.id) lines.push('$id: ' + doc.id);
2270
- for (const [key, value] of Object.entries(doc.data || {})) {
2271
- if (Array.isArray(value)) {
2272
- lines.push(key + ':');
2273
- for (const item of value) lines.push(' - ' + item);
2274
- } else {
2275
- lines.push(key + ': ' + value);
2276
- }
2277
- }
2278
- lines.push('---');
2279
- if (doc.content) lines.push('', doc.content);
2280
- return lines.join('\\n');
2281
- };
2282
-
2283
- const toAst = (doc) => {
2284
- const children = [];
2285
- const lines = (doc.content || '').split('\\n');
2286
- for (const line of lines) {
2287
- if (line.startsWith('# ')) children.push({ type: 'heading', depth: 1, text: line.slice(2) });
2288
- else if (line.startsWith('## ')) children.push({ type: 'heading', depth: 2, text: line.slice(3) });
2289
- else if (line.startsWith('- ')) {
2290
- const lastList = children[children.length - 1];
2291
- if (lastList?.type === 'list') {
2292
- lastList.items.push(line.slice(2));
2293
- } else {
2294
- children.push({ type: 'list', items: [line.slice(2)] });
2295
- }
2296
- } else if (line.startsWith('\`\`\`')) {
2297
- const lang = line.slice(3).trim();
2298
- children.push({ type: 'code', lang: lang || null, value: '' });
2299
- } else if (line.trim()) {
2300
- children.push({ type: 'paragraph', text: line });
2301
- }
2302
- }
2303
- return { type: 'root', children };
2304
- };
2305
-
2306
- const renderMarkdown = (doc, options = {}) => {
2307
- let result = '';
2308
- if (options.includeFrontmatter && doc.data && Object.keys(doc.data).length > 0) {
2309
- result += '---\\n';
2310
- if (doc.type) result += '$type: ' + doc.type + '\\n';
2311
- for (const [key, value] of Object.entries(doc.data)) {
2312
- if (Array.isArray(value)) {
2313
- result += key + ':\\n';
2314
- for (const item of value) result += ' - ' + item + '\\n';
2315
- } else {
2316
- result += key + ': ' + value + '\\n';
2317
- }
2318
- }
2319
- result += '---\\n';
2320
- }
2321
- result += doc.content || '';
2322
- return result;
2323
- };
2324
-
2325
- // Component factory (basic implementation)
2326
- const createComponents = (createElement) => {
2327
- const components = {};
2328
- const componentNames = ['Hero', 'Features', 'Pricing', 'CTA', 'Testimonials', 'FAQ', 'Footer', 'Header', 'Nav', 'Card', 'Grid', 'Section', 'Container', 'Button', 'Input', 'Form', 'Modal', 'Table', 'List', 'Badge', 'Alert', 'Progress', 'Spinner', 'Avatar', 'Image', 'Video', 'Code', 'Markdown'];
2329
- for (const name of componentNames) {
2330
- components[name] = (props) => createElement(name.toLowerCase() === 'hero' ? 'header' : 'section', { 'data-component': name, ...props });
2331
- }
2332
- return components;
2333
- };
2334
-
2335
- const getComponentNames = () => ['Hero', 'Features', 'Pricing', 'CTA', 'Testimonials', 'FAQ', 'Footer', 'Header', 'Nav', 'Card', 'Grid', 'Section', 'Container', 'Button', 'Input', 'Form', 'Modal', 'Table', 'List', 'Badge', 'Alert', 'Progress', 'Spinner', 'Avatar', 'Image', 'Video', 'Code', 'Markdown'];
2336
-
2337
- const getComponentMeta = (name) => ({
2338
- category: ['Hero', 'Features', 'Pricing', 'CTA', 'Testimonials'].includes(name) ? 'landing' : 'ui',
2339
- requiredProps: name === 'Hero' ? ['title'] : [],
2340
- related: name === 'Hero' ? ['CTA', 'Features'] : []
2341
- });
2342
-
2343
- const getComponentsByCategory = (category) => {
2344
- if (category === 'landing') return ['Hero', 'Features', 'Pricing', 'CTA', 'Testimonials'];
2345
- return ['Card', 'Button', 'Input', 'Form'];
2346
- };
2347
-
2348
- const extractTests = (content) => {
2349
- const tests = [];
2350
- const regex = /\`\`\`(?:ts|js)\\s+test[^\\n]*\\n([\\s\\S]*?)\`\`\`/g;
2351
- let match;
2352
- while ((match = regex.exec(content)) !== null) {
2353
- tests.push({ code: match[1].trim() });
2354
- }
2355
- return tests;
2356
- };
2357
-
2358
- const parseMeta = (meta) => {
2359
- const result = {};
2360
- const parts = meta.split(/\\s+/);
2361
- for (const part of parts) {
2362
- if (part.includes('=')) {
2363
- const [key, value] = part.split('=');
2364
- result[key] = value;
2365
- } else {
2366
- result[part] = true;
2367
- }
2368
- }
2369
- return result;
2370
- };
2371
-
2372
- // Simple createElement for testing
2373
- const createElement = (type, props, ...children) => ({ type, props: props || {}, children });
2374
-
2375
- // Graph/relationship helper functions
2376
- const extractLinks = (content) => {
2377
- const links = [];
2378
- const regex = /\\[([^\\]]+)\\]\\(([^)]+)\\)/g;
2379
- let match;
2380
- while ((match = regex.exec(content)) !== null) {
2381
- links.push({ text: match[1], url: match[2] });
2382
- }
2383
- return links;
2384
- };
2385
-
2386
- const extractRelationships = (doc) => {
2387
- const relationships = [];
2388
- const url = doc.id || doc.url;
2389
- if (!url) return relationships;
2390
- // Extract from content links
2391
- const links = extractLinks(doc.content || '');
2392
- for (const link of links) {
2393
- relationships.push({ from: url, to: link.url, type: 'links', label: link.text });
2394
- }
2395
- // Extract from data fields
2396
- for (const [key, value] of Object.entries(doc.data || {})) {
2397
- if (typeof value === 'string' && (value.startsWith('http') || value.startsWith('/'))) {
2398
- relationships.push({ from: url, to: value, type: key });
2399
- }
2400
- if (Array.isArray(value)) {
2401
- for (const item of value) {
2402
- if (typeof item === 'string' && (item.startsWith('http') || item.startsWith('/'))) {
2403
- relationships.push({ from: url, to: item, type: key });
2404
- }
2405
- }
2406
- }
2407
- }
2408
- return relationships;
2409
- };
2410
-
2411
- const withRelationships = (doc) => {
2412
- return { ...doc, relationships: extractRelationships(doc) };
2413
- };
2414
-
2415
- const resolveUrl = (entity) => {
2416
- if (entity.url) return entity.url;
2417
- if (entity.ns && entity.type && entity.id) {
2418
- return 'https://' + entity.ns + '/' + entity.type + '/' + entity.id;
2419
- }
2420
- if (entity.ns && entity.id) {
2421
- return 'https://' + entity.ns + '/' + entity.id;
2422
- }
2423
- return null;
2424
- };
2425
-
2426
- // Context object ($) - unified SDK context (aligned with ai-workflows WorkflowContext)
2427
- const $ = {
2428
- ns: __SDK_CONFIG__.ns,
2429
- db,
2430
- ai,
2431
- on,
2432
- every,
2433
- send,
2434
- do: __doEvent__,
2435
- try: __tryEvent__,
2436
- queue,
2437
- actor,
2438
- actions,
2439
- artifacts,
2440
- events,
2441
- decide,
2442
- track,
2443
- experiment,
2444
- delay,
2445
- // Workflow state
2446
- state: {},
2447
- history: __workflow_history__,
2448
- // User context
2449
- user: { id: 'test-user', name: 'Test User', role: 'admin' },
2450
- request: { method: 'GET', path: '/', headers: {}, body: null },
2451
- env: { NODE_ENV: 'test' },
2452
- config: {},
2453
- context: {},
2454
- meta: {},
2455
- // Logging
2456
- log: (message, data) => {
2457
- __workflow_history__.push({ type: 'action', name: 'log', data: { message, data }, timestamp: Date.now() });
2458
- console.log('[sdk] ' + message, data ?? '');
2459
- },
2460
- error: console.error,
2461
- warn: console.warn,
2462
- // Scoped execution
2463
- async scope(overrides, fn) {
2464
- const prev = { ns: $.ns, user: $.user, state: { ...$.state } };
2465
- Object.assign($, overrides);
2466
- try { return await fn(); }
2467
- finally { Object.assign($, prev); }
2468
- },
2469
- // Workflow helpers
2470
- getHandlers() { return { events: Array.from(__event_handlers__.keys()), schedules: __schedule_handlers__.length }; },
2471
- clearHandlers() { __event_handlers__.clear(); __schedule_handlers__.length = 0; },
2472
- getHistory() { return [...__workflow_history__]; },
2473
- clearHistory() { __workflow_history__.length = 0; }
2474
- };
2475
-
2476
- // Standalone exports (for import { db, ai, on, send } from 'sdk')
2477
- const api = {};
2478
- const search = __db_core__.search.bind(__db_core__);
2479
- `
2480
- }
2481
-
2482
- /**
2483
- * Generate remote SDK code (RPC-based)
2484
- *
2485
- * This creates the platform.do-style SDK with:
2486
- * - $ - Root context accessor
2487
- * - db - Database operations
2488
- * - ai - AI operations
2489
- * - api - Platform API
2490
- * - on/send - Event handling
2491
- *
2492
- * All operations go through RPC to actual services.
2493
- */
2494
- function generateRemoteSDKCode(config: SDKConfig = {}): string {
2495
- const rpcUrl = config.rpcUrl || 'https://rpc.do'
2496
- const token = config.token || ''
2497
- const ns = config.ns || 'default'
2498
-
2499
- return `
2500
- // ============================================================
2501
- // SDK - Thin RPC Proxy ($, db, ai, api, on, send)
2502
- // ============================================================
2503
-
2504
- const __SDK_CONFIG__ = {
2505
- rpcUrl: '${rpcUrl}',
2506
- token: '${token}',
2507
- ns: '${ns}'
2508
- };
2509
-
2510
- // HTTP RPC client
2511
- const __rpc__ = {
2512
- async do(path, ...args) {
2513
- const headers = { 'Content-Type': 'application/json' };
2514
- if (__SDK_CONFIG__.token) {
2515
- headers['Authorization'] = 'Bearer ' + __SDK_CONFIG__.token;
2516
- }
2517
-
2518
- // Serialize functions for remote execution
2519
- const serializedArgs = args.map(arg => {
2520
- if (typeof arg === 'function') {
2521
- return { __fn: arg.toString().replace(/"/g, "'") };
2522
- }
2523
- return arg;
2524
- });
2525
-
2526
- const response = await fetch(__SDK_CONFIG__.rpcUrl + '/rpc', {
2527
- method: 'POST',
2528
- headers,
2529
- body: JSON.stringify({ method: 'do', path, args: serializedArgs })
2530
- });
2531
-
2532
- if (!response.ok) {
2533
- const errorData = await response.json().catch(() => ({}));
2534
- throw new Error(errorData.error || 'RPC error: ' + response.statusText);
2535
- }
2536
-
2537
- return response.json();
2538
- }
2539
- };
2540
-
2541
- // Store for user-defined values
2542
- const __userDefinitions__ = new Map();
2543
-
2544
- // Thin proxy that converts property access to RPC paths
2545
- const __createProxy__ = (path = '') => {
2546
- // Track stored values for this proxy instance
2547
- const localStore = new Map();
2548
-
2549
- const proxy = new Proxy(() => {}, {
2550
- get: (target, prop, receiver) => {
2551
- // Handle JSON serialization
2552
- if (prop === 'toJSON') {
2553
- // Return stored values as a plain object
2554
- const obj = { __rpcPath: path };
2555
- for (const [key, value] of localStore) {
2556
- obj[key] = value;
2557
- }
2558
- for (const [key, value] of __userDefinitions__) {
2559
- if (key.startsWith(path ? path + '.' : '') && !key.slice(path ? path.length + 1 : 0).includes('.')) {
2560
- const localKey = key.slice(path ? path.length + 1 : 0);
2561
- if (localKey) obj[localKey] = value;
2562
- }
2563
- }
2564
- return () => obj;
2565
- }
2566
- if (prop === Symbol.toPrimitive || prop === 'valueOf' || prop === 'toString') {
2567
- return () => path || '[SDK Proxy]';
2568
- }
2569
- if (prop === Symbol.toStringTag) {
2570
- return 'SDKProxy';
2571
- }
2572
- if (prop === 'then' || prop === 'catch' || prop === 'finally') {
2573
- return undefined; // Don't treat as thenable
2574
- }
2575
- // Handle .should by creating an assertion chain
2576
- if (prop === 'should') {
2577
- // Build an object from stored values for assertion
2578
- const obj = {};
2579
- for (const [key, value] of localStore) {
2580
- obj[key] = value;
2581
- }
2582
- for (const [key, value] of __userDefinitions__) {
2583
- if (key.startsWith(path ? path + '.' : '') && !key.slice(path ? path.length + 1 : 0).includes('.')) {
2584
- const localKey = key.slice(path ? path.length + 1 : 0);
2585
- if (localKey) obj[localKey] = value;
2586
- }
2587
- }
2588
- // If we have stored values, assert on them; otherwise use path string or a marker
2589
- if (Object.keys(obj).length > 0) {
2590
- return __createShouldChain__(obj);
2591
- }
2592
- // For empty proxy, create a marker object that represents the proxy path
2593
- return __createShouldChain__(path ? { __path: path } : { __sdk: true, ns: __SDK_CONFIG__.ns });
2594
- }
2595
-
2596
- const fullPath = path ? path + '.' + String(prop) : String(prop);
2597
-
2598
- // Check local store first, then user definitions
2599
- if (localStore.has(String(prop))) {
2600
- return localStore.get(String(prop));
2601
- }
2602
- if (__userDefinitions__.has(fullPath)) {
2603
- return __userDefinitions__.get(fullPath);
2604
- }
2605
-
2606
- return __createProxy__(fullPath);
2607
- },
2608
-
2609
- set: (_, prop, value) => {
2610
- const fullPath = path ? path + '.' + String(prop) : String(prop);
2611
- localStore.set(String(prop), value);
2612
- __userDefinitions__.set(fullPath, value);
2613
- return true;
2614
- },
2615
-
2616
- apply: (_, __, args) => {
2617
- // Handle tagged template literals
2618
- if (Array.isArray(args[0]) && 'raw' in args[0]) {
2619
- const strings = args[0];
2620
- const values = args.slice(1);
2621
- const text = strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '');
2622
- return __rpc__.do(path, text);
2623
- }
2624
-
2625
- return __rpc__.do(path, ...args);
2626
- },
2627
-
2628
- // Prevent enumeration from causing infinite loops
2629
- ownKeys: () => {
2630
- const keys = [];
2631
- for (const [key, value] of localStore) {
2632
- keys.push(key);
2633
- }
2634
- for (const [key, value] of __userDefinitions__) {
2635
- if (key.startsWith(path ? path + '.' : '') && !key.slice(path ? path.length + 1 : 0).includes('.')) {
2636
- const localKey = key.slice(path ? path.length + 1 : 0);
2637
- if (localKey && !keys.includes(localKey)) keys.push(localKey);
2638
- }
2639
- }
2640
- return keys;
2641
- },
2642
-
2643
- getOwnPropertyDescriptor: (_, prop) => {
2644
- const fullPath = path ? path + '.' + String(prop) : String(prop);
2645
- if (localStore.has(String(prop)) || __userDefinitions__.has(fullPath)) {
2646
- return { configurable: true, enumerable: true, writable: true };
2647
- }
2648
- return undefined;
2649
- }
2650
- });
2651
-
2652
- return proxy;
2653
- };
2654
-
2655
- // Root proxy and named exports
2656
- const $ = __createProxy__();
2657
- const db = $.db;
2658
- const ai = $.ai;
2659
- const api = $.api;
2660
- const on = $.on;
2661
- const send = $.send;
2662
- const search = $.search;
2663
- const track = $.track;
2664
- const every = $.every;
2665
- const decide = $.decide;
2666
-
2667
- // Set default namespace and context properties
2668
- $.ns = __SDK_CONFIG__.ns;
2669
- $.user = { id: 'anonymous', name: 'Anonymous', role: 'guest' };
2670
- $.request = { method: 'GET', path: '/', headers: {}, body: null };
2671
- $.env = typeof process !== 'undefined' ? (process.env || {}) : {};
2672
- $.config = {};
2673
- $.context = {};
2674
- $.meta = {};
2675
- `
2676
- }
2677
-
2678
- /**
2679
- * Generate .should chainable assertions code
2680
- */
2681
- function generateShouldCode(): string {
2682
- return `
2683
- // ============================================================
2684
- // Global .should Chainable Assertions
2685
- // ============================================================
2686
-
2687
- const __createShouldChain__ = (actual, negated = false) => {
2688
- const check = (condition, message) => {
2689
- const passes = negated ? !condition : condition;
2690
- if (!passes) throw new Error(negated ? 'Expected NOT: ' + message : message);
2691
- };
2692
-
2693
- const stringify = (val) => {
2694
- try {
2695
- return JSON.stringify(val);
2696
- } catch {
2697
- return String(val);
2698
- }
2699
- };
2700
-
2701
- // Create a lazy chain getter - returns 'this' assertion for chaining
2702
- const assertion = {};
2703
-
2704
- // Core assertion methods
2705
- assertion.equal = (expected) => {
2706
- check(actual === expected, 'Expected ' + stringify(actual) + ' to equal ' + stringify(expected));
2707
- return assertion;
2708
- };
2709
- assertion.deep = {
2710
- equal: (expected) => {
2711
- check(stringify(actual) === stringify(expected), 'Expected deep equal to ' + stringify(expected));
2712
- return assertion;
2713
- },
2714
- include: (expected) => {
2715
- const actualStr = stringify(actual);
2716
- const expectedStr = stringify(expected);
2717
- // Check if expected properties exist with same values
2718
- const matches = Object.entries(expected || {}).every(([k, v]) =>
2719
- actual && stringify(actual[k]) === stringify(v)
2720
- );
2721
- check(matches, 'Expected ' + actualStr + ' to deeply include ' + expectedStr);
2722
- return assertion;
2723
- }
2724
- };
2725
- assertion.include = (value) => {
2726
- if (typeof actual === 'string') check(actual.includes(String(value)), 'Expected "' + actual + '" to include "' + value + '"');
2727
- else if (Array.isArray(actual)) check(actual.includes(value), 'Expected array to include ' + stringify(value));
2728
- return assertion;
2729
- };
2730
- assertion.contain = assertion.include;
2731
- assertion.lengthOf = (n) => {
2732
- check(actual?.length === n, 'Expected length ' + n + ', got ' + actual?.length);
2733
- return assertion;
2734
- };
2735
- assertion.match = (regex) => {
2736
- const str = String(actual);
2737
- check(regex.test(str), 'Expected "' + str + '" to match ' + regex);
2738
- return assertion;
2739
- };
2740
- assertion.matches = assertion.match;
2741
-
2742
- // .be accessor with type checks
2743
- Object.defineProperty(assertion, 'be', {
2744
- get: () => {
2745
- const beObj = {
2746
- a: (type) => {
2747
- const actualType = actual === null ? 'null' : Array.isArray(actual) ? 'array' : actual instanceof Date ? 'date' : typeof actual;
2748
- check(actualType === type.toLowerCase(), 'Expected ' + stringify(actual) + ' to be a ' + type);
2749
- return assertion;
2750
- },
2751
- above: (n) => { check(actual > n, 'Expected ' + actual + ' to be above ' + n); return assertion; },
2752
- below: (n) => { check(actual < n, 'Expected ' + actual + ' to be below ' + n); return assertion; },
2753
- within: (min, max) => { check(actual >= min && actual <= max, 'Expected ' + actual + ' to be within ' + min + '..' + max); return assertion; },
2754
- oneOf: (arr) => { check(Array.isArray(arr) && arr.includes(actual), 'Expected ' + stringify(actual) + ' to be one of ' + stringify(arr)); return assertion; },
2755
- instanceOf: (cls) => { check(actual instanceof cls, 'Expected to be instance of ' + cls.name); return assertion; }
2756
- };
2757
- beObj.an = beObj.a;
2758
- Object.defineProperty(beObj, 'true', { get: () => { check(actual === true, 'Expected ' + stringify(actual) + ' to be true'); return assertion; } });
2759
- Object.defineProperty(beObj, 'false', { get: () => { check(actual === false, 'Expected ' + stringify(actual) + ' to be false'); return assertion; } });
2760
- Object.defineProperty(beObj, 'ok', { get: () => { check(!!actual, 'Expected ' + stringify(actual) + ' to be truthy'); return assertion; } });
2761
- Object.defineProperty(beObj, 'null', { get: () => { check(actual === null, 'Expected ' + stringify(actual) + ' to be null'); return assertion; } });
2762
- Object.defineProperty(beObj, 'undefined', { get: () => { check(actual === undefined, 'Expected ' + stringify(actual) + ' to be undefined'); return assertion; } });
2763
- Object.defineProperty(beObj, 'empty', { get: () => {
2764
- const isEmpty = actual === '' || (Array.isArray(actual) && actual.length === 0) || (actual && typeof actual === 'object' && Object.keys(actual).length === 0);
2765
- check(isEmpty, 'Expected ' + stringify(actual) + ' to be empty');
2766
- return assertion;
2767
- }});
2768
- return beObj;
2769
- }
2770
- });
2771
-
2772
- // .have accessor with property/keys/lengthOf/at checks
2773
- Object.defineProperty(assertion, 'have', {
2774
- get: () => ({
2775
- property: (name, value) => {
2776
- const hasIt = actual != null && Object.prototype.hasOwnProperty.call(actual, name);
2777
- if (value !== undefined) {
2778
- check(hasIt && actual[name] === value, "Expected property '" + name + "' = " + stringify(value) + ", got " + stringify(actual?.[name]));
2779
- } else {
2780
- check(hasIt, "Expected to have property '" + name + "'");
2781
- }
2782
- if (hasIt) return __createShouldChain__(actual[name], negated);
2783
- return assertion;
2784
- },
2785
- keys: (...keys) => {
2786
- const actualKeys = Object.keys(actual || {});
2787
- check(keys.every(k => actualKeys.includes(k)), 'Expected to have keys ' + stringify(keys));
2788
- return assertion;
2789
- },
2790
- lengthOf: (n) => {
2791
- check(actual?.length === n, 'Expected length ' + n + ', got ' + actual?.length);
2792
- return assertion;
2793
- },
2794
- at: {
2795
- least: (n) => {
2796
- check(actual?.length >= n, 'Expected length at least ' + n + ', got ' + actual?.length);
2797
- return assertion;
2798
- },
2799
- most: (n) => {
2800
- check(actual?.length <= n, 'Expected length at most ' + n + ', got ' + actual?.length);
2801
- return assertion;
2802
- }
2803
- }
2804
- })
2805
- });
2806
-
2807
- // .not negation
2808
- Object.defineProperty(assertion, 'not', {
2809
- get: () => __createShouldChain__(actual, !negated)
2810
- });
2811
-
2812
- // .with passthrough for readability
2813
- Object.defineProperty(assertion, 'with', {
2814
- get: () => assertion
2815
- });
2816
-
2817
- // .that passthrough for chaining (e.g. .have.property('x').that.matches(/.../) )
2818
- Object.defineProperty(assertion, 'that', {
2819
- get: () => assertion
2820
- });
2821
-
2822
- // .and passthrough for chaining
2823
- Object.defineProperty(assertion, 'and', {
2824
- get: () => assertion
2825
- });
2826
-
2827
- return assertion;
2828
- };
2829
-
2830
- // Add .should to Object.prototype
2831
- Object.defineProperty(Object.prototype, 'should', {
2832
- get: function() { return __createShouldChain__(this); },
2833
- configurable: true,
2834
- enumerable: false
2835
- });
2836
- `
2837
- }
2838
-
2839
- /**
2840
- * Extract export names from module code
2841
- * Supports both CommonJS (exports.foo) and ES module (export const foo) syntax
2842
- */
2843
- function getExportNames(moduleCode: string): string {
2844
- const names = new Set<string>()
2845
-
2846
- // Match exports.name = ...
2847
- const dotPattern = /exports\.(\w+)\s*=/g
2848
- let match
2849
- while ((match = dotPattern.exec(moduleCode)) !== null) {
2850
- if (match[1]) names.add(match[1])
2851
- }
2852
-
2853
- // Match exports['name'] = ... or exports["name"] = ...
2854
- const bracketPattern = /exports\[['"](\w+)['"]\]\s*=/g
2855
- while ((match = bracketPattern.exec(moduleCode)) !== null) {
2856
- if (match[1]) names.add(match[1])
2857
- }
2858
-
2859
- // Match export const name = ... or export let name = ... or export var name = ...
2860
- const esConstPattern = /export\s+(?:const|let|var)\s+(\w+)\s*=/g
2861
- while ((match = esConstPattern.exec(moduleCode)) !== null) {
2862
- if (match[1]) names.add(match[1])
2863
- }
2864
-
2865
- // Match export function name(...) or export async function name(...) or export async function* name(...)
2866
- const esFunctionPattern = /export\s+(?:async\s+)?function\*?\s+(\w+)\s*\(/g
2867
- while ((match = esFunctionPattern.exec(moduleCode)) !== null) {
2868
- if (match[1]) names.add(match[1])
2869
- }
2870
-
2871
- // Match export class name
2872
- const esClassPattern = /export\s+class\s+(\w+)/g
2873
- while ((match = esClassPattern.exec(moduleCode)) !== null) {
2874
- if (match[1]) names.add(match[1])
2875
- }
2876
-
2877
- return Array.from(names).join(', ') || '_unused'
2878
- }
2879
-
2880
- /**
2881
- * Transform module code to work in sandbox
2882
- * Converts ES module exports to CommonJS-style for the sandbox
2883
- */
2884
- function transformModuleCode(moduleCode: string): string {
2885
- let code = moduleCode
2886
-
2887
- // Transform: export const foo = ... → const foo = ...; exports.foo = foo;
2888
- code = code.replace(/export\s+(const|let|var)\s+(\w+)\s*=/g, '$1 $2 = exports.$2 =')
2889
-
2890
- // Transform: export function foo(...) → function foo(...) exports.foo = foo;
2891
- // Also handles async generators: export async function* foo
2892
- code = code.replace(/export\s+(async\s+)?function(\*?)\s+(\w+)/g, '$1function$2 $3')
2893
- // Add exports for functions after their definition
2894
- const funcNames = [...moduleCode.matchAll(/export\s+(?:async\s+)?function\*?\s+(\w+)/g)]
2895
- for (const [, name] of funcNames) {
2896
- code += `\nexports.${name} = ${name};`
2897
- }
2898
-
2899
- // Transform: export class Foo → class Foo; exports.Foo = Foo;
2900
- code = code.replace(/export\s+class\s+(\w+)/g, 'class $1')
2901
- const classNames = [...moduleCode.matchAll(/export\s+class\s+(\w+)/g)]
2902
- for (const [, name] of classNames) {
2903
- code += `\nexports.${name} = ${name};`
2904
- }
2905
-
2906
- return code
2907
- }
2908
-
2909
- /**
2910
- * Wrap script to auto-return the last expression
2911
- * Converts: `add(1, 2)` → `return add(1, 2)`
2912
- */
2913
- function wrapScriptForReturn(script: string): string {
2914
- const trimmed = script.trim()
2915
- if (!trimmed) return script
2916
-
2917
- // If script already contains a return statement anywhere, don't modify
2918
- if (/\breturn\b/.test(trimmed)) return script
2919
-
2920
- // If script starts with throw, don't modify
2921
- if (/^\s*throw\b/.test(trimmed)) return script
2922
-
2923
- // If it's a single expression (no newlines, no semicolons except at end), wrap it
2924
- const withoutTrailingSemi = trimmed.replace(/;?\s*$/, '')
2925
- const isSingleLine = !withoutTrailingSemi.includes('\n')
2926
-
2927
- // Check if it looks like a single expression (no control flow, no declarations)
2928
- const startsWithKeyword =
2929
- /^\s*(const|let|var|if|for|while|switch|try|class|function|async\s+function)\b/.test(
2930
- withoutTrailingSemi
2931
- )
2932
-
2933
- if (isSingleLine && !startsWithKeyword) {
2934
- return `return ${withoutTrailingSemi}`
2935
- }
2936
-
2937
- // For multi-statement scripts, try to return the last expression
2938
- const lines = trimmed.split('\n')
2939
- const lastLineRaw = lines[lines.length - 1]
2940
- if (!lastLineRaw) return script
2941
- const lastLine = lastLineRaw.trim()
2942
-
2943
- // If last line is an expression (not a declaration, control flow, or throw)
2944
- if (
2945
- lastLine &&
2946
- !/^\s*(const|let|var|if|for|while|switch|try|class|function|return|throw)\b/.test(lastLine)
2947
- ) {
2948
- lines[lines.length - 1] = `return ${lastLine.replace(/;?\s*$/, '')}`
2949
- return lines.join('\n')
2950
- }
2951
-
2952
- return script
2953
- }
2954
-
2955
- /**
2956
- * Generate worker code for production (uses RPC to ai-tests)
2957
- */
2958
- export function generateWorkerCode(options: {
2959
- module?: string | undefined
2960
- tests?: string | undefined
2961
- script?: string | undefined
2962
- sdk?: SDKConfig | boolean | undefined
2963
- imports?: string[] | undefined
2964
- }): string {
2965
- const { module: rawModule = '', tests = '', script: rawScript = '', sdk, imports = [] } = options
2966
- const sdkConfig = sdk === true ? {} : sdk || null
2967
- const module = rawModule ? transformModuleCode(rawModule) : ''
2968
- const script = rawScript ? wrapScriptForReturn(rawScript) : ''
2969
- const exportNames = getExportNames(rawModule)
2970
-
2971
- // Hoisted imports (from MDX test files) - placed at true module top level
2972
- const hoistedImports = imports.length > 0 ? imports.join('\n') + '\n' : ''
2973
-
2974
- return `
2975
- // Sandbox Worker Entry Point
2976
- import { RpcTarget, newWorkersRpcResponse } from 'capnweb.js';
2977
- ${hoistedImports}
2978
- const logs = [];
2979
-
2980
- ${sdkConfig ? generateShouldCode() : ''}
2981
-
2982
- ${sdkConfig ? generateSDKCode(sdkConfig) : '// SDK not enabled'}
2983
-
2984
- // Capture console output
2985
- const originalConsole = { ...console };
2986
- const captureConsole = (level) => (...args) => {
2987
- logs.push({
2988
- level,
2989
- message: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' '),
2990
- timestamp: Date.now()
2991
- });
2992
- originalConsole[level](...args);
2993
- };
2994
- console.log = captureConsole('log');
2995
- console.warn = captureConsole('warn');
2996
- console.error = captureConsole('error');
2997
- console.info = captureConsole('info');
2998
- console.debug = captureConsole('debug');
2999
-
3000
- // ============================================================
3001
- // USER MODULE CODE (embedded at generation time)
3002
- // ============================================================
3003
- // Module exports object - exports become top-level variables
3004
- const exports = {};
3005
-
3006
- ${
3007
- module
3008
- ? `
3009
- // Execute module code
3010
- try {
3011
- ${module}
3012
- } catch (e) {
3013
- console.error('Module error:', e.message);
3014
- }
3015
- `
3016
- : '// No module code provided'
3017
- }
3018
-
3019
- // Expose all exports as top-level variables for tests and scripts
3020
- // This allows: export const add = (a, b) => a + b; then later: add(1, 2)
3021
- ${
3022
- rawModule
3023
- ? `
3024
- const { ${exportNames} } = exports;
3025
- `.trim()
3026
- : ''
3027
- }
3028
-
3029
- // ============================================================
3030
- // RPC SERVER - Expose exports via capnweb
3031
- // ============================================================
3032
- class ExportsRpcTarget extends RpcTarget {
3033
- // Dynamically expose all exports as RPC methods
3034
- constructor() {
3035
- super();
3036
- for (const [key, value] of Object.entries(exports)) {
3037
- if (typeof value === 'function') {
3038
- this[key] = value;
3039
- }
3040
- }
3041
- }
3042
-
3043
- // List available exports
3044
- list() {
3045
- return Object.keys(exports);
3046
- }
3047
-
3048
- // Get an export by name
3049
- get(name) {
3050
- return exports[name];
3051
- }
3052
- }
3053
-
3054
- // ============================================================
3055
- // WORKER ENTRY POINT
3056
- // ============================================================
3057
- export default {
3058
- async fetch(request, env) {
3059
- const url = new URL(request.url);
3060
-
3061
- // Route: GET / - Return info about exports
3062
- if (request.method === 'GET' && url.pathname === '/') {
3063
- return Response.json({
3064
- exports: Object.keys(exports),
3065
- rpc: '/rpc',
3066
- execute: '/execute'
3067
- });
3068
- }
3069
-
3070
- // Route: /rpc - capnweb RPC to module exports
3071
- if (url.pathname === '/rpc') {
3072
- return newWorkersRpcResponse(request, new ExportsRpcTarget());
3073
- }
3074
-
3075
- // Route: GET /:name - Simple JSON endpoint to access exports
3076
- if (request.method === 'GET' && url.pathname !== '/execute') {
3077
- const name = url.pathname.slice(1); // Remove leading /
3078
- const value = exports[name];
3079
-
3080
- // Check if export exists
3081
- if (!(name in exports)) {
3082
- return Response.json({ error: \`Export "\${name}" not found\` }, { status: 404 });
3083
- }
3084
-
3085
- // If it's not a function, just return the value
3086
- if (typeof value !== 'function') {
3087
- return Response.json({ result: value });
3088
- }
3089
-
3090
- // It's a function - parse args and call it
3091
- try {
3092
- const args = [];
3093
- const argsParam = url.searchParams.get('args');
3094
- if (argsParam) {
3095
- // Support JSON array: ?args=[1,2,3]
3096
- try {
3097
- const parsed = JSON.parse(argsParam);
3098
- if (Array.isArray(parsed)) {
3099
- args.push(...parsed);
3100
- } else {
3101
- args.push(parsed);
3102
- }
3103
- } catch {
3104
- // Not JSON, use as single string arg
3105
- args.push(argsParam);
3106
- }
3107
- } else {
3108
- // Support named params: ?a=1&b=2 -> passed as object
3109
- const params = Object.fromEntries(url.searchParams.entries());
3110
- if (Object.keys(params).length > 0) {
3111
- // Try to parse numeric values
3112
- for (const [key, val] of Object.entries(params)) {
3113
- const num = Number(val);
3114
- params[key] = !isNaN(num) && val !== '' ? num : val;
3115
- }
3116
- args.push(params);
3117
- }
3118
- }
3119
-
3120
- const result = await value(...args);
3121
- return Response.json({ result });
3122
- } catch (e) {
3123
- return Response.json({ error: e.message }, { status: 500 });
3124
- }
3125
- }
3126
-
3127
- // Route: /execute - Run tests and scripts
3128
- // Check for TEST service binding
3129
- if (!env.TEST) {
3130
- return Response.json({
3131
- success: false,
3132
- error: 'TEST service binding not available. Ensure ai-tests worker is bound.',
3133
- logs,
3134
- duration: 0
3135
- });
3136
- }
3137
-
3138
- // Connect to get the TestServiceCore via RPC
3139
- const testService = await env.TEST.connect();
3140
-
3141
- // Create global test functions that proxy to the RPC service
3142
- const describe = (name, fn) => testService.describe(name, fn);
3143
- const it = (name, fn) => testService.it(name, fn);
3144
- const test = (name, fn) => testService.test(name, fn);
3145
- const expect = (value, message) => testService.expect(value, message);
3146
- const should = (value) => testService.should(value);
3147
- const assert = testService.assert;
3148
- const beforeEach = (fn) => testService.beforeEach(fn);
3149
- const afterEach = (fn) => testService.afterEach(fn);
3150
- const beforeAll = (fn) => testService.beforeAll(fn);
3151
- const afterAll = (fn) => testService.afterAll(fn);
3152
-
3153
- // Add skip/only modifiers
3154
- it.skip = (name, fn) => testService.skip(name, fn);
3155
- it.only = (name, fn) => testService.only(name, fn);
3156
- test.skip = it.skip;
3157
- test.only = it.only;
3158
-
3159
- let scriptResult = undefined;
3160
- let scriptError = null;
3161
- let testResults = undefined;
3162
-
3163
- // ============================================================
3164
- // USER TEST CODE (embedded at generation time)
3165
- // ============================================================
3166
-
3167
- ${
3168
- tests
3169
- ? `
3170
- // Register tests
3171
- try {
3172
- ${tests}
3173
- } catch (e) {
3174
- console.error('Test registration error:', e.message);
3175
- }
3176
- `
3177
- : '// No test code provided'
3178
- }
3179
-
3180
- // Execute user script
3181
- ${
3182
- script
3183
- ? `
3184
- try {
3185
- scriptResult = await (async () => {
3186
- ${script}
3187
- })();
3188
- } catch (e) {
3189
- console.error('Script error:', e.message);
3190
- scriptError = e.message;
3191
- }
3192
- `
3193
- : '// No script code provided'
3194
- }
3195
-
3196
- // Run tests if any were registered
3197
- ${
3198
- tests
3199
- ? `
3200
- try {
3201
- testResults = await testService.run();
3202
- } catch (e) {
3203
- console.error('Test run error:', e.message);
3204
- testResults = { total: 0, passed: 0, failed: 1, skipped: 0, tests: [], duration: 0, error: e.message };
3205
- }
3206
- `
3207
- : ''
3208
- }
3209
-
3210
- const hasTests = ${tests ? 'true' : 'false'};
3211
- const success = scriptError === null && (!hasTests || (testResults && testResults.failed === 0));
3212
-
3213
- return Response.json({
3214
- success,
3215
- value: scriptResult,
3216
- logs,
3217
- testResults: hasTests ? testResults : undefined,
3218
- error: scriptError || undefined,
3219
- duration: 0
3220
- });
3221
- }
3222
- };
3223
- `
3224
- }
3225
-
3226
- /**
3227
- * Generate worker code for development (embedded test framework)
3228
- *
3229
- * This version bundles the test framework directly into the worker,
3230
- * avoiding the need for RPC service bindings in local development.
3231
- */
3232
- export function generateDevWorkerCode(options: {
3233
- module?: string | undefined
3234
- tests?: string | undefined
3235
- script?: string | undefined
3236
- sdk?: SDKConfig | boolean | undefined
3237
- imports?: string[] | undefined
3238
- fetch?: null | undefined
3239
- }): string {
3240
- const {
3241
- module: rawModule = '',
3242
- tests = '',
3243
- script: rawScript = '',
3244
- sdk,
3245
- imports = [],
3246
- fetch: fetchOption,
3247
- } = options
3248
- const sdkConfig = sdk === true ? {} : sdk || null
3249
- const module = rawModule ? transformModuleCode(rawModule) : ''
3250
- const script = rawScript ? wrapScriptForReturn(rawScript) : ''
3251
- const exportNames = getExportNames(rawModule)
3252
- const blockFetch = fetchOption === null
3253
-
3254
- // Hoisted imports (from MDX test files) - placed at true module top level
3255
- const hoistedImports = imports.length > 0 ? imports.join('\n') + '\n' : ''
3256
-
3257
- return `
3258
- // Sandbox Worker Entry Point (Dev Mode - embedded test framework)
3259
- ${hoistedImports}
3260
- const logs = [];
3261
- const testResults = { total: 0, passed: 0, failed: 0, skipped: 0, tests: [], duration: 0 };
3262
- const pendingTests = [];
3263
-
3264
- ${
3265
- blockFetch
3266
- ? `
3267
- // Block fetch when fetch: null is specified
3268
- const __originalFetch__ = globalThis.fetch;
3269
- globalThis.fetch = async (...args) => {
3270
- throw new Error('Network access blocked: fetch is disabled in this sandbox');
3271
- };
3272
- `
3273
- : ''
3274
- }
3275
-
3276
- ${sdkConfig ? generateShouldCode() : ''}
3277
-
3278
- ${sdkConfig ? generateSDKCode(sdkConfig) : '// SDK not enabled'}
3279
-
3280
- // Capture console output
3281
- const originalConsole = { ...console };
3282
- const captureConsole = (level) => (...args) => {
3283
- logs.push({
3284
- level,
3285
- message: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' '),
3286
- timestamp: Date.now()
3287
- });
3288
- originalConsole[level](...args);
3289
- };
3290
- console.log = captureConsole('log');
3291
- console.warn = captureConsole('warn');
3292
- console.error = captureConsole('error');
3293
- console.info = captureConsole('info');
3294
- console.debug = captureConsole('debug');
3295
-
3296
- // Test framework (vitest-compatible API)
3297
- let currentDescribe = '';
3298
- let beforeEachFns = [];
3299
- let afterEachFns = [];
3300
-
3301
- const describe = (name, fn) => {
3302
- const prev = currentDescribe;
3303
- const prevBeforeEach = [...beforeEachFns];
3304
- const prevAfterEach = [...afterEachFns];
3305
- currentDescribe = currentDescribe ? currentDescribe + ' > ' + name : name;
3306
- try { fn(); } finally {
3307
- currentDescribe = prev;
3308
- beforeEachFns = prevBeforeEach;
3309
- afterEachFns = prevAfterEach;
3310
- }
3311
- };
3312
-
3313
- // Hooks
3314
- const beforeEach = (fn) => { beforeEachFns.push(fn); };
3315
- const afterEach = (fn) => { afterEachFns.push(fn); };
3316
-
3317
- const it = (name, fn) => {
3318
- const fullName = currentDescribe ? currentDescribe + ' > ' + name : name;
3319
- const hooks = { before: [...beforeEachFns], after: [...afterEachFns] };
3320
- pendingTests.push({ name: fullName, fn, hooks });
3321
- };
3322
- const test = it;
3323
-
3324
- it.skip = (name, fn) => {
3325
- const fullName = currentDescribe ? currentDescribe + ' > ' + name : name;
3326
- pendingTests.push({ name: fullName, fn: null, skip: true });
3327
- };
3328
- test.skip = it.skip;
3329
-
3330
- it.only = (name, fn) => {
3331
- const fullName = currentDescribe ? currentDescribe + ' > ' + name : name;
3332
- const hooks = { before: [...beforeEachFns], after: [...afterEachFns] };
3333
- pendingTests.push({ name: fullName, fn, hooks, only: true });
3334
- };
3335
- test.only = it.only;
3336
-
3337
- // Deep equality check
3338
- const deepEqual = (a, b) => {
3339
- if (a === b) return true;
3340
- if (a == null || b == null) return false;
3341
- if (typeof a !== typeof b) return false;
3342
- if (typeof a !== 'object') return false;
3343
- if (Array.isArray(a) !== Array.isArray(b)) return false;
3344
- if (Array.isArray(a)) {
3345
- if (a.length !== b.length) return false;
3346
- return a.every((v, i) => deepEqual(v, b[i]));
3347
- }
3348
- const keysA = Object.keys(a);
3349
- const keysB = Object.keys(b);
3350
- if (keysA.length !== keysB.length) return false;
3351
- return keysA.every(k => deepEqual(a[k], b[k]));
3352
- };
3353
-
3354
- // Expect implementation with vitest-compatible matchers
3355
- const expect = (actual) => {
3356
- const matchers = {
3357
- toBe: (expected) => {
3358
- if (actual !== expected) {
3359
- throw new Error(\`Expected \${JSON.stringify(expected)} but got \${JSON.stringify(actual)}\`);
3360
- }
3361
- },
3362
- toEqual: (expected) => {
3363
- if (!deepEqual(actual, expected)) {
3364
- throw new Error(\`Expected \${JSON.stringify(expected)} but got \${JSON.stringify(actual)}\`);
3365
- }
3366
- },
3367
- toStrictEqual: (expected) => {
3368
- if (!deepEqual(actual, expected)) {
3369
- throw new Error(\`Expected \${JSON.stringify(expected)} but got \${JSON.stringify(actual)}\`);
3370
- }
3371
- },
3372
- toBeTruthy: () => {
3373
- if (!actual) throw new Error(\`Expected truthy but got \${JSON.stringify(actual)}\`);
3374
- },
3375
- toBeFalsy: () => {
3376
- if (actual) throw new Error(\`Expected falsy but got \${JSON.stringify(actual)}\`);
3377
- },
3378
- toBeNull: () => {
3379
- if (actual !== null) throw new Error(\`Expected null but got \${JSON.stringify(actual)}\`);
3380
- },
3381
- toBeUndefined: () => {
3382
- if (actual !== undefined) throw new Error(\`Expected undefined but got \${JSON.stringify(actual)}\`);
3383
- },
3384
- toBeDefined: () => {
3385
- if (actual === undefined) throw new Error('Expected defined but got undefined');
3386
- },
3387
- toBeNaN: () => {
3388
- if (!Number.isNaN(actual)) throw new Error(\`Expected NaN but got \${actual}\`);
3389
- },
3390
- toContain: (item) => {
3391
- if (Array.isArray(actual)) {
3392
- if (!actual.includes(item)) throw new Error(\`Expected array to contain \${JSON.stringify(item)}\`);
3393
- } else if (typeof actual === 'string') {
3394
- if (!actual.includes(item)) throw new Error(\`Expected string to contain "\${item}"\`);
3395
- } else {
3396
- throw new Error('toContain only works on arrays and strings');
3397
- }
3398
- },
3399
- toContainEqual: (item) => {
3400
- if (!Array.isArray(actual)) throw new Error('toContainEqual only works on arrays');
3401
- if (!actual.some(v => deepEqual(v, item))) {
3402
- throw new Error(\`Expected array to contain \${JSON.stringify(item)}\`);
3403
- }
3404
- },
3405
- toHaveLength: (length) => {
3406
- if (actual?.length !== length) {
3407
- throw new Error(\`Expected length \${length} but got \${actual?.length}\`);
3408
- }
3409
- },
3410
- toHaveProperty: function(path, value) {
3411
- const parts = typeof path === 'string' ? path.split('.') : [path];
3412
- let obj = actual;
3413
- for (const part of parts) {
3414
- if (obj == null || !(part in obj)) {
3415
- throw new Error(\`Expected object to have property "\${path}"\`);
3416
- }
3417
- obj = obj[part];
3418
- }
3419
- if (arguments.length > 1 && !deepEqual(obj, value)) {
3420
- throw new Error(\`Expected property "\${path}" to be \${JSON.stringify(value)} but got \${JSON.stringify(obj)}\`);
3421
- }
3422
- },
3423
- toMatchObject: (expected) => {
3424
- if (typeof actual !== 'object' || actual === null) {
3425
- throw new Error('toMatchObject expects an object');
3426
- }
3427
- for (const key of Object.keys(expected)) {
3428
- if (!deepEqual(actual[key], expected[key])) {
3429
- throw new Error(\`Expected property "\${key}" to be \${JSON.stringify(expected[key])} but got \${JSON.stringify(actual[key])}\`);
3430
- }
3431
- }
3432
- },
3433
- toThrow: (expected) => {
3434
- if (typeof actual !== 'function') throw new Error('toThrow expects a function');
3435
- let threw = false;
3436
- let error;
3437
- try {
3438
- actual();
3439
- } catch (e) {
3440
- threw = true;
3441
- error = e;
3442
- }
3443
- if (!threw) throw new Error('Expected function to throw');
3444
- if (expected !== undefined) {
3445
- if (typeof expected === 'string' && !error.message.includes(expected)) {
3446
- throw new Error(\`Expected error message to contain "\${expected}" but got "\${error.message}"\`);
3447
- }
3448
- if (expected instanceof RegExp && !expected.test(error.message)) {
3449
- throw new Error(\`Expected error message to match \${expected} but got "\${error.message}"\`);
3450
- }
3451
- if (typeof expected === 'function' && !(error instanceof expected)) {
3452
- throw new Error(\`Expected error to be instance of \${expected.name}\`);
3453
- }
3454
- }
3455
- },
3456
- toBeGreaterThan: (n) => {
3457
- if (!(actual > n)) throw new Error(\`Expected \${actual} to be greater than \${n}\`);
3458
- },
3459
- toBeLessThan: (n) => {
3460
- if (!(actual < n)) throw new Error(\`Expected \${actual} to be less than \${n}\`);
3461
- },
3462
- toBeGreaterThanOrEqual: (n) => {
3463
- if (!(actual >= n)) throw new Error(\`Expected \${actual} to be >= \${n}\`);
3464
- },
3465
- toBeLessThanOrEqual: (n) => {
3466
- if (!(actual <= n)) throw new Error(\`Expected \${actual} to be <= \${n}\`);
3467
- },
3468
- toBeCloseTo: (n, digits = 2) => {
3469
- const diff = Math.abs(actual - n);
3470
- const threshold = Math.pow(10, -digits) / 2;
3471
- if (diff > threshold) {
3472
- throw new Error(\`Expected \${actual} to be close to \${n}\`);
3473
- }
3474
- },
3475
- toMatch: (pattern) => {
3476
- if (typeof actual !== 'string') throw new Error('toMatch expects a string');
3477
- const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
3478
- if (!regex.test(actual)) {
3479
- throw new Error(\`Expected "\${actual}" to match \${pattern}\`);
3480
- }
3481
- },
3482
- toBeInstanceOf: (cls) => {
3483
- if (!(actual instanceof cls)) {
3484
- throw new Error(\`Expected instance of \${cls.name}\`);
3485
- }
3486
- },
3487
- toBeTypeOf: (type) => {
3488
- if (typeof actual !== type) {
3489
- throw new Error(\`Expected typeof to be "\${type}" but got "\${typeof actual}"\`);
3490
- }
3491
- },
3492
- };
3493
-
3494
- matchers.not = {
3495
- toBe: (expected) => {
3496
- if (actual === expected) throw new Error(\`Expected not \${JSON.stringify(expected)}\`);
3497
- },
3498
- toEqual: (expected) => {
3499
- if (deepEqual(actual, expected)) {
3500
- throw new Error(\`Expected not equal to \${JSON.stringify(expected)}\`);
3501
- }
3502
- },
3503
- toBeTruthy: () => {
3504
- if (actual) throw new Error('Expected not truthy');
3505
- },
3506
- toBeFalsy: () => {
3507
- if (!actual) throw new Error('Expected not falsy');
3508
- },
3509
- toBeNull: () => {
3510
- if (actual === null) throw new Error('Expected not null');
3511
- },
3512
- toBeUndefined: () => {
3513
- if (actual === undefined) throw new Error('Expected not undefined');
3514
- },
3515
- toBeDefined: () => {
3516
- if (actual !== undefined) throw new Error('Expected undefined');
3517
- },
3518
- toContain: (item) => {
3519
- if (Array.isArray(actual) && actual.includes(item)) {
3520
- throw new Error(\`Expected array not to contain \${JSON.stringify(item)}\`);
3521
- }
3522
- if (typeof actual === 'string' && actual.includes(item)) {
3523
- throw new Error(\`Expected string not to contain "\${item}"\`);
3524
- }
3525
- },
3526
- toHaveProperty: (path) => {
3527
- const parts = typeof path === 'string' ? path.split('.') : [path];
3528
- let obj = actual;
3529
- try {
3530
- for (const part of parts) {
3531
- if (obj == null || !(part in obj)) return;
3532
- obj = obj[part];
3533
- }
3534
- throw new Error(\`Expected object not to have property "\${path}"\`);
3535
- } catch {}
3536
- },
3537
- toThrow: () => {
3538
- if (typeof actual !== 'function') throw new Error('toThrow expects a function');
3539
- try {
3540
- actual();
3541
- } catch (e) {
3542
- throw new Error('Expected function not to throw');
3543
- }
3544
- },
3545
- toMatch: (pattern) => {
3546
- if (typeof actual !== 'string') throw new Error('toMatch expects a string');
3547
- const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
3548
- if (regex.test(actual)) {
3549
- throw new Error(\`Expected "\${actual}" not to match \${pattern}\`);
3550
- }
3551
- },
3552
- };
3553
-
3554
- matchers.resolves = new Proxy({}, {
3555
- get: (_, prop) => async (...args) => {
3556
- const resolved = await actual;
3557
- return expect(resolved)[prop](...args);
3558
- }
3559
- });
3560
-
3561
- matchers.rejects = new Proxy({}, {
3562
- get: (_, prop) => async (...args) => {
3563
- try {
3564
- await actual;
3565
- throw new Error('Expected promise to reject');
3566
- } catch (e) {
3567
- if (e.message === 'Expected promise to reject') throw e;
3568
- return expect(e)[prop](...args);
3569
- }
3570
- }
3571
- });
3572
-
3573
- return matchers;
3574
- };
3575
-
3576
- // ============================================================
3577
- // USER MODULE CODE (embedded at generation time)
3578
- // ============================================================
3579
- // Module exports object - exports become top-level variables
3580
- const exports = {};
3581
-
3582
- ${
3583
- module
3584
- ? `
3585
- // Execute module code
3586
- try {
3587
- ${module}
3588
- } catch (e) {
3589
- console.error('Module error:', e.message);
3590
- }
3591
- `
3592
- : '// No module code provided'
3593
- }
3594
-
3595
- // Expose all exports as top-level variables for tests and scripts
3596
- // This allows: export const add = (a, b) => a + b; then later: add(1, 2)
3597
- ${
3598
- rawModule
3599
- ? `
3600
- const { ${exportNames} } = exports;
3601
- `.trim()
3602
- : ''
3603
- }
3604
-
3605
- // ============================================================
3606
- // USER TEST CODE (embedded at generation time)
3607
- // ============================================================
3608
- ${
3609
- tests
3610
- ? `
3611
- // Register tests
3612
- try {
3613
- ${tests}
3614
- } catch (e) {
3615
- console.error('Test registration error:', e.message);
3616
- }
3617
- `
3618
- : '// No test code provided'
3619
- }
3620
-
3621
- // ============================================================
3622
- // SIMPLE RPC HANDLER (dev mode - no capnweb dependency)
3623
- // ============================================================
3624
- async function handleRpc(request) {
3625
- try {
3626
- const { method, args = [] } = await request.json();
3627
- if (method === 'list') {
3628
- return Response.json({ result: Object.keys(exports) });
3629
- }
3630
- if (method === 'get') {
3631
- const [name] = args;
3632
- const value = exports[name];
3633
- if (typeof value === 'function') {
3634
- return Response.json({ result: { type: 'function', name } });
3635
- }
3636
- return Response.json({ result: value });
3637
- }
3638
- // Call an exported function
3639
- const fn = exports[method];
3640
- if (typeof fn !== 'function') {
3641
- return Response.json({ error: \`Export "\${method}" is not a function\` }, { status: 400 });
3642
- }
3643
- const result = await fn(...args);
3644
- return Response.json({ result });
3645
- } catch (e) {
3646
- return Response.json({ error: e.message }, { status: 500 });
3647
- }
3648
- }
3649
-
3650
- // ============================================================
3651
- // WORKER ENTRY POINT
3652
- // ============================================================
3653
- export default {
3654
- async fetch(request, env) {
3655
- const url = new URL(request.url);
3656
-
3657
- // Route: GET / - Return info about exports
3658
- if (request.method === 'GET' && url.pathname === '/') {
3659
- return Response.json({
3660
- exports: Object.keys(exports),
3661
- rpc: '/rpc',
3662
- execute: '/execute'
3663
- });
3664
- }
3665
-
3666
- // Route: POST /rpc - Simple RPC to module exports
3667
- if (url.pathname === '/rpc' && request.method === 'POST') {
3668
- return handleRpc(request);
3669
- }
3670
-
3671
- // Route: GET /:name - Simple JSON endpoint to access exports
3672
- if (request.method === 'GET' && url.pathname !== '/execute') {
3673
- const name = url.pathname.slice(1);
3674
- const value = exports[name];
3675
-
3676
- // Check if export exists
3677
- if (!(name in exports)) {
3678
- return Response.json({ error: \`Export "\${name}" not found\` }, { status: 404 });
3679
- }
3680
-
3681
- // If it's not a function, just return the value
3682
- if (typeof value !== 'function') {
3683
- return Response.json({ result: value });
3684
- }
3685
-
3686
- // It's a function - parse args and call it
3687
- try {
3688
- const args = [];
3689
- const argsParam = url.searchParams.get('args');
3690
- if (argsParam) {
3691
- try {
3692
- const parsed = JSON.parse(argsParam);
3693
- if (Array.isArray(parsed)) args.push(...parsed);
3694
- else args.push(parsed);
3695
- } catch {
3696
- args.push(argsParam);
3697
- }
3698
- } else {
3699
- const params = Object.fromEntries(url.searchParams.entries());
3700
- if (Object.keys(params).length > 0) {
3701
- for (const [key, val] of Object.entries(params)) {
3702
- const num = Number(val);
3703
- params[key] = !isNaN(num) && val !== '' ? num : val;
3704
- }
3705
- args.push(params);
3706
- }
3707
- }
3708
- const result = await value(...args);
3709
- return Response.json({ result });
3710
- } catch (e) {
3711
- return Response.json({ error: e.message }, { status: 500 });
3712
- }
3713
- }
3714
-
3715
- // Route: /execute - Run tests and scripts
3716
- let scriptResult = undefined;
3717
- let scriptError = null;
3718
-
3719
- // Execute user script
3720
- ${
3721
- script
3722
- ? `
3723
- try {
3724
- scriptResult = await (async () => {
3725
- ${script}
3726
- })();
3727
- } catch (e) {
3728
- console.error('Script error:', e.message);
3729
- scriptError = e.message;
3730
- }
3731
- `
3732
- : '// No script code provided'
3733
- }
3734
-
3735
- // Run all pending tests
3736
- const testStart = Date.now();
3737
- const hasOnly = pendingTests.some(t => t.only);
3738
- const testsToRun = hasOnly ? pendingTests.filter(t => t.only || t.skip) : pendingTests;
3739
-
3740
- for (const { name, fn, hooks, skip } of testsToRun) {
3741
- testResults.total++;
3742
-
3743
- if (skip) {
3744
- testResults.skipped++;
3745
- testResults.tests.push({ name, passed: true, skipped: true, duration: 0 });
3746
- continue;
3747
- }
3748
-
3749
- const start = Date.now();
3750
- try {
3751
- // Run beforeEach hooks
3752
- if (hooks?.before) {
3753
- for (const hook of hooks.before) {
3754
- const hookResult = hook();
3755
- if (hookResult && typeof hookResult.then === 'function') {
3756
- await hookResult;
3757
- }
3758
- }
3759
- }
3760
-
3761
- // Run the test
3762
- const result = fn();
3763
- if (result && typeof result.then === 'function') {
3764
- await result;
3765
- }
3766
-
3767
- // Run afterEach hooks
3768
- if (hooks?.after) {
3769
- for (const hook of hooks.after) {
3770
- const hookResult = hook();
3771
- if (hookResult && typeof hookResult.then === 'function') {
3772
- await hookResult;
3773
- }
3774
- }
3775
- }
3776
-
3777
- testResults.passed++;
3778
- testResults.tests.push({ name, passed: true, duration: Date.now() - start });
3779
- } catch (e) {
3780
- testResults.failed++;
3781
- testResults.tests.push({
3782
- name,
3783
- passed: false,
3784
- error: e.message || String(e),
3785
- duration: Date.now() - start
3786
- });
3787
- }
3788
- }
3789
-
3790
- testResults.duration = Date.now() - testStart;
3791
-
3792
- const hasTests = ${tests ? 'true' : 'false'};
3793
- const success = scriptError === null && (!hasTests || testResults.failed === 0);
3794
-
3795
- return Response.json({
3796
- success,
3797
- value: scriptResult,
3798
- logs,
3799
- testResults: hasTests ? testResults : undefined,
3800
- error: scriptError || undefined,
3801
- duration: 0
3802
- });
3803
- }
3804
- };
3805
- `
3806
- }