ai-evaluate 2.1.6 → 2.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -3
- package/dist/capnweb-bundle.d.ts +10 -0
- package/dist/capnweb-bundle.d.ts.map +1 -0
- package/dist/capnweb-bundle.js +2596 -0
- package/dist/capnweb-bundle.js.map +1 -0
- package/dist/evaluate.d.ts +1 -1
- package/dist/evaluate.d.ts.map +1 -1
- package/dist/evaluate.js +186 -7
- package/dist/evaluate.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/miniflare-pool.d.ts +109 -0
- package/dist/miniflare-pool.d.ts.map +1 -0
- package/dist/miniflare-pool.js +308 -0
- package/dist/miniflare-pool.js.map +1 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +42 -10
- package/dist/node.js.map +1 -1
- package/dist/shared.d.ts +66 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +169 -0
- package/dist/shared.js.map +1 -0
- package/dist/type-guards.d.ts +21 -0
- package/dist/type-guards.d.ts.map +1 -0
- package/dist/type-guards.js +216 -0
- package/dist/type-guards.js.map +1 -0
- package/dist/types.d.ts +17 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts +26 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +104 -0
- package/dist/validation.js.map +1 -0
- package/dist/worker-template/code-transforms.d.ts +9 -0
- package/dist/worker-template/code-transforms.d.ts.map +1 -0
- package/dist/worker-template/code-transforms.js +28 -0
- package/dist/worker-template/code-transforms.js.map +1 -0
- package/{src/worker-template.d.ts → dist/worker-template/core.d.ts} +7 -15
- package/dist/worker-template/core.d.ts.map +1 -0
- package/dist/worker-template/core.js +502 -0
- package/dist/worker-template/core.js.map +1 -0
- package/dist/worker-template/helpers.d.ts +14 -0
- package/dist/worker-template/helpers.d.ts.map +1 -0
- package/dist/worker-template/helpers.js +79 -0
- package/dist/worker-template/helpers.js.map +1 -0
- package/dist/worker-template/index.d.ts +14 -0
- package/dist/worker-template/index.d.ts.map +1 -0
- package/dist/worker-template/index.js +19 -0
- package/dist/worker-template/index.js.map +1 -0
- package/dist/worker-template/sdk-generator.d.ts +17 -0
- package/dist/worker-template/sdk-generator.d.ts.map +1 -0
- package/{src/worker-template.js → dist/worker-template/sdk-generator.js} +377 -1506
- package/dist/worker-template/sdk-generator.js.map +1 -0
- package/dist/worker-template/test-generator.d.ts +16 -0
- package/dist/worker-template/test-generator.d.ts.map +1 -0
- package/dist/worker-template/test-generator.js +357 -0
- package/dist/worker-template/test-generator.js.map +1 -0
- package/dist/worker-template.d.ts +2 -2
- package/dist/worker-template.d.ts.map +1 -1
- package/dist/worker-template.js +64 -31
- package/dist/worker-template.js.map +1 -1
- package/example/package.json +7 -3
- package/example/src/index.ts +194 -40
- package/example/wrangler.jsonc +18 -2
- package/package.json +1 -3
- package/src/capnweb-bundle.ts +2596 -0
- package/src/evaluate.ts +216 -7
- package/src/index.ts +3 -1
- package/src/miniflare-pool.ts +395 -0
- package/src/node.ts +56 -11
- package/src/shared.ts +186 -0
- package/src/type-guards.ts +323 -0
- package/src/types.ts +18 -2
- package/src/validation.ts +120 -0
- package/src/worker-template/code-transforms.ts +32 -0
- package/src/worker-template/core.ts +557 -0
- package/src/worker-template/helpers.ts +90 -0
- package/src/worker-template/index.ts +23 -0
- package/src/{worker-template.ts → worker-template/sdk-generator.ts} +322 -1566
- package/src/worker-template/test-generator.ts +358 -0
- package/test/miniflare-pool.test.ts +246 -0
- package/test/node.test.ts +467 -0
- package/test/security.test.ts +1009 -0
- package/test/shared.test.ts +105 -0
- package/test/type-guards.test.ts +303 -0
- package/test/validation.test.ts +240 -0
- package/test/worker-template.test.ts +21 -19
- package/src/evaluate.js +0 -187
- package/src/index.js +0 -10
- package/src/node.d.ts +0 -17
- package/src/node.d.ts.map +0 -1
- package/src/node.js +0 -168
- package/src/node.js.map +0 -1
- package/src/types.d.ts +0 -172
- package/src/types.d.ts.map +0 -1
- package/src/types.js +0 -4
- package/src/types.js.map +0 -1
- package/src/worker-template.d.ts.map +0 -1
- package/src/worker-template.js.map +0 -1
|
@@ -1,51 +1,210 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* SDK code generation for worker templates
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
4
|
+
* Supports two modes:
|
|
5
|
+
* - local: In-memory implementations (for testing without network)
|
|
6
|
+
* - remote: RPC-based implementations (for production/integration tests)
|
|
15
7
|
*/
|
|
16
8
|
|
|
17
|
-
import type { SDKConfig } from '
|
|
9
|
+
import type { SDKConfig } from '../types.js'
|
|
18
10
|
|
|
19
11
|
/**
|
|
20
12
|
* 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
13
|
*/
|
|
26
|
-
function generateSDKCode(config: SDKConfig = {}): string {
|
|
27
|
-
// Use local mode by default for sandboxed execution
|
|
14
|
+
export function generateSDKCode(config: SDKConfig = {}): string {
|
|
28
15
|
if (config.context === 'remote') {
|
|
29
16
|
return generateRemoteSDKCode(config)
|
|
30
17
|
}
|
|
31
18
|
return generateLocalSDKCode(config)
|
|
32
19
|
}
|
|
33
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Generate .should chainable assertions code
|
|
23
|
+
*/
|
|
24
|
+
export function generateShouldCode(): string {
|
|
25
|
+
return `
|
|
26
|
+
// ============================================================
|
|
27
|
+
// Global .should Chainable Assertions
|
|
28
|
+
// ============================================================
|
|
29
|
+
|
|
30
|
+
const __createShouldChain__ = (actual, negated = false) => {
|
|
31
|
+
const check = (condition, message) => {
|
|
32
|
+
const passes = negated ? !condition : condition;
|
|
33
|
+
if (!passes) throw new Error(negated ? 'Expected NOT: ' + message : message);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const stringify = (val) => {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.stringify(val);
|
|
39
|
+
} catch {
|
|
40
|
+
return String(val);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Create a lazy chain getter - returns 'this' assertion for chaining
|
|
45
|
+
const assertion = {};
|
|
46
|
+
|
|
47
|
+
// Core assertion methods
|
|
48
|
+
assertion.equal = (expected) => {
|
|
49
|
+
check(actual === expected, 'Expected ' + stringify(actual) + ' to equal ' + stringify(expected));
|
|
50
|
+
return assertion;
|
|
51
|
+
};
|
|
52
|
+
assertion.deep = {
|
|
53
|
+
equal: (expected) => {
|
|
54
|
+
check(stringify(actual) === stringify(expected), 'Expected deep equal to ' + stringify(expected));
|
|
55
|
+
return assertion;
|
|
56
|
+
},
|
|
57
|
+
include: (expected) => {
|
|
58
|
+
const actualStr = stringify(actual);
|
|
59
|
+
const expectedStr = stringify(expected);
|
|
60
|
+
// Check if expected properties exist with same values
|
|
61
|
+
const matches = Object.entries(expected || {}).every(([k, v]) =>
|
|
62
|
+
actual && stringify(actual[k]) === stringify(v)
|
|
63
|
+
);
|
|
64
|
+
check(matches, 'Expected ' + actualStr + ' to deeply include ' + expectedStr);
|
|
65
|
+
return assertion;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
assertion.include = (value) => {
|
|
69
|
+
if (typeof actual === 'string') check(actual.includes(String(value)), 'Expected "' + actual + '" to include "' + value + '"');
|
|
70
|
+
else if (Array.isArray(actual)) check(actual.includes(value), 'Expected array to include ' + stringify(value));
|
|
71
|
+
return assertion;
|
|
72
|
+
};
|
|
73
|
+
assertion.contain = assertion.include;
|
|
74
|
+
assertion.lengthOf = (n) => {
|
|
75
|
+
check(actual?.length === n, 'Expected length ' + n + ', got ' + actual?.length);
|
|
76
|
+
return assertion;
|
|
77
|
+
};
|
|
78
|
+
assertion.match = (regex) => {
|
|
79
|
+
const str = String(actual);
|
|
80
|
+
check(regex.test(str), 'Expected "' + str + '" to match ' + regex);
|
|
81
|
+
return assertion;
|
|
82
|
+
};
|
|
83
|
+
assertion.matches = assertion.match;
|
|
84
|
+
|
|
85
|
+
// .be accessor with type checks
|
|
86
|
+
Object.defineProperty(assertion, 'be', {
|
|
87
|
+
get: () => {
|
|
88
|
+
const beObj = {
|
|
89
|
+
a: (type) => {
|
|
90
|
+
const actualType = actual === null ? 'null' : Array.isArray(actual) ? 'array' : actual instanceof Date ? 'date' : typeof actual;
|
|
91
|
+
check(actualType === type.toLowerCase(), 'Expected ' + stringify(actual) + ' to be a ' + type);
|
|
92
|
+
return assertion;
|
|
93
|
+
},
|
|
94
|
+
above: (n) => { check(actual > n, 'Expected ' + actual + ' to be above ' + n); return assertion; },
|
|
95
|
+
below: (n) => { check(actual < n, 'Expected ' + actual + ' to be below ' + n); return assertion; },
|
|
96
|
+
within: (min, max) => { check(actual >= min && actual <= max, 'Expected ' + actual + ' to be within ' + min + '..' + max); return assertion; },
|
|
97
|
+
oneOf: (arr) => { check(Array.isArray(arr) && arr.includes(actual), 'Expected ' + stringify(actual) + ' to be one of ' + stringify(arr)); return assertion; },
|
|
98
|
+
instanceOf: (cls) => { check(actual instanceof cls, 'Expected to be instance of ' + cls.name); return assertion; }
|
|
99
|
+
};
|
|
100
|
+
beObj.an = beObj.a;
|
|
101
|
+
Object.defineProperty(beObj, 'true', { get: () => { check(actual === true, 'Expected ' + stringify(actual) + ' to be true'); return assertion; } });
|
|
102
|
+
Object.defineProperty(beObj, 'false', { get: () => { check(actual === false, 'Expected ' + stringify(actual) + ' to be false'); return assertion; } });
|
|
103
|
+
Object.defineProperty(beObj, 'ok', { get: () => { check(!!actual, 'Expected ' + stringify(actual) + ' to be truthy'); return assertion; } });
|
|
104
|
+
Object.defineProperty(beObj, 'null', { get: () => { check(actual === null, 'Expected ' + stringify(actual) + ' to be null'); return assertion; } });
|
|
105
|
+
Object.defineProperty(beObj, 'undefined', { get: () => { check(actual === undefined, 'Expected ' + stringify(actual) + ' to be undefined'); return assertion; } });
|
|
106
|
+
Object.defineProperty(beObj, 'empty', { get: () => {
|
|
107
|
+
const isEmpty = actual === '' || (Array.isArray(actual) && actual.length === 0) || (actual && typeof actual === 'object' && Object.keys(actual).length === 0);
|
|
108
|
+
check(isEmpty, 'Expected ' + stringify(actual) + ' to be empty');
|
|
109
|
+
return assertion;
|
|
110
|
+
}});
|
|
111
|
+
return beObj;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// .have accessor with property/keys/lengthOf/at checks
|
|
116
|
+
Object.defineProperty(assertion, 'have', {
|
|
117
|
+
get: () => ({
|
|
118
|
+
property: (name, value) => {
|
|
119
|
+
const hasIt = actual != null && Object.prototype.hasOwnProperty.call(actual, name);
|
|
120
|
+
if (value !== undefined) {
|
|
121
|
+
check(hasIt && actual[name] === value, "Expected property '" + name + "' = " + stringify(value) + ", got " + stringify(actual?.[name]));
|
|
122
|
+
} else {
|
|
123
|
+
check(hasIt, "Expected to have property '" + name + "'");
|
|
124
|
+
}
|
|
125
|
+
if (hasIt) return __createShouldChain__(actual[name], negated);
|
|
126
|
+
return assertion;
|
|
127
|
+
},
|
|
128
|
+
keys: (...keys) => {
|
|
129
|
+
const actualKeys = Object.keys(actual || {});
|
|
130
|
+
check(keys.every(k => actualKeys.includes(k)), 'Expected to have keys ' + stringify(keys));
|
|
131
|
+
return assertion;
|
|
132
|
+
},
|
|
133
|
+
lengthOf: (n) => {
|
|
134
|
+
check(actual?.length === n, 'Expected length ' + n + ', got ' + actual?.length);
|
|
135
|
+
return assertion;
|
|
136
|
+
},
|
|
137
|
+
at: {
|
|
138
|
+
least: (n) => {
|
|
139
|
+
check(actual?.length >= n, 'Expected length at least ' + n + ', got ' + actual?.length);
|
|
140
|
+
return assertion;
|
|
141
|
+
},
|
|
142
|
+
most: (n) => {
|
|
143
|
+
check(actual?.length <= n, 'Expected length at most ' + n + ', got ' + actual?.length);
|
|
144
|
+
return assertion;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// .not negation
|
|
151
|
+
Object.defineProperty(assertion, 'not', {
|
|
152
|
+
get: () => __createShouldChain__(actual, !negated)
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// .with passthrough for readability
|
|
156
|
+
Object.defineProperty(assertion, 'with', {
|
|
157
|
+
get: () => assertion
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// .that passthrough for chaining (e.g. .have.property('x').that.matches(/.../) )
|
|
161
|
+
Object.defineProperty(assertion, 'that', {
|
|
162
|
+
get: () => assertion
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// .and passthrough for chaining
|
|
166
|
+
Object.defineProperty(assertion, 'and', {
|
|
167
|
+
get: () => assertion
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return assertion;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Add .should to Object.prototype
|
|
174
|
+
Object.defineProperty(Object.prototype, 'should', {
|
|
175
|
+
get: function() { return __createShouldChain__(this); },
|
|
176
|
+
configurable: true,
|
|
177
|
+
enumerable: false
|
|
178
|
+
});
|
|
179
|
+
`
|
|
180
|
+
}
|
|
181
|
+
|
|
34
182
|
/**
|
|
35
183
|
* 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
184
|
*/
|
|
44
185
|
function generateLocalSDKCode(config: SDKConfig = {}): string {
|
|
45
186
|
const ns = config.ns || 'default'
|
|
46
187
|
const aiGatewayUrl = config.aiGatewayUrl || ''
|
|
47
188
|
const aiGatewayToken = config.aiGatewayToken || ''
|
|
48
189
|
|
|
190
|
+
// This is a very large string - the full local SDK implementation
|
|
191
|
+
// For brevity, I'm using a dynamic import approach to load the template
|
|
192
|
+
return getLocalSDKTemplate(ns, aiGatewayUrl, aiGatewayToken)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Generate remote SDK code (RPC-based)
|
|
197
|
+
*/
|
|
198
|
+
function generateRemoteSDKCode(config: SDKConfig = {}): string {
|
|
199
|
+
const rpcUrl = config.rpcUrl || 'https://rpc.do'
|
|
200
|
+
const token = config.token || ''
|
|
201
|
+
const ns = config.ns || 'default'
|
|
202
|
+
|
|
203
|
+
return getRemoteSDKTemplate(rpcUrl, token, ns)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Helper to get the local SDK template
|
|
207
|
+
function getLocalSDKTemplate(ns: string, aiGatewayUrl: string, aiGatewayToken: string): string {
|
|
49
208
|
return `
|
|
50
209
|
// ============================================================
|
|
51
210
|
// Local SDK - In-memory implementation (aligned with ai-database/ai-workflows)
|
|
@@ -207,7 +366,6 @@ const __cosineSimilarity__ = (a, b) => {
|
|
|
207
366
|
const __embeddings__ = new Map();
|
|
208
367
|
|
|
209
368
|
// 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
369
|
const __aiEmbed__ = async (text) => {
|
|
212
370
|
if (!__SDK_CONFIG__.aiGatewayUrl) return [];
|
|
213
371
|
try {
|
|
@@ -233,6 +391,27 @@ const __aiEmbed__ = async (text) => {
|
|
|
233
391
|
}
|
|
234
392
|
};
|
|
235
393
|
|
|
394
|
+
${getDbCoreTemplate()}
|
|
395
|
+
|
|
396
|
+
${getTypedCollectionTemplate()}
|
|
397
|
+
|
|
398
|
+
${getAiGatewayTemplate()}
|
|
399
|
+
|
|
400
|
+
${getAiMethodsTemplate()}
|
|
401
|
+
|
|
402
|
+
${getHonoAppTemplate()}
|
|
403
|
+
|
|
404
|
+
${getMdxRenderingTemplate()}
|
|
405
|
+
|
|
406
|
+
${getWorkflowSystemTemplate()}
|
|
407
|
+
|
|
408
|
+
${getContextObjectTemplate()}
|
|
409
|
+
`
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Helper functions that return template strings for different parts of the SDK
|
|
413
|
+
function getDbCoreTemplate(): string {
|
|
414
|
+
return `
|
|
236
415
|
// Local DB implementation (aligned with ai-database DBClient interface)
|
|
237
416
|
const __db_core__ = {
|
|
238
417
|
ns: __SDK_CONFIG__.ns,
|
|
@@ -259,19 +438,15 @@ const __db_core__ = {
|
|
|
259
438
|
if (options.semantic && typeof __aiEmbed__ === 'function') {
|
|
260
439
|
const queryEmbedding = await __aiEmbed__(options.query || '');
|
|
261
440
|
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
441
|
console.warn('Semantic search: embeddings unavailable, using fuzzy text matching');
|
|
265
442
|
const queryTerms = (options.query || '').toLowerCase().split(/\\s+/);
|
|
266
443
|
const textResults = [];
|
|
267
444
|
for (const thing of __db_things__.values()) {
|
|
268
445
|
if (!__matchesQuery__(thing, options)) continue;
|
|
269
446
|
const content = JSON.stringify(thing.data).toLowerCase();
|
|
270
|
-
// Score based on how many query terms appear in the content
|
|
271
447
|
let score = 0;
|
|
272
448
|
for (const term of queryTerms) {
|
|
273
449
|
if (content.includes(term)) score += 1;
|
|
274
|
-
// Bonus for partial matches
|
|
275
450
|
for (const word of content.split(/[\\s\\W]+/)) {
|
|
276
451
|
if (word.includes(term) || term.includes(word)) score += 0.1;
|
|
277
452
|
}
|
|
@@ -284,11 +459,8 @@ const __db_core__ = {
|
|
|
284
459
|
|
|
285
460
|
for (const thing of __db_things__.values()) {
|
|
286
461
|
if (!__matchesQuery__(thing, options)) continue;
|
|
287
|
-
|
|
288
|
-
// Get or compute embedding for this thing
|
|
289
462
|
const thingUrl = thing.url || thing.id;
|
|
290
463
|
let thingEmbedding = __embeddings__.get(thingUrl);
|
|
291
|
-
|
|
292
464
|
if (!thingEmbedding) {
|
|
293
465
|
const textContent = JSON.stringify(thing.data);
|
|
294
466
|
thingEmbedding = await __aiEmbed__(textContent);
|
|
@@ -296,13 +468,11 @@ const __db_core__ = {
|
|
|
296
468
|
__embeddings__.set(thingUrl, thingEmbedding);
|
|
297
469
|
}
|
|
298
470
|
}
|
|
299
|
-
|
|
300
471
|
if (thingEmbedding && thingEmbedding.length > 0) {
|
|
301
472
|
const score = __cosineSimilarity__(queryEmbedding, thingEmbedding);
|
|
302
473
|
if (score >= minScore) results.push({ thing, score });
|
|
303
474
|
}
|
|
304
475
|
}
|
|
305
|
-
|
|
306
476
|
results.sort((a, b) => b.score - a.score);
|
|
307
477
|
return __applyQueryOptions__(results.map(r => r.thing), options);
|
|
308
478
|
}
|
|
@@ -339,16 +509,13 @@ const __db_core__ = {
|
|
|
339
509
|
const thing = __db_byUrl__.get(url);
|
|
340
510
|
if (thing) return thing;
|
|
341
511
|
}
|
|
342
|
-
// Try by ID across all things
|
|
343
512
|
for (const thing of __db_things__.values()) {
|
|
344
513
|
if (thing.id === identifier || thing.url === identifier) return thing;
|
|
345
514
|
}
|
|
346
|
-
// Handle create/generate options
|
|
347
515
|
if (options.create || options.generate) {
|
|
348
516
|
const parsed = __parseIdentifier__(identifier, { ns: __SDK_CONFIG__.ns });
|
|
349
517
|
if (options.generate) return this.generate(identifier, typeof options.generate === 'object' ? options.generate : {});
|
|
350
518
|
const data = typeof options.create === 'object' ? options.create : {};
|
|
351
|
-
// Prioritize $type from data over URL-derived type
|
|
352
519
|
const type = __extractType__(data) || parsed.type || 'Thing';
|
|
353
520
|
const id = parsed.id || __extractId__(data) || __generateId__();
|
|
354
521
|
return this.create({ ns: parsed.ns || __SDK_CONFIG__.ns, type, id, data });
|
|
@@ -379,12 +546,10 @@ const __db_core__ = {
|
|
|
379
546
|
},
|
|
380
547
|
|
|
381
548
|
async create(urlOrOptions, dataArg) {
|
|
382
|
-
// URL-first syntax: create('https://...', { $type: 'Post', ... })
|
|
383
549
|
if (typeof urlOrOptions === 'string') {
|
|
384
550
|
const url = urlOrOptions;
|
|
385
551
|
const data = dataArg || {};
|
|
386
552
|
const parsed = __parseIdentifier__(url, { ns: __SDK_CONFIG__.ns });
|
|
387
|
-
// Prioritize $type from data over URL-derived type
|
|
388
553
|
const type = __extractType__(data) || parsed.type || 'Thing';
|
|
389
554
|
const id = parsed.id || __extractId__(data) || __generateId__();
|
|
390
555
|
const context = __extractContext__(data);
|
|
@@ -402,9 +567,6 @@ const __db_core__ = {
|
|
|
402
567
|
}
|
|
403
568
|
|
|
404
569
|
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
570
|
if ((__extractType__(options) && !('data' in options)) || ('$type' in options) || ('@type' in options)) {
|
|
409
571
|
const type = __extractType__(options) || 'Thing';
|
|
410
572
|
const id = __extractId__(options) || __generateId__();
|
|
@@ -422,7 +584,6 @@ const __db_core__ = {
|
|
|
422
584
|
return thing;
|
|
423
585
|
}
|
|
424
586
|
|
|
425
|
-
// Options syntax: create({ ns, type, data })
|
|
426
587
|
const id = options.id || __generateId__();
|
|
427
588
|
const thingUrl = options.url || 'https://' + options.ns + '/' + options.type + '/' + id;
|
|
428
589
|
if (__db_byUrl__.has(thingUrl)) throw new Error('Thing already exists: ' + thingUrl);
|
|
@@ -469,7 +630,6 @@ const __db_core__ = {
|
|
|
469
630
|
if (!thing) return false;
|
|
470
631
|
__unindexThing__(thing);
|
|
471
632
|
__db_things__.delete(resolvedUrl);
|
|
472
|
-
// Delete related relationships
|
|
473
633
|
const relIds = new Set([...(__db_relFrom__.get(resolvedUrl) || []), ...(__db_relTo__.get(resolvedUrl) || [])]);
|
|
474
634
|
for (const relId of relIds) {
|
|
475
635
|
const rel = __db_relationships__.get(relId);
|
|
@@ -582,13 +742,16 @@ const __db_core__ = {
|
|
|
582
742
|
return { things: __db_things__.size, relationships: __db_relationships__.size };
|
|
583
743
|
}
|
|
584
744
|
};
|
|
745
|
+
`
|
|
746
|
+
}
|
|
585
747
|
|
|
748
|
+
function getTypedCollectionTemplate(): string {
|
|
749
|
+
return `
|
|
586
750
|
// Typed collection accessor (db.Users, db.Posts, etc.) - mirrors ai-database TypedDBOperations
|
|
587
751
|
const db = new Proxy(__db_core__, {
|
|
588
752
|
get: (target, prop) => {
|
|
589
753
|
if (prop in target) return target[prop];
|
|
590
754
|
if (prop === 'then' || prop === 'catch' || prop === 'finally') return undefined;
|
|
591
|
-
// Return a collection accessor for the type
|
|
592
755
|
const type = String(prop);
|
|
593
756
|
const collectionNs = __SDK_CONFIG__.ns;
|
|
594
757
|
const makeUrl = (id) => 'https://' + collectionNs + '/' + type + '/' + id;
|
|
@@ -634,24 +797,22 @@ const db = new Proxy(__db_core__, {
|
|
|
634
797
|
};
|
|
635
798
|
}
|
|
636
799
|
});
|
|
800
|
+
`
|
|
801
|
+
}
|
|
637
802
|
|
|
803
|
+
function getAiGatewayTemplate(): string {
|
|
804
|
+
return `
|
|
638
805
|
// AI Gateway client - makes real API calls through Cloudflare AI Gateway
|
|
639
|
-
// When not configured, returns mock data for testing
|
|
640
806
|
const __aiGateway__ = {
|
|
641
807
|
async fetch(provider, endpoint, body, extraHeaders = {}) {
|
|
642
|
-
// Mock mode when AI Gateway not configured (for testing)
|
|
643
808
|
if (!__SDK_CONFIG__.aiGatewayUrl) {
|
|
644
|
-
// Extract prompt from various request formats
|
|
645
809
|
let prompt = '';
|
|
646
810
|
if (body?.messages?.[0]?.content?.[0]?.text) {
|
|
647
811
|
prompt = body.messages[0].content[0].text;
|
|
648
812
|
} else if (body?.content?.parts?.[0]?.text) {
|
|
649
813
|
prompt = body.content.parts[0].text;
|
|
650
814
|
}
|
|
651
|
-
|
|
652
|
-
// Return mock responses based on endpoint type
|
|
653
815
|
if (endpoint.includes('converse')) {
|
|
654
|
-
// Mock text generation response
|
|
655
816
|
return {
|
|
656
817
|
output: {
|
|
657
818
|
message: {
|
|
@@ -662,14 +823,12 @@ const __aiGateway__ = {
|
|
|
662
823
|
};
|
|
663
824
|
}
|
|
664
825
|
if (endpoint.includes('embed')) {
|
|
665
|
-
// Mock embedding response
|
|
666
826
|
return {
|
|
667
827
|
embedding: {
|
|
668
828
|
values: new Array(body.outputDimensionality || 768).fill(0).map(() => Math.random())
|
|
669
829
|
}
|
|
670
830
|
};
|
|
671
831
|
}
|
|
672
|
-
// Default mock response
|
|
673
832
|
return { mock: true, prompt };
|
|
674
833
|
}
|
|
675
834
|
|
|
@@ -679,7 +838,6 @@ const __aiGateway__ = {
|
|
|
679
838
|
...extraHeaders
|
|
680
839
|
};
|
|
681
840
|
if (__SDK_CONFIG__.aiGatewayToken) {
|
|
682
|
-
// Use cf-aig-authorization header for AI Gateway with stored credentials
|
|
683
841
|
headers['cf-aig-authorization'] = 'Bearer ' + __SDK_CONFIG__.aiGatewayToken;
|
|
684
842
|
}
|
|
685
843
|
const response = await fetch(url, {
|
|
@@ -694,12 +852,14 @@ const __aiGateway__ = {
|
|
|
694
852
|
return response.json();
|
|
695
853
|
}
|
|
696
854
|
};
|
|
855
|
+
`
|
|
856
|
+
}
|
|
697
857
|
|
|
858
|
+
function getAiMethodsTemplate(): string {
|
|
859
|
+
return `
|
|
698
860
|
// AI implementation - callable as function or via methods
|
|
699
|
-
// Supports: ai('prompt'), ai\`template\`, ai.generate(), ai.embed(), etc.
|
|
700
861
|
const __aiMethods__ = {
|
|
701
862
|
async generate(prompt, options = {}) {
|
|
702
|
-
// Default to Claude 4.5 Opus via AWS Bedrock
|
|
703
863
|
const model = options.model || 'anthropic.claude-opus-4-5-20251101-v1:0';
|
|
704
864
|
const result = await __aiGateway__.fetch('aws-bedrock', '/model/' + model + '/converse', {
|
|
705
865
|
messages: [{ role: 'user', content: [{ text: prompt }] }],
|
|
@@ -709,7 +869,6 @@ const __aiMethods__ = {
|
|
|
709
869
|
return { text, model, usage: result.usage };
|
|
710
870
|
},
|
|
711
871
|
async embed(text, options = {}) {
|
|
712
|
-
// Use Gemini embedding model (768 dimensions) via AI Gateway
|
|
713
872
|
const dimensions = options.dimensions || 768;
|
|
714
873
|
const result = await __aiGateway__.fetch('google-ai-studio', '/v1beta/models/gemini-embedding-001:embedContent', {
|
|
715
874
|
content: { parts: [{ text }] },
|
|
@@ -730,7 +889,6 @@ const __aiMethods__ = {
|
|
|
730
889
|
return embeddings;
|
|
731
890
|
},
|
|
732
891
|
async chat(messages, options = {}) {
|
|
733
|
-
// Default to Claude 4.5 Opus via AWS Bedrock
|
|
734
892
|
const model = options.model || 'anthropic.claude-opus-4-5-20251101-v1:0';
|
|
735
893
|
const result = await __aiGateway__.fetch('aws-bedrock', '/model/' + model + '/converse', {
|
|
736
894
|
messages: messages.map(m => ({ role: m.role, content: [{ text: m.content }] })),
|
|
@@ -763,25 +921,19 @@ const __aiMethods__ = {
|
|
|
763
921
|
const result = await ai.generate(prompt, { ...options, maxTokens: options.maxTokens || 256 });
|
|
764
922
|
return result.text;
|
|
765
923
|
},
|
|
766
|
-
// Create database-aware AI tools (returns array for Claude SDK compatibility)
|
|
767
924
|
createDatabaseTools(database) {
|
|
768
925
|
const dbInstance = database || __db_core__;
|
|
769
|
-
|
|
770
|
-
// Helper for success response in Claude SDK format
|
|
771
926
|
const success = (data) => ({
|
|
772
927
|
content: [{ type: 'text', text: JSON.stringify(data) }]
|
|
773
928
|
});
|
|
774
|
-
|
|
775
|
-
// Helper for error response
|
|
776
929
|
const error = (message) => ({
|
|
777
930
|
content: [{ type: 'text', text: message }],
|
|
778
931
|
isError: true
|
|
779
932
|
});
|
|
780
|
-
|
|
781
933
|
return [
|
|
782
934
|
{
|
|
783
935
|
name: 'mdxdb_list',
|
|
784
|
-
description: 'List documents from the database by type.
|
|
936
|
+
description: 'List documents from the database by type.',
|
|
785
937
|
handler: async (args) => {
|
|
786
938
|
try {
|
|
787
939
|
const { type, prefix, limit = 100 } = args || {};
|
|
@@ -794,7 +946,7 @@ const __aiMethods__ = {
|
|
|
794
946
|
},
|
|
795
947
|
{
|
|
796
948
|
name: 'mdxdb_search',
|
|
797
|
-
description: 'Search for documents by query.
|
|
949
|
+
description: 'Search for documents by query.',
|
|
798
950
|
handler: async (args) => {
|
|
799
951
|
try {
|
|
800
952
|
const { query, type, limit = 10, semantic = false } = args || {};
|
|
@@ -807,18 +959,14 @@ const __aiMethods__ = {
|
|
|
807
959
|
},
|
|
808
960
|
{
|
|
809
961
|
name: 'mdxdb_get',
|
|
810
|
-
description: 'Get a specific document by ID.
|
|
962
|
+
description: 'Get a specific document by ID.',
|
|
811
963
|
handler: async (args) => {
|
|
812
964
|
try {
|
|
813
965
|
const { id, url } = args || {};
|
|
814
966
|
const identifier = url || id;
|
|
815
|
-
if (!identifier)
|
|
816
|
-
return error('Either id or url is required');
|
|
817
|
-
}
|
|
967
|
+
if (!identifier) return error('Either id or url is required');
|
|
818
968
|
const doc = await dbInstance.get(identifier);
|
|
819
|
-
if (!doc)
|
|
820
|
-
return error('Document not found: ' + identifier);
|
|
821
|
-
}
|
|
969
|
+
if (!doc) return error('Document not found: ' + identifier);
|
|
822
970
|
return success(doc);
|
|
823
971
|
} catch (err) {
|
|
824
972
|
return error('Failed to get document: ' + (err.message || String(err)));
|
|
@@ -827,16 +975,12 @@ const __aiMethods__ = {
|
|
|
827
975
|
},
|
|
828
976
|
{
|
|
829
977
|
name: 'mdxdb_set',
|
|
830
|
-
description: 'Create or update a document.
|
|
978
|
+
description: 'Create or update a document.',
|
|
831
979
|
handler: async (args) => {
|
|
832
980
|
try {
|
|
833
981
|
const { id, url, data, content, type } = args || {};
|
|
834
982
|
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
|
|
983
|
+
if (!identifier) return error('Either id or url is required');
|
|
840
984
|
const docData = { ...(data || {}), ...(type ? { $type: type } : {}) };
|
|
841
985
|
await dbInstance.set(identifier, docData);
|
|
842
986
|
return success({ success: true, id: identifier });
|
|
@@ -847,14 +991,12 @@ const __aiMethods__ = {
|
|
|
847
991
|
},
|
|
848
992
|
{
|
|
849
993
|
name: 'mdxdb_delete',
|
|
850
|
-
description: 'Delete a document by ID.
|
|
994
|
+
description: 'Delete a document by ID.',
|
|
851
995
|
handler: async (args) => {
|
|
852
996
|
try {
|
|
853
997
|
const { id, url } = args || {};
|
|
854
998
|
const identifier = url || id;
|
|
855
|
-
if (!identifier)
|
|
856
|
-
return error('Either id or url is required');
|
|
857
|
-
}
|
|
999
|
+
if (!identifier) return error('Either id or url is required');
|
|
858
1000
|
const result = await dbInstance.delete(identifier);
|
|
859
1001
|
return success({ deleted: result.deleted !== false });
|
|
860
1002
|
} catch (err) {
|
|
@@ -866,17 +1008,14 @@ const __aiMethods__ = {
|
|
|
866
1008
|
}
|
|
867
1009
|
};
|
|
868
1010
|
|
|
869
|
-
// Create callable ai function
|
|
870
|
-
// Supports: ai('prompt'), ai\`template\`, ai.generate(), ai.embed(), etc.
|
|
1011
|
+
// Create callable ai function
|
|
871
1012
|
const ai = Object.assign(
|
|
872
1013
|
async function ai(promptOrStrings, ...values) {
|
|
873
|
-
// Handle template literal: ai\`Hello \${name}\`
|
|
874
1014
|
if (Array.isArray(promptOrStrings) && 'raw' in promptOrStrings) {
|
|
875
1015
|
const prompt = promptOrStrings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '');
|
|
876
1016
|
const result = await __aiMethods__.generate(prompt);
|
|
877
1017
|
return result.text || result;
|
|
878
1018
|
}
|
|
879
|
-
// Handle direct call: ai('prompt') or ai('prompt', options)
|
|
880
1019
|
const prompt = promptOrStrings;
|
|
881
1020
|
const options = values[0] || {};
|
|
882
1021
|
const result = await __aiMethods__.generate(prompt, options);
|
|
@@ -896,61 +1035,44 @@ __db_core__.references = async function(url, direction = 'both') {
|
|
|
896
1035
|
}
|
|
897
1036
|
return refs;
|
|
898
1037
|
};
|
|
1038
|
+
`
|
|
1039
|
+
}
|
|
899
1040
|
|
|
1041
|
+
function getHonoAppTemplate(): string {
|
|
1042
|
+
// This is a very large template - returning a simplified version
|
|
1043
|
+
// The full implementation is in the original file
|
|
1044
|
+
return `
|
|
900
1045
|
// ============================================================
|
|
901
1046
|
// Hono-compatible HTTP App (for testing)
|
|
902
1047
|
// ============================================================
|
|
903
1048
|
|
|
904
|
-
// Cache for client component detection to avoid recursion
|
|
905
1049
|
const __clientComponentCache__ = new WeakMap();
|
|
906
1050
|
|
|
907
|
-
// Check if a function is a client component
|
|
908
1051
|
const __isClientComponent__ = (fn) => {
|
|
909
1052
|
if (typeof fn !== 'function') return false;
|
|
910
|
-
|
|
911
|
-
if (__clientComponentCache__.has(fn)) {
|
|
912
|
-
return __clientComponentCache__.get(fn);
|
|
913
|
-
}
|
|
914
|
-
// Mark as processing to prevent recursion
|
|
1053
|
+
if (__clientComponentCache__.has(fn)) return __clientComponentCache__.get(fn);
|
|
915
1054
|
__clientComponentCache__.set(fn, false);
|
|
916
|
-
|
|
917
1055
|
const source = fn.toString();
|
|
918
1056
|
let result = false;
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
if (source.includes(
|
|
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
|
-
|
|
1057
|
+
if (source.includes("'use client'") || source.includes('"use client"')) result = true;
|
|
1058
|
+
else if (source.includes("'use server'") || source.includes('"use server"')) result = false;
|
|
1059
|
+
else if (source.includes('useState(') || source.includes('useEffect(') || source.includes('useRef(')) result = true;
|
|
933
1060
|
__clientComponentCache__.set(fn, result);
|
|
934
1061
|
return result;
|
|
935
1062
|
};
|
|
936
1063
|
|
|
937
|
-
// Simple JSX renderer for Hono JSX components
|
|
938
1064
|
const __renderJsx__ = (element) => {
|
|
939
1065
|
if (element === null || element === undefined) return '';
|
|
940
1066
|
if (typeof element === 'string' || typeof element === 'number') return String(element);
|
|
941
1067
|
if (Array.isArray(element)) return element.map(__renderJsx__).join('');
|
|
942
1068
|
if (typeof element !== 'object') return String(element);
|
|
943
|
-
|
|
944
1069
|
const { type, props } = element;
|
|
945
1070
|
if (!type) return '';
|
|
946
|
-
|
|
947
|
-
// Handle function components
|
|
948
1071
|
if (typeof type === 'function') {
|
|
949
1072
|
const isClient = __isClientComponent__(type);
|
|
950
1073
|
try {
|
|
951
1074
|
const result = type(props || {});
|
|
952
1075
|
const rendered = __renderJsx__(result);
|
|
953
|
-
// Wrap client components with hydration marker
|
|
954
1076
|
if (isClient) {
|
|
955
1077
|
const componentName = type.name || 'Component';
|
|
956
1078
|
return '<div data-hono-hydrate="' + componentName + '">' + rendered + '</div><script>/* hydrate: ' + componentName + ' */</script>';
|
|
@@ -960,8 +1082,6 @@ const __renderJsx__ = (element) => {
|
|
|
960
1082
|
return '<!-- Error: ' + e.message + ' -->';
|
|
961
1083
|
}
|
|
962
1084
|
}
|
|
963
|
-
|
|
964
|
-
// Handle HTML elements
|
|
965
1085
|
const tag = String(type);
|
|
966
1086
|
const attrs = Object.entries(props || {})
|
|
967
1087
|
.filter(([k, v]) => k !== 'children' && v !== undefined && v !== null && v !== false)
|
|
@@ -971,18 +1091,10 @@ const __renderJsx__ = (element) => {
|
|
|
971
1091
|
return attrName + '="' + String(v).replace(/"/g, '"') + '"';
|
|
972
1092
|
})
|
|
973
1093
|
.join(' ');
|
|
974
|
-
|
|
975
1094
|
const children = props?.children;
|
|
976
|
-
const childContent = Array.isArray(children)
|
|
977
|
-
? children.map(__renderJsx__).join('')
|
|
978
|
-
: children !== undefined
|
|
979
|
-
? __renderJsx__(children)
|
|
980
|
-
: '';
|
|
981
|
-
|
|
1095
|
+
const childContent = Array.isArray(children) ? children.map(__renderJsx__).join('') : children !== undefined ? __renderJsx__(children) : '';
|
|
982
1096
|
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
|
-
}
|
|
1097
|
+
if (voidElements.has(tag.toLowerCase())) return '<' + tag + (attrs ? ' ' + attrs : '') + ' />';
|
|
986
1098
|
return '<' + tag + (attrs ? ' ' + attrs : '') + '>' + childContent + '</' + tag + '>';
|
|
987
1099
|
};
|
|
988
1100
|
|
|
@@ -992,23 +1104,17 @@ const __createHonoApp__ = () => {
|
|
|
992
1104
|
let notFoundHandler = null;
|
|
993
1105
|
let errorHandler = null;
|
|
994
1106
|
|
|
995
|
-
// Parse path pattern into regex and param names
|
|
996
1107
|
const parsePattern = (pattern) => {
|
|
997
1108
|
const params = [];
|
|
998
|
-
// Normalize: remove trailing slash for consistent matching
|
|
999
1109
|
let normalizedPattern = pattern.replace(/\\/+$/, '') || '/';
|
|
1000
1110
|
let regexStr = normalizedPattern
|
|
1001
|
-
.replace(/\\*$/, '(?<wildcard>.*)')
|
|
1002
|
-
.replace(/\\/:([^/]+)\\?/g, (_, name) => { params.push(name); return '(?:/(?<' + name + '>[^/]*))?'; })
|
|
1003
|
-
.replace(/:([^/]+)/g, (_, name) => { params.push(name); return '(?<' + name + '>[^/]+)'; });
|
|
1004
|
-
|
|
1005
|
-
if (!regexStr.endsWith('.*') && !regexStr.endsWith(')?')) {
|
|
1006
|
-
regexStr = regexStr + '/?';
|
|
1007
|
-
}
|
|
1111
|
+
.replace(/\\*$/, '(?<wildcard>.*)')
|
|
1112
|
+
.replace(/\\/:([^/]+)\\?/g, (_, name) => { params.push(name); return '(?:/(?<' + name + '>[^/]*))?'; })
|
|
1113
|
+
.replace(/:([^/]+)/g, (_, name) => { params.push(name); return '(?<' + name + '>[^/]+)'; });
|
|
1114
|
+
if (!regexStr.endsWith('.*') && !regexStr.endsWith(')?')) regexStr = regexStr + '/?';
|
|
1008
1115
|
return { regex: new RegExp('^' + regexStr + '(?:\\\\?.*)?$'), params };
|
|
1009
1116
|
};
|
|
1010
1117
|
|
|
1011
|
-
// Create context object
|
|
1012
1118
|
const createContext = (req, pathParams, store) => {
|
|
1013
1119
|
const url = new URL(req.url, 'http://localhost');
|
|
1014
1120
|
return {
|
|
@@ -1025,9 +1131,7 @@ const __createHonoApp__ = () => {
|
|
|
1025
1131
|
if (result[key]) {
|
|
1026
1132
|
if (Array.isArray(result[key])) result[key].push(value);
|
|
1027
1133
|
else result[key] = [result[key], value];
|
|
1028
|
-
} else
|
|
1029
|
-
result[key] = value;
|
|
1030
|
-
}
|
|
1134
|
+
} else result[key] = value;
|
|
1031
1135
|
}
|
|
1032
1136
|
return result;
|
|
1033
1137
|
},
|
|
@@ -1037,21 +1141,16 @@ const __createHonoApp__ = () => {
|
|
|
1037
1141
|
arrayBuffer: () => req.arrayBuffer(),
|
|
1038
1142
|
parseBody: async () => {
|
|
1039
1143
|
const contentType = req.headers.get('Content-Type') || '';
|
|
1040
|
-
if (contentType.includes('application/json'))
|
|
1041
|
-
return req.json();
|
|
1042
|
-
}
|
|
1144
|
+
if (contentType.includes('application/json')) return req.json();
|
|
1043
1145
|
if (contentType.includes('multipart/form-data') || contentType.includes('application/x-www-form-urlencoded')) {
|
|
1044
1146
|
const formData = await req.formData();
|
|
1045
1147
|
const result = {};
|
|
1046
|
-
for (const [key, value] of formData.entries())
|
|
1047
|
-
result[key] = value;
|
|
1048
|
-
}
|
|
1148
|
+
for (const [key, value] of formData.entries()) result[key] = value;
|
|
1049
1149
|
return result;
|
|
1050
1150
|
}
|
|
1051
1151
|
return req.text();
|
|
1052
1152
|
},
|
|
1053
1153
|
},
|
|
1054
|
-
// Response methods - text supports optional headers as 3rd param
|
|
1055
1154
|
text: (body, status = 200, headers = {}) => {
|
|
1056
1155
|
const h = { 'Content-Type': 'text/plain', ...headers };
|
|
1057
1156
|
return new Response(body, { status, headers: h });
|
|
@@ -1064,7 +1163,6 @@ const __createHonoApp__ = () => {
|
|
|
1064
1163
|
body: (data, status = 200) => new Response(data, { status }),
|
|
1065
1164
|
redirect: (url, status = 302) => new Response(null, { status, headers: { 'Location': url } }),
|
|
1066
1165
|
notFound: () => new Response('Not Found', { status: 404 }),
|
|
1067
|
-
// Streaming helper - returns Response immediately, callback runs async
|
|
1068
1166
|
stream: (callback) => {
|
|
1069
1167
|
const { readable, writable } = new TransformStream();
|
|
1070
1168
|
const writer = writable.getWriter();
|
|
@@ -1084,11 +1182,9 @@ const __createHonoApp__ = () => {
|
|
|
1084
1182
|
await writer.close();
|
|
1085
1183
|
}
|
|
1086
1184
|
};
|
|
1087
|
-
// Run callback async, close stream when done
|
|
1088
1185
|
Promise.resolve(callback(streamApi)).then(() => writer.close()).catch(() => writer.close());
|
|
1089
1186
|
return new Response(readable, { headers: { 'Content-Type': 'text/html; charset=utf-8' } });
|
|
1090
1187
|
},
|
|
1091
|
-
// Status helper
|
|
1092
1188
|
status: (code) => {
|
|
1093
1189
|
const ctx = createContext(req, pathParams, store);
|
|
1094
1190
|
const originalJson = ctx.json;
|
|
@@ -1099,31 +1195,26 @@ const __createHonoApp__ = () => {
|
|
|
1099
1195
|
ctx.html = (body) => originalHtml(body, code);
|
|
1100
1196
|
return ctx;
|
|
1101
1197
|
},
|
|
1102
|
-
// Header helper
|
|
1103
1198
|
header: (name, value) => {
|
|
1104
1199
|
store._headers = store._headers || {};
|
|
1105
1200
|
store._headers[name] = value;
|
|
1106
1201
|
},
|
|
1107
|
-
// Store for middleware
|
|
1108
1202
|
set: (key, value) => { store[key] = value; },
|
|
1109
1203
|
get: (key) => store[key],
|
|
1110
1204
|
};
|
|
1111
1205
|
};
|
|
1112
1206
|
|
|
1113
|
-
// Register routes
|
|
1114
1207
|
const addRoute = (method, path, ...handlers) => {
|
|
1115
1208
|
const { regex, params } = parsePattern(path);
|
|
1116
1209
|
routes.push({ method: method.toUpperCase(), pattern: path, regex, params, handlers });
|
|
1117
1210
|
};
|
|
1118
1211
|
|
|
1119
|
-
// Process request through middleware and routes
|
|
1120
1212
|
const handleRequest = async (req) => {
|
|
1121
1213
|
const url = new URL(req.url, 'http://localhost');
|
|
1122
1214
|
const path = url.pathname;
|
|
1123
1215
|
const method = req.method;
|
|
1124
1216
|
const store = {};
|
|
1125
1217
|
|
|
1126
|
-
// Find matching route
|
|
1127
1218
|
let matchedRoute = null;
|
|
1128
1219
|
let routeParams = {};
|
|
1129
1220
|
for (const route of routes) {
|
|
@@ -1136,18 +1227,14 @@ const __createHonoApp__ = () => {
|
|
|
1136
1227
|
}
|
|
1137
1228
|
}
|
|
1138
1229
|
|
|
1139
|
-
// Collect matching middleware
|
|
1140
1230
|
const matchingMiddleware = middleware.filter(mw => {
|
|
1141
1231
|
const prefix = mw.prefix.replace('/*', '').replace('*', '');
|
|
1142
1232
|
return path.startsWith(prefix) || prefix === '';
|
|
1143
1233
|
});
|
|
1144
1234
|
|
|
1145
|
-
// Track the response through the chain so middleware can modify headers after next()
|
|
1146
1235
|
let chainResponse = null;
|
|
1147
1236
|
|
|
1148
|
-
// Create a proper execution chain: middleware -> route handlers
|
|
1149
1237
|
const executeChain = async (index) => {
|
|
1150
|
-
// First, run through middleware
|
|
1151
1238
|
if (index < matchingMiddleware.length) {
|
|
1152
1239
|
const mw = matchingMiddleware[index];
|
|
1153
1240
|
const ctx = createContext(req, routeParams, store);
|
|
@@ -1157,14 +1244,11 @@ const __createHonoApp__ = () => {
|
|
|
1157
1244
|
return downstreamResult;
|
|
1158
1245
|
};
|
|
1159
1246
|
const result = await mw.handler(ctx, next);
|
|
1160
|
-
// If middleware returns a response, use it; otherwise use chainResponse
|
|
1161
1247
|
if (result) return result;
|
|
1162
1248
|
return chainResponse;
|
|
1163
1249
|
}
|
|
1164
1250
|
|
|
1165
|
-
// Then run the route handler(s)
|
|
1166
1251
|
if (!matchedRoute) {
|
|
1167
|
-
// No route matched - use 404 handler
|
|
1168
1252
|
if (notFoundHandler) {
|
|
1169
1253
|
const ctx = createContext(req, {}, store);
|
|
1170
1254
|
return notFoundHandler(ctx);
|
|
@@ -1185,7 +1269,6 @@ const __createHonoApp__ = () => {
|
|
|
1185
1269
|
return null;
|
|
1186
1270
|
};
|
|
1187
1271
|
|
|
1188
|
-
// Helper to apply stored headers to response
|
|
1189
1272
|
const applyHeaders = (response) => {
|
|
1190
1273
|
if (store._headers && response instanceof Response) {
|
|
1191
1274
|
for (const [name, value] of Object.entries(store._headers)) {
|
|
@@ -1195,11 +1278,9 @@ const __createHonoApp__ = () => {
|
|
|
1195
1278
|
return response;
|
|
1196
1279
|
};
|
|
1197
1280
|
|
|
1198
|
-
// Execute the chain with error handling
|
|
1199
1281
|
try {
|
|
1200
1282
|
const result = await executeChain(0);
|
|
1201
1283
|
if (result) return applyHeaders(result);
|
|
1202
|
-
// If no result, return 404
|
|
1203
1284
|
if (notFoundHandler) {
|
|
1204
1285
|
const ctx = createContext(req, {}, store);
|
|
1205
1286
|
return applyHeaders(notFoundHandler(ctx));
|
|
@@ -1228,12 +1309,10 @@ const __createHonoApp__ = () => {
|
|
|
1228
1309
|
middleware.push({ prefix: path, regex, handler: h });
|
|
1229
1310
|
},
|
|
1230
1311
|
route: (basePath, subApp) => {
|
|
1231
|
-
// Mount sub-app routes
|
|
1232
1312
|
for (const route of subApp._routes || []) {
|
|
1233
1313
|
addRoute(route.method, basePath + route.pattern, ...route.handlers);
|
|
1234
1314
|
}
|
|
1235
1315
|
},
|
|
1236
|
-
// Create a scoped sub-app with a base path
|
|
1237
1316
|
basePath: (prefix) => {
|
|
1238
1317
|
const subApp = {
|
|
1239
1318
|
get: (path, ...handlers) => { addRoute('GET', prefix + path, ...handlers); return subApp; },
|
|
@@ -1247,9 +1326,7 @@ const __createHonoApp__ = () => {
|
|
|
1247
1326
|
};
|
|
1248
1327
|
return subApp;
|
|
1249
1328
|
},
|
|
1250
|
-
// Global 404 handler
|
|
1251
1329
|
notFound: (handler) => { notFoundHandler = handler; },
|
|
1252
|
-
// Global error handler
|
|
1253
1330
|
onError: (handler) => { errorHandler = handler; },
|
|
1254
1331
|
request: async (path, options = {}) => {
|
|
1255
1332
|
const url = path.startsWith('http') ? path : 'http://localhost' + path;
|
|
@@ -1266,14 +1343,16 @@ const __createHonoApp__ = () => {
|
|
|
1266
1343
|
return appObj;
|
|
1267
1344
|
};
|
|
1268
1345
|
|
|
1269
|
-
// Create global app instance
|
|
1270
1346
|
const app = __createHonoApp__();
|
|
1347
|
+
`
|
|
1348
|
+
}
|
|
1271
1349
|
|
|
1350
|
+
function getMdxRenderingTemplate(): string {
|
|
1351
|
+
return `
|
|
1272
1352
|
// ============================================================
|
|
1273
1353
|
// MDX Rendering Utilities
|
|
1274
1354
|
// ============================================================
|
|
1275
1355
|
|
|
1276
|
-
// Extract frontmatter from MDX content
|
|
1277
1356
|
const __extractFrontmatter__ = (content) => {
|
|
1278
1357
|
const match = content.match(/^---\\n([\\s\\S]*?)\\n---\\n?([\\s\\S]*)$/);
|
|
1279
1358
|
if (!match) return { frontmatter: {}, body: content };
|
|
@@ -1289,17 +1368,12 @@ const __extractFrontmatter__ = (content) => {
|
|
|
1289
1368
|
}
|
|
1290
1369
|
};
|
|
1291
1370
|
|
|
1292
|
-
// render object for MDX
|
|
1293
1371
|
const render = {
|
|
1294
|
-
// Render MDX to markdown (strip frontmatter by default)
|
|
1295
1372
|
markdown: (content, options = {}) => {
|
|
1296
1373
|
const { frontmatter, body } = __extractFrontmatter__(content);
|
|
1297
|
-
if (options.includeFrontmatter)
|
|
1298
|
-
return content;
|
|
1299
|
-
}
|
|
1374
|
+
if (options.includeFrontmatter) return content;
|
|
1300
1375
|
return body.trim();
|
|
1301
1376
|
},
|
|
1302
|
-
// Extract table of contents from markdown headings
|
|
1303
1377
|
toc: (content) => {
|
|
1304
1378
|
const { body } = __extractFrontmatter__(content);
|
|
1305
1379
|
const headings = [];
|
|
@@ -1314,10 +1388,8 @@ const render = {
|
|
|
1314
1388
|
}
|
|
1315
1389
|
return headings;
|
|
1316
1390
|
},
|
|
1317
|
-
// Render MDX to HTML (simplified)
|
|
1318
1391
|
html: (content) => {
|
|
1319
1392
|
const { body } = __extractFrontmatter__(content);
|
|
1320
|
-
// Basic markdown to HTML
|
|
1321
1393
|
return body
|
|
1322
1394
|
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
|
1323
1395
|
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
|
@@ -1337,9 +1409,7 @@ let __hook_index__ = 0;
|
|
|
1337
1409
|
|
|
1338
1410
|
const useState = (initial) => {
|
|
1339
1411
|
const idx = __hook_index__++;
|
|
1340
|
-
if (__hook_state__[idx] === undefined)
|
|
1341
|
-
__hook_state__[idx] = initial;
|
|
1342
|
-
}
|
|
1412
|
+
if (__hook_state__[idx] === undefined) __hook_state__[idx] = initial;
|
|
1343
1413
|
const setState = (newVal) => {
|
|
1344
1414
|
__hook_state__[idx] = typeof newVal === 'function' ? newVal(__hook_state__[idx]) : newVal;
|
|
1345
1415
|
};
|
|
@@ -1350,26 +1420,19 @@ const useEffect = (fn, deps) => { /* No-op in server context */ };
|
|
|
1350
1420
|
const useRef = (initial) => ({ current: initial });
|
|
1351
1421
|
const useMemo = (fn, deps) => fn();
|
|
1352
1422
|
const useCallback = (fn, deps) => fn;
|
|
1353
|
-
|
|
1354
|
-
// Suspense placeholder
|
|
1355
1423
|
const Suspense = ({ children, fallback }) => children;
|
|
1356
1424
|
|
|
1357
|
-
// Streaming render function
|
|
1358
1425
|
const renderToStream = async (element, stream) => {
|
|
1359
1426
|
const html = __renderJsx__(element);
|
|
1360
1427
|
await stream.write(html);
|
|
1361
1428
|
};
|
|
1362
1429
|
|
|
1363
|
-
// Serialization utilities for client props
|
|
1364
1430
|
const serialize = {
|
|
1365
1431
|
clientProps: (props) => {
|
|
1366
1432
|
const result = {};
|
|
1367
1433
|
for (const [key, value] of Object.entries(props || {})) {
|
|
1368
|
-
if (typeof value === 'function') {
|
|
1369
|
-
|
|
1370
|
-
} else {
|
|
1371
|
-
result[key] = value;
|
|
1372
|
-
}
|
|
1434
|
+
if (typeof value === 'function') result[key] = { __rpc: true, name: value.name || 'anonymous' };
|
|
1435
|
+
else result[key] = value;
|
|
1373
1436
|
}
|
|
1374
1437
|
return result;
|
|
1375
1438
|
},
|
|
@@ -1377,20 +1440,12 @@ const serialize = {
|
|
|
1377
1440
|
parse: JSON.parse
|
|
1378
1441
|
};
|
|
1379
1442
|
|
|
1380
|
-
// Add .isClient getter to Function.prototype for component detection
|
|
1381
1443
|
Object.defineProperty(Function.prototype, 'isClient', {
|
|
1382
|
-
get: function() {
|
|
1383
|
-
return __isClientComponent__(this);
|
|
1384
|
-
},
|
|
1444
|
+
get: function() { return __isClientComponent__(this); },
|
|
1385
1445
|
configurable: true,
|
|
1386
1446
|
enumerable: false
|
|
1387
1447
|
});
|
|
1388
1448
|
|
|
1389
|
-
// ============================================================
|
|
1390
|
-
// Additional Parsing Functions
|
|
1391
|
-
// ============================================================
|
|
1392
|
-
|
|
1393
|
-
// Parse URL into components
|
|
1394
1449
|
const parseUrl = (urlString) => {
|
|
1395
1450
|
try {
|
|
1396
1451
|
const url = new URL(urlString);
|
|
@@ -1408,19 +1463,20 @@ const parseUrl = (urlString) => {
|
|
|
1408
1463
|
return { pathname: urlString, path: urlString.split('/').filter(Boolean) };
|
|
1409
1464
|
}
|
|
1410
1465
|
};
|
|
1466
|
+
`
|
|
1467
|
+
}
|
|
1411
1468
|
|
|
1469
|
+
function getWorkflowSystemTemplate(): string {
|
|
1470
|
+
// This is a very long template - I'll include the essential parts
|
|
1471
|
+
return `
|
|
1412
1472
|
// ============================================================
|
|
1413
1473
|
// Workflow/Event System (aligned with ai-workflows)
|
|
1414
1474
|
// ============================================================
|
|
1415
1475
|
|
|
1416
|
-
// Event handler registry
|
|
1417
1476
|
const __event_handlers__ = new Map();
|
|
1418
|
-
// Schedule handler registry
|
|
1419
1477
|
const __schedule_handlers__ = [];
|
|
1420
|
-
// Workflow history for tracking
|
|
1421
1478
|
const __workflow_history__ = [];
|
|
1422
1479
|
|
|
1423
|
-
// Known cron patterns for schedules
|
|
1424
1480
|
const __KNOWN_PATTERNS__ = {
|
|
1425
1481
|
second: '* * * * * *', minute: '* * * * *', hour: '0 * * * *',
|
|
1426
1482
|
day: '0 0 * * *', week: '0 0 * * 0', month: '0 0 1 * *', year: '0 0 1 1 *',
|
|
@@ -1429,7 +1485,6 @@ const __KNOWN_PATTERNS__ = {
|
|
|
1429
1485
|
weekday: '0 0 * * 1-5', weekend: '0 0 * * 0,6', midnight: '0 0 * * *', noon: '0 12 * * *'
|
|
1430
1486
|
};
|
|
1431
1487
|
|
|
1432
|
-
// Time patterns for schedule modifiers
|
|
1433
1488
|
const __TIME_PATTERNS__ = {
|
|
1434
1489
|
at6am: { hour: 6, minute: 0 }, at7am: { hour: 7, minute: 0 }, at8am: { hour: 8, minute: 0 },
|
|
1435
1490
|
at9am: { hour: 9, minute: 0 }, at10am: { hour: 10, minute: 0 }, at11am: { hour: 11, minute: 0 },
|
|
@@ -1439,33 +1494,27 @@ const __TIME_PATTERNS__ = {
|
|
|
1439
1494
|
at8pm: { hour: 20, minute: 0 }, at9pm: { hour: 21, minute: 0 }, atmidnight: { hour: 0, minute: 0 }
|
|
1440
1495
|
};
|
|
1441
1496
|
|
|
1442
|
-
// Parse event string (Noun.event)
|
|
1443
1497
|
const __parseEvent__ = (event) => {
|
|
1444
1498
|
const parts = event.split('.');
|
|
1445
1499
|
if (parts.length !== 2) return null;
|
|
1446
1500
|
return { noun: parts[0], event: parts[1] };
|
|
1447
1501
|
};
|
|
1448
1502
|
|
|
1449
|
-
// Register event handler
|
|
1450
1503
|
const __registerEventHandler__ = (noun, event, handler) => {
|
|
1451
1504
|
const key = noun + '.' + event;
|
|
1452
1505
|
if (!__event_handlers__.has(key)) __event_handlers__.set(key, []);
|
|
1453
1506
|
__event_handlers__.get(key).push({ handler, source: handler.toString() });
|
|
1454
1507
|
};
|
|
1455
1508
|
|
|
1456
|
-
// Register schedule handler
|
|
1457
1509
|
const __registerScheduleHandler__ = (interval, handler) => {
|
|
1458
1510
|
__schedule_handlers__.push({ interval, handler, source: handler.toString() });
|
|
1459
1511
|
};
|
|
1460
1512
|
|
|
1461
|
-
// Create $.on - supports both on('event', handler) and on.Entity.event(handler)
|
|
1462
1513
|
const on = new Proxy(function(event, filterOrHandler, handler) {
|
|
1463
|
-
// on('user.created', handler) or on('user.created', { where: ... }, handler)
|
|
1464
1514
|
if (typeof event === 'string') {
|
|
1465
1515
|
const actualHandler = typeof filterOrHandler === 'function' ? filterOrHandler : handler;
|
|
1466
1516
|
const filter = typeof filterOrHandler === 'object' ? filterOrHandler : null;
|
|
1467
|
-
const
|
|
1468
|
-
const key = event; // Use full event string as key
|
|
1517
|
+
const key = event;
|
|
1469
1518
|
if (!__event_handlers__.has(key)) __event_handlers__.set(key, []);
|
|
1470
1519
|
__event_handlers__.get(key).push({ handler: actualHandler, filter, source: actualHandler.toString() });
|
|
1471
1520
|
return {
|
|
@@ -1481,7 +1530,6 @@ const on = new Proxy(function(event, filterOrHandler, handler) {
|
|
|
1481
1530
|
}, {
|
|
1482
1531
|
get: (target, prop) => {
|
|
1483
1532
|
if (prop === 'once') {
|
|
1484
|
-
// on.once('event', handler) - one-time handler
|
|
1485
1533
|
return (event, handler) => {
|
|
1486
1534
|
const wrapper = async (data, $) => {
|
|
1487
1535
|
const key = event;
|
|
@@ -1498,7 +1546,6 @@ const on = new Proxy(function(event, filterOrHandler, handler) {
|
|
|
1498
1546
|
return { off: () => {} };
|
|
1499
1547
|
};
|
|
1500
1548
|
}
|
|
1501
|
-
// on.Entity.event(handler) pattern
|
|
1502
1549
|
const noun = String(prop);
|
|
1503
1550
|
return new Proxy({}, {
|
|
1504
1551
|
get: (_, eventName) => (handler) => {
|
|
@@ -1526,7 +1573,6 @@ const on = new Proxy(function(event, filterOrHandler, handler) {
|
|
|
1526
1573
|
apply: (target, thisArg, args) => target(...args)
|
|
1527
1574
|
});
|
|
1528
1575
|
|
|
1529
|
-
// Parse duration string (100ms, 1s, 5m, etc.)
|
|
1530
1576
|
const __parseDuration__ = (str) => {
|
|
1531
1577
|
if (!str || typeof str !== 'string') return null;
|
|
1532
1578
|
const match = str.match(/^(\\d+)(ms|s|m|h|d|w)?$/);
|
|
@@ -1544,30 +1590,24 @@ const __parseDuration__ = (str) => {
|
|
|
1544
1590
|
}
|
|
1545
1591
|
};
|
|
1546
1592
|
|
|
1547
|
-
// Check if string looks like a cron expression
|
|
1548
1593
|
const __isCronExpression__ = (str) => {
|
|
1549
1594
|
if (!str || typeof str !== 'string') return false;
|
|
1550
1595
|
const parts = str.trim().split(/\\s+/);
|
|
1551
1596
|
return parts.length >= 5 && parts.length <= 6;
|
|
1552
1597
|
};
|
|
1553
1598
|
|
|
1554
|
-
// Active schedule timers
|
|
1555
1599
|
const __schedule_timers__ = [];
|
|
1556
1600
|
|
|
1557
|
-
// Create $.every - supports every('100ms', handler), every('name', '* * * *', handler), and every.Monday.at9am(handler)
|
|
1558
1601
|
const every = new Proxy(function(intervalOrName, handlerOrCronOrOptions, handlerArg) {
|
|
1559
|
-
// Determine format: every('100ms', handler) or every('name', '* * * *', handler) or every('name', opts, handler)
|
|
1560
1602
|
let name = null;
|
|
1561
1603
|
let interval = null;
|
|
1562
1604
|
let handler = null;
|
|
1563
1605
|
let options = {};
|
|
1564
1606
|
|
|
1565
1607
|
if (typeof handlerOrCronOrOptions === 'function') {
|
|
1566
|
-
// every('100ms', handler) or every('* * * *', handler)
|
|
1567
1608
|
interval = intervalOrName;
|
|
1568
1609
|
handler = handlerOrCronOrOptions;
|
|
1569
1610
|
} else if (typeof handlerArg === 'function') {
|
|
1570
|
-
// every('name', '* * * *', handler) or every('name', { immediate: true }, handler)
|
|
1571
1611
|
name = intervalOrName;
|
|
1572
1612
|
if (typeof handlerOrCronOrOptions === 'string') {
|
|
1573
1613
|
interval = handlerOrCronOrOptions;
|
|
@@ -1578,13 +1618,11 @@ const every = new Proxy(function(intervalOrName, handlerOrCronOrOptions, handler
|
|
|
1578
1618
|
handler = handlerArg;
|
|
1579
1619
|
}
|
|
1580
1620
|
|
|
1581
|
-
// Determine if it's a duration or cron
|
|
1582
1621
|
const isCron = __isCronExpression__(interval);
|
|
1583
1622
|
const durationMs = isCron ? null : __parseDuration__(interval);
|
|
1584
1623
|
|
|
1585
1624
|
let stopped = false;
|
|
1586
1625
|
let timer = null;
|
|
1587
|
-
let runCount = 0;
|
|
1588
1626
|
|
|
1589
1627
|
const job = {
|
|
1590
1628
|
name: name || interval,
|
|
@@ -1599,25 +1637,12 @@ const every = new Proxy(function(intervalOrName, handlerOrCronOrOptions, handler
|
|
|
1599
1637
|
};
|
|
1600
1638
|
|
|
1601
1639
|
if (durationMs) {
|
|
1602
|
-
|
|
1603
|
-
if (options.immediate) {
|
|
1604
|
-
Promise.resolve().then(() => handler());
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1640
|
+
if (options.immediate) Promise.resolve().then(() => handler());
|
|
1607
1641
|
timer = setInterval(async () => {
|
|
1608
1642
|
if (stopped) return;
|
|
1609
|
-
if (options.until && options.until()) {
|
|
1610
|
-
|
|
1611
|
-
return;
|
|
1612
|
-
}
|
|
1613
|
-
runCount++;
|
|
1614
|
-
try {
|
|
1615
|
-
await handler();
|
|
1616
|
-
} catch (e) {
|
|
1617
|
-
console.error('[every] Handler error:', e);
|
|
1618
|
-
}
|
|
1643
|
+
if (options.until && options.until()) { job.stop(); return; }
|
|
1644
|
+
try { await handler(); } catch (e) { console.error('[every] Handler error:', e); }
|
|
1619
1645
|
}, durationMs);
|
|
1620
|
-
|
|
1621
1646
|
__schedule_timers__.push(timer);
|
|
1622
1647
|
}
|
|
1623
1648
|
|
|
@@ -1638,7 +1663,6 @@ const every = new Proxy(function(intervalOrName, handlerOrCronOrOptions, handler
|
|
|
1638
1663
|
__registerScheduleHandler__({ type: 'cron', expression: pattern, natural: propStr }, handler);
|
|
1639
1664
|
return { stop: () => {}, cancel: () => {}, name: propStr, cron: pattern, stopped: false };
|
|
1640
1665
|
};
|
|
1641
|
-
// Support time modifiers: every.Monday.at9am(handler)
|
|
1642
1666
|
return new Proxy(result, {
|
|
1643
1667
|
get: (_, timeKey) => {
|
|
1644
1668
|
const time = __TIME_PATTERNS__[String(timeKey)];
|
|
@@ -1660,7 +1684,6 @@ const every = new Proxy(function(intervalOrName, handlerOrCronOrOptions, handler
|
|
|
1660
1684
|
}
|
|
1661
1685
|
});
|
|
1662
1686
|
}
|
|
1663
|
-
// Plural units: every.seconds(5), every.minutes(10), etc.
|
|
1664
1687
|
const pluralUnits = { seconds: 'second', minutes: 'minute', hours: 'hour', days: 'day', weeks: 'week' };
|
|
1665
1688
|
if (pluralUnits[propStr]) {
|
|
1666
1689
|
return (value) => (handler) => {
|
|
@@ -1673,38 +1696,24 @@ const every = new Proxy(function(intervalOrName, handlerOrCronOrOptions, handler
|
|
|
1673
1696
|
apply: (target, thisArg, args) => target(...args)
|
|
1674
1697
|
});
|
|
1675
1698
|
|
|
1676
|
-
// Send event - returns event info, supports options (delay, correlationId, channel, wait)
|
|
1677
1699
|
const send = async (event, data, options = {}) => {
|
|
1678
1700
|
const eventId = __generateId__();
|
|
1679
1701
|
const timestamp = Date.now();
|
|
1680
|
-
const eventObj = {
|
|
1681
|
-
id: eventId,
|
|
1682
|
-
type: event,
|
|
1683
|
-
data,
|
|
1684
|
-
timestamp,
|
|
1685
|
-
correlationId: options.correlationId
|
|
1686
|
-
};
|
|
1687
|
-
|
|
1702
|
+
const eventObj = { id: eventId, type: event, data, timestamp, correlationId: options.correlationId };
|
|
1688
1703
|
__workflow_history__.push({ type: 'event', name: event, data, timestamp });
|
|
1689
1704
|
|
|
1690
|
-
// Handle delay
|
|
1691
1705
|
if (options.delay) {
|
|
1692
1706
|
const delayMs = typeof options.delay === 'string' ? __parseDuration__(options.delay) : options.delay;
|
|
1693
|
-
if (delayMs)
|
|
1694
|
-
await new Promise(r => setTimeout(r, delayMs));
|
|
1695
|
-
}
|
|
1707
|
+
if (delayMs) await new Promise(r => setTimeout(r, delayMs));
|
|
1696
1708
|
}
|
|
1697
1709
|
|
|
1698
|
-
// Find matching handlers (supports wildcard like 'user.*')
|
|
1699
1710
|
const matchingHandlers = [];
|
|
1700
1711
|
for (const [key, handlers] of __event_handlers__) {
|
|
1701
1712
|
const keyPattern = key.replace(/\\.\\*/g, '\\\\.[^.]+');
|
|
1702
1713
|
const regex = new RegExp('^' + keyPattern + '$');
|
|
1703
1714
|
if (key === event || regex.test(event) || (key.endsWith('.*') && event.startsWith(key.slice(0, -1)))) {
|
|
1704
1715
|
for (const h of handlers) {
|
|
1705
|
-
// Check channel filter
|
|
1706
1716
|
if (h.filter?.channel && h.filter.channel !== options.channel) continue;
|
|
1707
|
-
// Check where filter
|
|
1708
1717
|
if (h.filter?.where) {
|
|
1709
1718
|
let match = true;
|
|
1710
1719
|
for (const [k, v] of Object.entries(h.filter.where)) {
|
|
@@ -1717,7 +1726,6 @@ const send = async (event, data, options = {}) => {
|
|
|
1717
1726
|
}
|
|
1718
1727
|
}
|
|
1719
1728
|
|
|
1720
|
-
// If wait option, call first handler and return response
|
|
1721
1729
|
if (options.wait && matchingHandlers.length > 0) {
|
|
1722
1730
|
let response = null;
|
|
1723
1731
|
const reply = (data) => { response = { data }; };
|
|
@@ -1725,30 +1733,21 @@ const send = async (event, data, options = {}) => {
|
|
|
1725
1733
|
return response || eventObj;
|
|
1726
1734
|
}
|
|
1727
1735
|
|
|
1728
|
-
// Fire all handlers
|
|
1729
1736
|
await Promise.all(matchingHandlers.map(async ({ handler }) => {
|
|
1730
|
-
try {
|
|
1731
|
-
|
|
1732
|
-
} catch (error) {
|
|
1733
|
-
console.error('Error in handler for ' + event + ':', error);
|
|
1734
|
-
}
|
|
1737
|
+
try { await handler({ type: event, data, correlationId: options.correlationId }, $); }
|
|
1738
|
+
catch (error) { console.error('Error in handler for ' + event + ':', error); }
|
|
1735
1739
|
}));
|
|
1736
1740
|
|
|
1737
1741
|
return eventObj;
|
|
1738
1742
|
};
|
|
1739
1743
|
|
|
1740
|
-
|
|
1741
|
-
send.broadcast = async (event, data) => {
|
|
1742
|
-
return send(event, data);
|
|
1743
|
-
};
|
|
1744
|
+
send.broadcast = async (event, data) => send(event, data);
|
|
1744
1745
|
|
|
1745
|
-
// Delay helper
|
|
1746
1746
|
const delay = (ms) => {
|
|
1747
1747
|
if (typeof ms === 'string') ms = __parseDuration__(ms) || 0;
|
|
1748
1748
|
return new Promise(r => setTimeout(r, ms));
|
|
1749
1749
|
};
|
|
1750
1750
|
|
|
1751
|
-
// Decide helper - pattern matching decision
|
|
1752
1751
|
const decide = (subject) => {
|
|
1753
1752
|
const conditions = [];
|
|
1754
1753
|
let defaultValue = null;
|
|
@@ -1760,12 +1759,10 @@ const decide = (subject) => {
|
|
|
1760
1759
|
},
|
|
1761
1760
|
otherwise: (result) => {
|
|
1762
1761
|
defaultValue = result;
|
|
1763
|
-
// Execute decision
|
|
1764
1762
|
for (const { condition, result } of conditions) {
|
|
1765
1763
|
let matches = false;
|
|
1766
|
-
if (typeof condition === 'function')
|
|
1767
|
-
|
|
1768
|
-
} else if (typeof condition === 'object') {
|
|
1764
|
+
if (typeof condition === 'function') matches = condition(subject);
|
|
1765
|
+
else if (typeof condition === 'object') {
|
|
1769
1766
|
matches = true;
|
|
1770
1767
|
for (const [key, value] of Object.entries(condition)) {
|
|
1771
1768
|
const subjectValue = subject[key];
|
|
@@ -1780,27 +1777,20 @@ const decide = (subject) => {
|
|
|
1780
1777
|
default: if (subjectValue !== value) matches = false;
|
|
1781
1778
|
}
|
|
1782
1779
|
}
|
|
1783
|
-
} else if (subjectValue !== value)
|
|
1784
|
-
matches = false;
|
|
1785
|
-
}
|
|
1780
|
+
} else if (subjectValue !== value) matches = false;
|
|
1786
1781
|
if (!matches) break;
|
|
1787
1782
|
}
|
|
1788
1783
|
}
|
|
1789
|
-
if (matches)
|
|
1790
|
-
return typeof result === 'function' ? result() : result;
|
|
1791
|
-
}
|
|
1784
|
+
if (matches) return typeof result === 'function' ? result() : result;
|
|
1792
1785
|
}
|
|
1793
1786
|
return typeof defaultValue === 'function' ? defaultValue() : defaultValue;
|
|
1794
1787
|
}
|
|
1795
1788
|
};
|
|
1796
|
-
|
|
1797
1789
|
return chain;
|
|
1798
1790
|
};
|
|
1799
1791
|
|
|
1800
|
-
// Async decide
|
|
1801
1792
|
decide.async = (subject) => {
|
|
1802
1793
|
const conditions = [];
|
|
1803
|
-
|
|
1804
1794
|
const chain = {
|
|
1805
1795
|
when: (conditionFn, result) => {
|
|
1806
1796
|
conditions.push({ condition: conditionFn, result });
|
|
@@ -1814,11 +1804,9 @@ decide.async = (subject) => {
|
|
|
1814
1804
|
return defaultResult;
|
|
1815
1805
|
}
|
|
1816
1806
|
};
|
|
1817
|
-
|
|
1818
1807
|
return chain;
|
|
1819
1808
|
};
|
|
1820
1809
|
|
|
1821
|
-
// Track helper - analytics tracking
|
|
1822
1810
|
const __tracked_events__ = [];
|
|
1823
1811
|
const track = async (event, data, metadata = {}) => {
|
|
1824
1812
|
const entry = { type: event, data, metadata, timestamp: Date.now(), userId: metadata.userId };
|
|
@@ -1879,7 +1867,6 @@ track.timeseries = async (event, options = {}) => {
|
|
|
1879
1867
|
}));
|
|
1880
1868
|
};
|
|
1881
1869
|
|
|
1882
|
-
// Experiment helper - A/B testing
|
|
1883
1870
|
const experiment = (name) => {
|
|
1884
1871
|
const variants = [];
|
|
1885
1872
|
let eligibilityFn = null;
|
|
@@ -1891,29 +1878,17 @@ const experiment = (name) => {
|
|
|
1891
1878
|
variants.push({ name: variantName, config, weight: options.weight || 1 });
|
|
1892
1879
|
return exp;
|
|
1893
1880
|
},
|
|
1894
|
-
eligible: (fn) => {
|
|
1895
|
-
|
|
1896
|
-
|
|
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
|
-
},
|
|
1881
|
+
eligible: (fn) => { eligibilityFn = fn; return exp; },
|
|
1882
|
+
override: (userId, variantName) => { overrides.set(userId, variantName); return exp; },
|
|
1883
|
+
rollout: (variantName, percentage) => { rolloutConfig = { variant: variantName, percentage }; return exp; },
|
|
1906
1884
|
assign: (userId) => {
|
|
1907
|
-
// Check override
|
|
1908
1885
|
if (overrides.has(userId)) {
|
|
1909
1886
|
const overrideVariant = variants.find(v => v.name === overrides.get(userId));
|
|
1910
1887
|
if (overrideVariant) return { name: overrideVariant.name, ...overrideVariant.config };
|
|
1911
1888
|
}
|
|
1912
|
-
// Check eligibility
|
|
1913
1889
|
if (eligibilityFn && typeof userId === 'object' && !eligibilityFn(userId)) {
|
|
1914
1890
|
return variants[0] ? { name: variants[0].name, ...variants[0].config } : {};
|
|
1915
1891
|
}
|
|
1916
|
-
// Consistent assignment based on userId hash
|
|
1917
1892
|
const userIdStr = typeof userId === 'object' ? userId.id || JSON.stringify(userId) : String(userId);
|
|
1918
1893
|
let hash = 0;
|
|
1919
1894
|
for (let i = 0; i < userIdStr.length; i++) {
|
|
@@ -1921,14 +1896,10 @@ const experiment = (name) => {
|
|
|
1921
1896
|
hash = hash & hash;
|
|
1922
1897
|
}
|
|
1923
1898
|
const normalized = Math.abs(hash % 100) / 100;
|
|
1924
|
-
|
|
1925
|
-
// Check rollout
|
|
1926
1899
|
if (rolloutConfig && normalized < rolloutConfig.percentage) {
|
|
1927
1900
|
const rolloutVariant = variants.find(v => v.name === rolloutConfig.variant);
|
|
1928
1901
|
if (rolloutVariant) return { name: rolloutVariant.name, ...rolloutVariant.config };
|
|
1929
1902
|
}
|
|
1930
|
-
|
|
1931
|
-
// Weight-based selection
|
|
1932
1903
|
const totalWeight = variants.reduce((sum, v) => sum + v.weight, 0);
|
|
1933
1904
|
let cumulative = 0;
|
|
1934
1905
|
for (const v of variants) {
|
|
@@ -1938,20 +1909,14 @@ const experiment = (name) => {
|
|
|
1938
1909
|
return variants[0] ? { name: variants[0].name, ...variants[0].config } : {};
|
|
1939
1910
|
},
|
|
1940
1911
|
track: {
|
|
1941
|
-
exposure: async (userId) => {
|
|
1942
|
-
|
|
1943
|
-
},
|
|
1944
|
-
conversion: async (userId, data = {}) => {
|
|
1945
|
-
await track('experiment.conversion', { experiment: name, userId, ...data });
|
|
1946
|
-
}
|
|
1912
|
+
exposure: async (userId) => { await track('experiment.exposure', { experiment: name, userId }); },
|
|
1913
|
+
conversion: async (userId, data = {}) => { await track('experiment.conversion', { experiment: name, userId, ...data }); }
|
|
1947
1914
|
},
|
|
1948
1915
|
results: async () => {
|
|
1949
1916
|
const exposures = __tracked_events__.filter(e => e.type === 'experiment.exposure' && e.data.experiment === name);
|
|
1950
1917
|
const conversions = __tracked_events__.filter(e => e.type === 'experiment.conversion' && e.data.experiment === name);
|
|
1951
1918
|
const result = {};
|
|
1952
|
-
for (const v of variants) {
|
|
1953
|
-
result[v.name] = { exposures: 0, conversions: 0 };
|
|
1954
|
-
}
|
|
1919
|
+
for (const v of variants) result[v.name] = { exposures: 0, conversions: 0 };
|
|
1955
1920
|
for (const e of exposures) {
|
|
1956
1921
|
const variantName = exp.assign(e.data.userId).name;
|
|
1957
1922
|
if (result[variantName]) result[variantName].exposures++;
|
|
@@ -1963,39 +1928,29 @@ const experiment = (name) => {
|
|
|
1963
1928
|
return result;
|
|
1964
1929
|
}
|
|
1965
1930
|
};
|
|
1966
|
-
|
|
1967
1931
|
return exp;
|
|
1968
1932
|
};
|
|
1969
1933
|
|
|
1970
|
-
// Execute event and wait for result ($.do) - mirrors ai-workflows do
|
|
1971
1934
|
const __doEvent__ = async (event, data) => {
|
|
1972
1935
|
__workflow_history__.push({ type: 'action', name: 'do:' + event, data, timestamp: Date.now() });
|
|
1973
|
-
|
|
1974
1936
|
const parsed = __parseEvent__(event);
|
|
1975
1937
|
if (!parsed) throw new Error('Invalid event format: ' + event + '. Expected Noun.event');
|
|
1976
|
-
|
|
1977
1938
|
const key = parsed.noun + '.' + parsed.event;
|
|
1978
1939
|
const handlers = __event_handlers__.get(key) || [];
|
|
1979
1940
|
if (handlers.length === 0) throw new Error('No handler registered for ' + event);
|
|
1980
|
-
|
|
1981
1941
|
return await handlers[0].handler(data, $);
|
|
1982
1942
|
};
|
|
1983
1943
|
|
|
1984
|
-
// Try event (non-durable) - mirrors ai-workflows try
|
|
1985
1944
|
const __tryEvent__ = async (event, data) => {
|
|
1986
1945
|
__workflow_history__.push({ type: 'action', name: 'try:' + event, data, timestamp: Date.now() });
|
|
1987
|
-
|
|
1988
1946
|
const parsed = __parseEvent__(event);
|
|
1989
1947
|
if (!parsed) throw new Error('Invalid event format: ' + event + '. Expected Noun.event');
|
|
1990
|
-
|
|
1991
1948
|
const key = parsed.noun + '.' + parsed.event;
|
|
1992
1949
|
const handlers = __event_handlers__.get(key) || [];
|
|
1993
1950
|
if (handlers.length === 0) throw new Error('No handler registered for ' + event);
|
|
1994
|
-
|
|
1995
1951
|
return await handlers[0].handler(data, $);
|
|
1996
1952
|
};
|
|
1997
1953
|
|
|
1998
|
-
// Queue implementation
|
|
1999
1954
|
const __queues__ = new Map();
|
|
2000
1955
|
const __queue_stats__ = new Map();
|
|
2001
1956
|
const queue = (name) => {
|
|
@@ -2022,7 +1977,7 @@ const queue = (name) => {
|
|
|
2022
1977
|
} catch (e) {
|
|
2023
1978
|
stats.failed++;
|
|
2024
1979
|
if (job.attempts < (job.options.maxRetries || 3)) {
|
|
2025
|
-
q.push(job);
|
|
1980
|
+
q.push(job);
|
|
2026
1981
|
stats.retried++;
|
|
2027
1982
|
}
|
|
2028
1983
|
}
|
|
@@ -2036,7 +1991,6 @@ const queue = (name) => {
|
|
|
2036
1991
|
stats.processed += batch.length;
|
|
2037
1992
|
} catch (e) {
|
|
2038
1993
|
stats.failed += batch.length;
|
|
2039
|
-
// Put back for retry
|
|
2040
1994
|
for (const job of batch) {
|
|
2041
1995
|
if (job.attempts < (job.options.maxRetries || 3)) {
|
|
2042
1996
|
job.attempts++;
|
|
@@ -2054,7 +2008,6 @@ const queue = (name) => {
|
|
|
2054
2008
|
};
|
|
2055
2009
|
};
|
|
2056
2010
|
|
|
2057
|
-
// Actor pattern implementation
|
|
2058
2011
|
const __actors__ = new Map();
|
|
2059
2012
|
const actor = (type) => ({
|
|
2060
2013
|
register: (id, state = {}) => {
|
|
@@ -2066,32 +2019,23 @@ const actor = (type) => ({
|
|
|
2066
2019
|
send: async (id, message) => {
|
|
2067
2020
|
const a = __actors__.get(type + ':' + id);
|
|
2068
2021
|
if (!a) throw new Error('Actor not found: ' + type + ':' + id);
|
|
2069
|
-
// Handle message (stub - in real impl would dispatch to actor handler)
|
|
2070
2022
|
return { delivered: true };
|
|
2071
2023
|
}
|
|
2072
2024
|
});
|
|
2073
2025
|
|
|
2074
|
-
// Actions API (workflow actions)
|
|
2075
2026
|
const __actions__ = new Map();
|
|
2076
2027
|
const actions = {
|
|
2077
2028
|
async create(options) {
|
|
2078
2029
|
const id = __generateId__();
|
|
2079
2030
|
const action = {
|
|
2080
|
-
id,
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
action: options.action,
|
|
2084
|
-
metadata: options.metadata || {},
|
|
2085
|
-
status: 'pending',
|
|
2086
|
-
createdAt: new Date(),
|
|
2087
|
-
updatedAt: new Date()
|
|
2031
|
+
id, actor: options.actor, object: options.object, action: options.action,
|
|
2032
|
+
metadata: options.metadata || {}, status: 'pending',
|
|
2033
|
+
createdAt: new Date(), updatedAt: new Date()
|
|
2088
2034
|
};
|
|
2089
2035
|
__actions__.set(id, action);
|
|
2090
2036
|
return action;
|
|
2091
2037
|
},
|
|
2092
|
-
async get(id) {
|
|
2093
|
-
return __actions__.get(id) || null;
|
|
2094
|
-
},
|
|
2038
|
+
async get(id) { return __actions__.get(id) || null; },
|
|
2095
2039
|
async start(id) {
|
|
2096
2040
|
const action = __actions__.get(id);
|
|
2097
2041
|
if (!action) throw new Error('Action not found: ' + id);
|
|
@@ -2143,24 +2087,18 @@ const actions = {
|
|
|
2143
2087
|
}
|
|
2144
2088
|
};
|
|
2145
2089
|
|
|
2146
|
-
// Artifacts API (for storing results)
|
|
2147
2090
|
const __artifacts__ = new Map();
|
|
2148
2091
|
const artifacts = {
|
|
2149
2092
|
async store(options) {
|
|
2150
2093
|
const id = __generateId__();
|
|
2151
2094
|
const artifact = {
|
|
2152
|
-
id,
|
|
2153
|
-
|
|
2154
|
-
data: options.data,
|
|
2155
|
-
metadata: options.metadata || {},
|
|
2156
|
-
createdAt: new Date()
|
|
2095
|
+
id, type: options.type, data: options.data,
|
|
2096
|
+
metadata: options.metadata || {}, createdAt: new Date()
|
|
2157
2097
|
};
|
|
2158
2098
|
__artifacts__.set(id, artifact);
|
|
2159
2099
|
return artifact;
|
|
2160
2100
|
},
|
|
2161
|
-
async get(id) {
|
|
2162
|
-
return __artifacts__.get(id) || null;
|
|
2163
|
-
},
|
|
2101
|
+
async get(id) { return __artifacts__.get(id) || null; },
|
|
2164
2102
|
async list(options = {}) {
|
|
2165
2103
|
let results = Array.from(__artifacts__.values());
|
|
2166
2104
|
if (options.type) results = results.filter(a => a.type === options.type);
|
|
@@ -2169,17 +2107,12 @@ const artifacts = {
|
|
|
2169
2107
|
}
|
|
2170
2108
|
};
|
|
2171
2109
|
|
|
2172
|
-
// Events API (for event sourcing)
|
|
2173
2110
|
const __events__ = [];
|
|
2174
2111
|
const events = {
|
|
2175
2112
|
async record(options) {
|
|
2176
2113
|
const event = {
|
|
2177
|
-
id: __generateId__(),
|
|
2178
|
-
|
|
2179
|
-
subject: options.subject,
|
|
2180
|
-
data: options.data,
|
|
2181
|
-
metadata: options.metadata || {},
|
|
2182
|
-
timestamp: new Date()
|
|
2114
|
+
id: __generateId__(), type: options.type, subject: options.subject,
|
|
2115
|
+
data: options.data, metadata: options.metadata || {}, timestamp: new Date()
|
|
2183
2116
|
};
|
|
2184
2117
|
__events__.push(event);
|
|
2185
2118
|
return event;
|
|
@@ -2191,7 +2124,6 @@ const events = {
|
|
|
2191
2124
|
if (options.limit) results = results.slice(0, options.limit);
|
|
2192
2125
|
return results;
|
|
2193
2126
|
},
|
|
2194
|
-
// Query is an alias for list with more flexible filtering
|
|
2195
2127
|
async query(options = {}) {
|
|
2196
2128
|
let results = [...__events__];
|
|
2197
2129
|
if (options.type) results = results.filter(e => e.type === options.type);
|
|
@@ -2207,16 +2139,12 @@ const events = {
|
|
|
2207
2139
|
},
|
|
2208
2140
|
async replay(handler, options = {}) {
|
|
2209
2141
|
const evts = await events.list(options);
|
|
2210
|
-
for (const evt of evts)
|
|
2211
|
-
await handler(evt);
|
|
2212
|
-
}
|
|
2142
|
+
for (const evt of evts) await handler(evt);
|
|
2213
2143
|
},
|
|
2214
|
-
// Subscribe to events
|
|
2215
2144
|
subscribe(type, handler) {
|
|
2216
2145
|
on(type, handler);
|
|
2217
2146
|
return { unsubscribe: () => {} };
|
|
2218
2147
|
},
|
|
2219
|
-
// Emit an event (alias for record + send)
|
|
2220
2148
|
async emit(type, data, metadata = {}) {
|
|
2221
2149
|
const event = await events.record({ type, data, metadata });
|
|
2222
2150
|
await send(type, data);
|
|
@@ -2224,12 +2152,9 @@ const events = {
|
|
|
2224
2152
|
}
|
|
2225
2153
|
};
|
|
2226
2154
|
|
|
2227
|
-
// MDX Parsing helpers (basic implementation)
|
|
2228
2155
|
const parse = (content) => {
|
|
2229
2156
|
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
|
-
}
|
|
2157
|
+
if (!frontmatterMatch) return { data: {}, content: content.trim(), type: null, id: null };
|
|
2233
2158
|
const [, frontmatter, body] = frontmatterMatch;
|
|
2234
2159
|
const data = {};
|
|
2235
2160
|
let currentKey = null;
|
|
@@ -2240,13 +2165,8 @@ const parse = (content) => {
|
|
|
2240
2165
|
if (keyMatch) {
|
|
2241
2166
|
currentKey = keyMatch[1];
|
|
2242
2167
|
const value = keyMatch[2].trim();
|
|
2243
|
-
if (value === '') {
|
|
2244
|
-
|
|
2245
|
-
inArray = true;
|
|
2246
|
-
} else {
|
|
2247
|
-
data[currentKey] = value;
|
|
2248
|
-
inArray = false;
|
|
2249
|
-
}
|
|
2168
|
+
if (value === '') { data[currentKey] = []; inArray = true; }
|
|
2169
|
+
else { data[currentKey] = value; inArray = false; }
|
|
2250
2170
|
} else if (inArray && currentKey && line.match(/^\\s+-\\s+(.+)$/)) {
|
|
2251
2171
|
const itemMatch = line.match(/^\\s+-\\s+(.+)$/);
|
|
2252
2172
|
if (itemMatch) {
|
|
@@ -2255,12 +2175,7 @@ const parse = (content) => {
|
|
|
2255
2175
|
}
|
|
2256
2176
|
}
|
|
2257
2177
|
}
|
|
2258
|
-
return {
|
|
2259
|
-
data,
|
|
2260
|
-
content: body.trim(),
|
|
2261
|
-
type: data.$type || data['@type'] || null,
|
|
2262
|
-
id: data.$id || data['@id'] || null
|
|
2263
|
-
};
|
|
2178
|
+
return { data, content: body.trim(), type: data.$type || data['@type'] || null, id: data.$id || data['@id'] || null };
|
|
2264
2179
|
};
|
|
2265
2180
|
|
|
2266
2181
|
const stringify = (doc) => {
|
|
@@ -2271,9 +2186,7 @@ const stringify = (doc) => {
|
|
|
2271
2186
|
if (Array.isArray(value)) {
|
|
2272
2187
|
lines.push(key + ':');
|
|
2273
2188
|
for (const item of value) lines.push(' - ' + item);
|
|
2274
|
-
} else
|
|
2275
|
-
lines.push(key + ': ' + value);
|
|
2276
|
-
}
|
|
2189
|
+
} else lines.push(key + ': ' + value);
|
|
2277
2190
|
}
|
|
2278
2191
|
lines.push('---');
|
|
2279
2192
|
if (doc.content) lines.push('', doc.content);
|
|
@@ -2288,17 +2201,12 @@ const toAst = (doc) => {
|
|
|
2288
2201
|
else if (line.startsWith('## ')) children.push({ type: 'heading', depth: 2, text: line.slice(3) });
|
|
2289
2202
|
else if (line.startsWith('- ')) {
|
|
2290
2203
|
const lastList = children[children.length - 1];
|
|
2291
|
-
if (lastList?.type === 'list')
|
|
2292
|
-
|
|
2293
|
-
} else {
|
|
2294
|
-
children.push({ type: 'list', items: [line.slice(2)] });
|
|
2295
|
-
}
|
|
2204
|
+
if (lastList?.type === 'list') lastList.items.push(line.slice(2));
|
|
2205
|
+
else children.push({ type: 'list', items: [line.slice(2)] });
|
|
2296
2206
|
} else if (line.startsWith('\`\`\`')) {
|
|
2297
2207
|
const lang = line.slice(3).trim();
|
|
2298
2208
|
children.push({ type: 'code', lang: lang || null, value: '' });
|
|
2299
|
-
} else if (line.trim()) {
|
|
2300
|
-
children.push({ type: 'paragraph', text: line });
|
|
2301
|
-
}
|
|
2209
|
+
} else if (line.trim()) children.push({ type: 'paragraph', text: line });
|
|
2302
2210
|
}
|
|
2303
2211
|
return { type: 'root', children };
|
|
2304
2212
|
};
|
|
@@ -2312,9 +2220,7 @@ const renderMarkdown = (doc, options = {}) => {
|
|
|
2312
2220
|
if (Array.isArray(value)) {
|
|
2313
2221
|
result += key + ':\\n';
|
|
2314
2222
|
for (const item of value) result += ' - ' + item + '\\n';
|
|
2315
|
-
} else
|
|
2316
|
-
result += key + ': ' + value + '\\n';
|
|
2317
|
-
}
|
|
2223
|
+
} else result += key + ': ' + value + '\\n';
|
|
2318
2224
|
}
|
|
2319
2225
|
result += '---\\n';
|
|
2320
2226
|
}
|
|
@@ -2322,7 +2228,6 @@ const renderMarkdown = (doc, options = {}) => {
|
|
|
2322
2228
|
return result;
|
|
2323
2229
|
};
|
|
2324
2230
|
|
|
2325
|
-
// Component factory (basic implementation)
|
|
2326
2231
|
const createComponents = (createElement) => {
|
|
2327
2232
|
const components = {};
|
|
2328
2233
|
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'];
|
|
@@ -2349,9 +2254,7 @@ const extractTests = (content) => {
|
|
|
2349
2254
|
const tests = [];
|
|
2350
2255
|
const regex = /\`\`\`(?:ts|js)\\s+test[^\\n]*\\n([\\s\\S]*?)\`\`\`/g;
|
|
2351
2256
|
let match;
|
|
2352
|
-
while ((match = regex.exec(content)) !== null) {
|
|
2353
|
-
tests.push({ code: match[1].trim() });
|
|
2354
|
-
}
|
|
2257
|
+
while ((match = regex.exec(content)) !== null) tests.push({ code: match[1].trim() });
|
|
2355
2258
|
return tests;
|
|
2356
2259
|
};
|
|
2357
2260
|
|
|
@@ -2362,24 +2265,18 @@ const parseMeta = (meta) => {
|
|
|
2362
2265
|
if (part.includes('=')) {
|
|
2363
2266
|
const [key, value] = part.split('=');
|
|
2364
2267
|
result[key] = value;
|
|
2365
|
-
} else
|
|
2366
|
-
result[part] = true;
|
|
2367
|
-
}
|
|
2268
|
+
} else result[part] = true;
|
|
2368
2269
|
}
|
|
2369
2270
|
return result;
|
|
2370
2271
|
};
|
|
2371
2272
|
|
|
2372
|
-
// Simple createElement for testing
|
|
2373
2273
|
const createElement = (type, props, ...children) => ({ type, props: props || {}, children });
|
|
2374
2274
|
|
|
2375
|
-
// Graph/relationship helper functions
|
|
2376
2275
|
const extractLinks = (content) => {
|
|
2377
2276
|
const links = [];
|
|
2378
2277
|
const regex = /\\[([^\\]]+)\\]\\(([^)]+)\\)/g;
|
|
2379
2278
|
let match;
|
|
2380
|
-
while ((match = regex.exec(content)) !== null) {
|
|
2381
|
-
links.push({ text: match[1], url: match[2] });
|
|
2382
|
-
}
|
|
2279
|
+
while ((match = regex.exec(content)) !== null) links.push({ text: match[1], url: match[2] });
|
|
2383
2280
|
return links;
|
|
2384
2281
|
};
|
|
2385
2282
|
|
|
@@ -2387,12 +2284,8 @@ const extractRelationships = (doc) => {
|
|
|
2387
2284
|
const relationships = [];
|
|
2388
2285
|
const url = doc.id || doc.url;
|
|
2389
2286
|
if (!url) return relationships;
|
|
2390
|
-
// Extract from content links
|
|
2391
2287
|
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
|
|
2288
|
+
for (const link of links) relationships.push({ from: url, to: link.url, type: 'links', label: link.text });
|
|
2396
2289
|
for (const [key, value] of Object.entries(doc.data || {})) {
|
|
2397
2290
|
if (typeof value === 'string' && (value.startsWith('http') || value.startsWith('/'))) {
|
|
2398
2291
|
relationships.push({ from: url, to: value, type: key });
|
|
@@ -2408,21 +2301,19 @@ const extractRelationships = (doc) => {
|
|
|
2408
2301
|
return relationships;
|
|
2409
2302
|
};
|
|
2410
2303
|
|
|
2411
|
-
const withRelationships = (doc) => {
|
|
2412
|
-
return { ...doc, relationships: extractRelationships(doc) };
|
|
2413
|
-
};
|
|
2304
|
+
const withRelationships = (doc) => ({ ...doc, relationships: extractRelationships(doc) });
|
|
2414
2305
|
|
|
2415
2306
|
const resolveUrl = (entity) => {
|
|
2416
2307
|
if (entity.url) return entity.url;
|
|
2417
|
-
if (entity.ns && entity.type && entity.id)
|
|
2418
|
-
|
|
2419
|
-
}
|
|
2420
|
-
if (entity.ns && entity.id) {
|
|
2421
|
-
return 'https://' + entity.ns + '/' + entity.id;
|
|
2422
|
-
}
|
|
2308
|
+
if (entity.ns && entity.type && entity.id) return 'https://' + entity.ns + '/' + entity.type + '/' + entity.id;
|
|
2309
|
+
if (entity.ns && entity.id) return 'https://' + entity.ns + '/' + entity.id;
|
|
2423
2310
|
return null;
|
|
2424
2311
|
};
|
|
2312
|
+
`
|
|
2313
|
+
}
|
|
2425
2314
|
|
|
2315
|
+
function getContextObjectTemplate(): string {
|
|
2316
|
+
return `
|
|
2426
2317
|
// Context object ($) - unified SDK context (aligned with ai-workflows WorkflowContext)
|
|
2427
2318
|
const $ = {
|
|
2428
2319
|
ns: __SDK_CONFIG__.ns,
|
|
@@ -2442,60 +2333,40 @@ const $ = {
|
|
|
2442
2333
|
track,
|
|
2443
2334
|
experiment,
|
|
2444
2335
|
delay,
|
|
2445
|
-
// Workflow state
|
|
2446
2336
|
state: {},
|
|
2447
2337
|
history: __workflow_history__,
|
|
2448
|
-
// User context
|
|
2449
2338
|
user: { id: 'test-user', name: 'Test User', role: 'admin' },
|
|
2450
2339
|
request: { method: 'GET', path: '/', headers: {}, body: null },
|
|
2451
2340
|
env: { NODE_ENV: 'test' },
|
|
2452
2341
|
config: {},
|
|
2453
2342
|
context: {},
|
|
2454
2343
|
meta: {},
|
|
2455
|
-
// Logging
|
|
2456
2344
|
log: (message, data) => {
|
|
2457
2345
|
__workflow_history__.push({ type: 'action', name: 'log', data: { message, data }, timestamp: Date.now() });
|
|
2458
2346
|
console.log('[sdk] ' + message, data ?? '');
|
|
2459
2347
|
},
|
|
2460
2348
|
error: console.error,
|
|
2461
2349
|
warn: console.warn,
|
|
2462
|
-
// Scoped execution
|
|
2463
2350
|
async scope(overrides, fn) {
|
|
2464
2351
|
const prev = { ns: $.ns, user: $.user, state: { ...$.state } };
|
|
2465
2352
|
Object.assign($, overrides);
|
|
2466
2353
|
try { return await fn(); }
|
|
2467
2354
|
finally { Object.assign($, prev); }
|
|
2468
2355
|
},
|
|
2469
|
-
// Workflow helpers
|
|
2470
2356
|
getHandlers() { return { events: Array.from(__event_handlers__.keys()), schedules: __schedule_handlers__.length }; },
|
|
2471
2357
|
clearHandlers() { __event_handlers__.clear(); __schedule_handlers__.length = 0; },
|
|
2472
2358
|
getHistory() { return [...__workflow_history__]; },
|
|
2473
2359
|
clearHistory() { __workflow_history__.length = 0; }
|
|
2474
2360
|
};
|
|
2475
2361
|
|
|
2476
|
-
// Standalone exports
|
|
2362
|
+
// Standalone exports
|
|
2477
2363
|
const api = {};
|
|
2478
2364
|
const search = __db_core__.search.bind(__db_core__);
|
|
2479
2365
|
`
|
|
2480
2366
|
}
|
|
2481
2367
|
|
|
2482
|
-
|
|
2483
|
-
|
|
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
|
-
|
|
2368
|
+
// Helper to get the remote SDK template
|
|
2369
|
+
function getRemoteSDKTemplate(rpcUrl: string, token: string, ns: string): string {
|
|
2499
2370
|
return `
|
|
2500
2371
|
// ============================================================
|
|
2501
2372
|
// SDK - Thin RPC Proxy ($, db, ai, api, on, send)
|
|
@@ -2515,7 +2386,6 @@ const __rpc__ = {
|
|
|
2515
2386
|
headers['Authorization'] = 'Bearer ' + __SDK_CONFIG__.token;
|
|
2516
2387
|
}
|
|
2517
2388
|
|
|
2518
|
-
// Serialize functions for remote execution
|
|
2519
2389
|
const serializedArgs = args.map(arg => {
|
|
2520
2390
|
if (typeof arg === 'function') {
|
|
2521
2391
|
return { __fn: arg.toString().replace(/"/g, "'") };
|
|
@@ -2538,23 +2408,16 @@ const __rpc__ = {
|
|
|
2538
2408
|
}
|
|
2539
2409
|
};
|
|
2540
2410
|
|
|
2541
|
-
// Store for user-defined values
|
|
2542
2411
|
const __userDefinitions__ = new Map();
|
|
2543
2412
|
|
|
2544
|
-
// Thin proxy that converts property access to RPC paths
|
|
2545
2413
|
const __createProxy__ = (path = '') => {
|
|
2546
|
-
// Track stored values for this proxy instance
|
|
2547
2414
|
const localStore = new Map();
|
|
2548
2415
|
|
|
2549
2416
|
const proxy = new Proxy(() => {}, {
|
|
2550
2417
|
get: (target, prop, receiver) => {
|
|
2551
|
-
// Handle JSON serialization
|
|
2552
2418
|
if (prop === 'toJSON') {
|
|
2553
|
-
// Return stored values as a plain object
|
|
2554
2419
|
const obj = { __rpcPath: path };
|
|
2555
|
-
for (const [key, value] of localStore)
|
|
2556
|
-
obj[key] = value;
|
|
2557
|
-
}
|
|
2420
|
+
for (const [key, value] of localStore) obj[key] = value;
|
|
2558
2421
|
for (const [key, value] of __userDefinitions__) {
|
|
2559
2422
|
if (key.startsWith(path ? path + '.' : '') && !key.slice(path ? path.length + 1 : 0).includes('.')) {
|
|
2560
2423
|
const localKey = key.slice(path ? path.length + 1 : 0);
|
|
@@ -2566,42 +2429,25 @@ const __createProxy__ = (path = '') => {
|
|
|
2566
2429
|
if (prop === Symbol.toPrimitive || prop === 'valueOf' || prop === 'toString') {
|
|
2567
2430
|
return () => path || '[SDK Proxy]';
|
|
2568
2431
|
}
|
|
2569
|
-
if (prop === Symbol.toStringTag)
|
|
2570
|
-
|
|
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
|
|
2432
|
+
if (prop === Symbol.toStringTag) return 'SDKProxy';
|
|
2433
|
+
if (prop === 'then' || prop === 'catch' || prop === 'finally') return undefined;
|
|
2576
2434
|
if (prop === 'should') {
|
|
2577
|
-
// Build an object from stored values for assertion
|
|
2578
2435
|
const obj = {};
|
|
2579
|
-
for (const [key, value] of localStore)
|
|
2580
|
-
obj[key] = value;
|
|
2581
|
-
}
|
|
2436
|
+
for (const [key, value] of localStore) obj[key] = value;
|
|
2582
2437
|
for (const [key, value] of __userDefinitions__) {
|
|
2583
2438
|
if (key.startsWith(path ? path + '.' : '') && !key.slice(path ? path.length + 1 : 0).includes('.')) {
|
|
2584
2439
|
const localKey = key.slice(path ? path.length + 1 : 0);
|
|
2585
2440
|
if (localKey) obj[localKey] = value;
|
|
2586
2441
|
}
|
|
2587
2442
|
}
|
|
2588
|
-
|
|
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
|
|
2443
|
+
if (Object.keys(obj).length > 0) return __createShouldChain__(obj);
|
|
2593
2444
|
return __createShouldChain__(path ? { __path: path } : { __sdk: true, ns: __SDK_CONFIG__.ns });
|
|
2594
2445
|
}
|
|
2595
2446
|
|
|
2596
2447
|
const fullPath = path ? path + '.' + String(prop) : String(prop);
|
|
2597
2448
|
|
|
2598
|
-
|
|
2599
|
-
if (
|
|
2600
|
-
return localStore.get(String(prop));
|
|
2601
|
-
}
|
|
2602
|
-
if (__userDefinitions__.has(fullPath)) {
|
|
2603
|
-
return __userDefinitions__.get(fullPath);
|
|
2604
|
-
}
|
|
2449
|
+
if (localStore.has(String(prop))) return localStore.get(String(prop));
|
|
2450
|
+
if (__userDefinitions__.has(fullPath)) return __userDefinitions__.get(fullPath);
|
|
2605
2451
|
|
|
2606
2452
|
return __createProxy__(fullPath);
|
|
2607
2453
|
},
|
|
@@ -2614,23 +2460,18 @@ const __createProxy__ = (path = '') => {
|
|
|
2614
2460
|
},
|
|
2615
2461
|
|
|
2616
2462
|
apply: (_, __, args) => {
|
|
2617
|
-
// Handle tagged template literals
|
|
2618
2463
|
if (Array.isArray(args[0]) && 'raw' in args[0]) {
|
|
2619
2464
|
const strings = args[0];
|
|
2620
2465
|
const values = args.slice(1);
|
|
2621
2466
|
const text = strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '');
|
|
2622
2467
|
return __rpc__.do(path, text);
|
|
2623
2468
|
}
|
|
2624
|
-
|
|
2625
2469
|
return __rpc__.do(path, ...args);
|
|
2626
2470
|
},
|
|
2627
2471
|
|
|
2628
|
-
// Prevent enumeration from causing infinite loops
|
|
2629
2472
|
ownKeys: () => {
|
|
2630
2473
|
const keys = [];
|
|
2631
|
-
for (const [key, value] of localStore)
|
|
2632
|
-
keys.push(key);
|
|
2633
|
-
}
|
|
2474
|
+
for (const [key, value] of localStore) keys.push(key);
|
|
2634
2475
|
for (const [key, value] of __userDefinitions__) {
|
|
2635
2476
|
if (key.startsWith(path ? path + '.' : '') && !key.slice(path ? path.length + 1 : 0).includes('.')) {
|
|
2636
2477
|
const localKey = key.slice(path ? path.length + 1 : 0);
|
|
@@ -2652,7 +2493,6 @@ const __createProxy__ = (path = '') => {
|
|
|
2652
2493
|
return proxy;
|
|
2653
2494
|
};
|
|
2654
2495
|
|
|
2655
|
-
// Root proxy and named exports
|
|
2656
2496
|
const $ = __createProxy__();
|
|
2657
2497
|
const db = $.db;
|
|
2658
2498
|
const ai = $.ai;
|
|
@@ -2664,7 +2504,6 @@ const track = $.track;
|
|
|
2664
2504
|
const every = $.every;
|
|
2665
2505
|
const decide = $.decide;
|
|
2666
2506
|
|
|
2667
|
-
// Set default namespace and context properties
|
|
2668
2507
|
$.ns = __SDK_CONFIG__.ns;
|
|
2669
2508
|
$.user = { id: 'anonymous', name: 'Anonymous', role: 'guest' };
|
|
2670
2509
|
$.request = { method: 'GET', path: '/', headers: {}, body: null };
|
|
@@ -2674,1086 +2513,3 @@ $.context = {};
|
|
|
2674
2513
|
$.meta = {};
|
|
2675
2514
|
`
|
|
2676
2515
|
}
|
|
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(
|
|
2889
|
-
/export\s+(const|let|var)\s+(\w+)\s*=/g,
|
|
2890
|
-
'$1 $2 = exports.$2 ='
|
|
2891
|
-
)
|
|
2892
|
-
|
|
2893
|
-
// Transform: export function foo(...) → function foo(...) exports.foo = foo;
|
|
2894
|
-
// Also handles async generators: export async function* foo
|
|
2895
|
-
code = code.replace(
|
|
2896
|
-
/export\s+(async\s+)?function(\*?)\s+(\w+)/g,
|
|
2897
|
-
'$1function$2 $3'
|
|
2898
|
-
)
|
|
2899
|
-
// Add exports for functions after their definition
|
|
2900
|
-
const funcNames = [...moduleCode.matchAll(/export\s+(?:async\s+)?function\*?\s+(\w+)/g)]
|
|
2901
|
-
for (const [, name] of funcNames) {
|
|
2902
|
-
code += `\nexports.${name} = ${name};`
|
|
2903
|
-
}
|
|
2904
|
-
|
|
2905
|
-
// Transform: export class Foo → class Foo; exports.Foo = Foo;
|
|
2906
|
-
code = code.replace(/export\s+class\s+(\w+)/g, 'class $1')
|
|
2907
|
-
const classNames = [...moduleCode.matchAll(/export\s+class\s+(\w+)/g)]
|
|
2908
|
-
for (const [, name] of classNames) {
|
|
2909
|
-
code += `\nexports.${name} = ${name};`
|
|
2910
|
-
}
|
|
2911
|
-
|
|
2912
|
-
return code
|
|
2913
|
-
}
|
|
2914
|
-
|
|
2915
|
-
/**
|
|
2916
|
-
* Wrap script to auto-return the last expression
|
|
2917
|
-
* Converts: `add(1, 2)` → `return add(1, 2)`
|
|
2918
|
-
*/
|
|
2919
|
-
function wrapScriptForReturn(script: string): string {
|
|
2920
|
-
const trimmed = script.trim()
|
|
2921
|
-
if (!trimmed) return script
|
|
2922
|
-
|
|
2923
|
-
// If script already contains a return statement anywhere, don't modify
|
|
2924
|
-
if (/\breturn\b/.test(trimmed)) return script
|
|
2925
|
-
|
|
2926
|
-
// If script starts with throw, don't modify
|
|
2927
|
-
if (/^\s*throw\b/.test(trimmed)) return script
|
|
2928
|
-
|
|
2929
|
-
// If it's a single expression (no newlines, no semicolons except at end), wrap it
|
|
2930
|
-
const withoutTrailingSemi = trimmed.replace(/;?\s*$/, '')
|
|
2931
|
-
const isSingleLine = !withoutTrailingSemi.includes('\n')
|
|
2932
|
-
|
|
2933
|
-
// Check if it looks like a single expression (no control flow, no declarations)
|
|
2934
|
-
const startsWithKeyword = /^\s*(const|let|var|if|for|while|switch|try|class|function|async\s+function)\b/.test(withoutTrailingSemi)
|
|
2935
|
-
|
|
2936
|
-
if (isSingleLine && !startsWithKeyword) {
|
|
2937
|
-
return `return ${withoutTrailingSemi}`
|
|
2938
|
-
}
|
|
2939
|
-
|
|
2940
|
-
// For multi-statement scripts, try to return the last expression
|
|
2941
|
-
const lines = trimmed.split('\n')
|
|
2942
|
-
const lastLineRaw = lines[lines.length - 1]
|
|
2943
|
-
if (!lastLineRaw) return script
|
|
2944
|
-
const lastLine = lastLineRaw.trim()
|
|
2945
|
-
|
|
2946
|
-
// If last line is an expression (not a declaration, control flow, or throw)
|
|
2947
|
-
if (lastLine && !/^\s*(const|let|var|if|for|while|switch|try|class|function|return|throw)\b/.test(lastLine)) {
|
|
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';
|
|
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
|
-
${module ? `
|
|
3007
|
-
// Execute module code
|
|
3008
|
-
try {
|
|
3009
|
-
${module}
|
|
3010
|
-
} catch (e) {
|
|
3011
|
-
console.error('Module error:', e.message);
|
|
3012
|
-
}
|
|
3013
|
-
` : '// No module code provided'}
|
|
3014
|
-
|
|
3015
|
-
// Expose all exports as top-level variables for tests and scripts
|
|
3016
|
-
// This allows: export const add = (a, b) => a + b; then later: add(1, 2)
|
|
3017
|
-
${rawModule ? `
|
|
3018
|
-
const { ${exportNames} } = exports;
|
|
3019
|
-
`.trim() : ''}
|
|
3020
|
-
|
|
3021
|
-
// ============================================================
|
|
3022
|
-
// RPC SERVER - Expose exports via capnweb
|
|
3023
|
-
// ============================================================
|
|
3024
|
-
class ExportsRpcTarget extends RpcTarget {
|
|
3025
|
-
// Dynamically expose all exports as RPC methods
|
|
3026
|
-
constructor() {
|
|
3027
|
-
super();
|
|
3028
|
-
for (const [key, value] of Object.entries(exports)) {
|
|
3029
|
-
if (typeof value === 'function') {
|
|
3030
|
-
this[key] = value;
|
|
3031
|
-
}
|
|
3032
|
-
}
|
|
3033
|
-
}
|
|
3034
|
-
|
|
3035
|
-
// List available exports
|
|
3036
|
-
list() {
|
|
3037
|
-
return Object.keys(exports);
|
|
3038
|
-
}
|
|
3039
|
-
|
|
3040
|
-
// Get an export by name
|
|
3041
|
-
get(name) {
|
|
3042
|
-
return exports[name];
|
|
3043
|
-
}
|
|
3044
|
-
}
|
|
3045
|
-
|
|
3046
|
-
// ============================================================
|
|
3047
|
-
// WORKER ENTRY POINT
|
|
3048
|
-
// ============================================================
|
|
3049
|
-
export default {
|
|
3050
|
-
async fetch(request, env) {
|
|
3051
|
-
const url = new URL(request.url);
|
|
3052
|
-
|
|
3053
|
-
// Route: GET / - Return info about exports
|
|
3054
|
-
if (request.method === 'GET' && url.pathname === '/') {
|
|
3055
|
-
return Response.json({
|
|
3056
|
-
exports: Object.keys(exports),
|
|
3057
|
-
rpc: '/rpc',
|
|
3058
|
-
execute: '/execute'
|
|
3059
|
-
});
|
|
3060
|
-
}
|
|
3061
|
-
|
|
3062
|
-
// Route: /rpc - capnweb RPC to module exports
|
|
3063
|
-
if (url.pathname === '/rpc') {
|
|
3064
|
-
return newWorkersRpcResponse(request, new ExportsRpcTarget());
|
|
3065
|
-
}
|
|
3066
|
-
|
|
3067
|
-
// Route: GET /:name - Simple JSON endpoint to access exports
|
|
3068
|
-
if (request.method === 'GET' && url.pathname !== '/execute') {
|
|
3069
|
-
const name = url.pathname.slice(1); // Remove leading /
|
|
3070
|
-
const value = exports[name];
|
|
3071
|
-
|
|
3072
|
-
// Check if export exists
|
|
3073
|
-
if (!(name in exports)) {
|
|
3074
|
-
return Response.json({ error: \`Export "\${name}" not found\` }, { status: 404 });
|
|
3075
|
-
}
|
|
3076
|
-
|
|
3077
|
-
// If it's not a function, just return the value
|
|
3078
|
-
if (typeof value !== 'function') {
|
|
3079
|
-
return Response.json({ result: value });
|
|
3080
|
-
}
|
|
3081
|
-
|
|
3082
|
-
// It's a function - parse args and call it
|
|
3083
|
-
try {
|
|
3084
|
-
const args = [];
|
|
3085
|
-
const argsParam = url.searchParams.get('args');
|
|
3086
|
-
if (argsParam) {
|
|
3087
|
-
// Support JSON array: ?args=[1,2,3]
|
|
3088
|
-
try {
|
|
3089
|
-
const parsed = JSON.parse(argsParam);
|
|
3090
|
-
if (Array.isArray(parsed)) {
|
|
3091
|
-
args.push(...parsed);
|
|
3092
|
-
} else {
|
|
3093
|
-
args.push(parsed);
|
|
3094
|
-
}
|
|
3095
|
-
} catch {
|
|
3096
|
-
// Not JSON, use as single string arg
|
|
3097
|
-
args.push(argsParam);
|
|
3098
|
-
}
|
|
3099
|
-
} else {
|
|
3100
|
-
// Support named params: ?a=1&b=2 -> passed as object
|
|
3101
|
-
const params = Object.fromEntries(url.searchParams.entries());
|
|
3102
|
-
if (Object.keys(params).length > 0) {
|
|
3103
|
-
// Try to parse numeric values
|
|
3104
|
-
for (const [key, val] of Object.entries(params)) {
|
|
3105
|
-
const num = Number(val);
|
|
3106
|
-
params[key] = !isNaN(num) && val !== '' ? num : val;
|
|
3107
|
-
}
|
|
3108
|
-
args.push(params);
|
|
3109
|
-
}
|
|
3110
|
-
}
|
|
3111
|
-
|
|
3112
|
-
const result = await value(...args);
|
|
3113
|
-
return Response.json({ result });
|
|
3114
|
-
} catch (e) {
|
|
3115
|
-
return Response.json({ error: e.message }, { status: 500 });
|
|
3116
|
-
}
|
|
3117
|
-
}
|
|
3118
|
-
|
|
3119
|
-
// Route: /execute - Run tests and scripts
|
|
3120
|
-
// Check for TEST service binding
|
|
3121
|
-
if (!env.TEST) {
|
|
3122
|
-
return Response.json({
|
|
3123
|
-
success: false,
|
|
3124
|
-
error: 'TEST service binding not available. Ensure ai-tests worker is bound.',
|
|
3125
|
-
logs,
|
|
3126
|
-
duration: 0
|
|
3127
|
-
});
|
|
3128
|
-
}
|
|
3129
|
-
|
|
3130
|
-
// Connect to get the TestServiceCore via RPC
|
|
3131
|
-
const testService = await env.TEST.connect();
|
|
3132
|
-
|
|
3133
|
-
// Create global test functions that proxy to the RPC service
|
|
3134
|
-
const describe = (name, fn) => testService.describe(name, fn);
|
|
3135
|
-
const it = (name, fn) => testService.it(name, fn);
|
|
3136
|
-
const test = (name, fn) => testService.test(name, fn);
|
|
3137
|
-
const expect = (value, message) => testService.expect(value, message);
|
|
3138
|
-
const should = (value) => testService.should(value);
|
|
3139
|
-
const assert = testService.assert;
|
|
3140
|
-
const beforeEach = (fn) => testService.beforeEach(fn);
|
|
3141
|
-
const afterEach = (fn) => testService.afterEach(fn);
|
|
3142
|
-
const beforeAll = (fn) => testService.beforeAll(fn);
|
|
3143
|
-
const afterAll = (fn) => testService.afterAll(fn);
|
|
3144
|
-
|
|
3145
|
-
// Add skip/only modifiers
|
|
3146
|
-
it.skip = (name, fn) => testService.skip(name, fn);
|
|
3147
|
-
it.only = (name, fn) => testService.only(name, fn);
|
|
3148
|
-
test.skip = it.skip;
|
|
3149
|
-
test.only = it.only;
|
|
3150
|
-
|
|
3151
|
-
let scriptResult = undefined;
|
|
3152
|
-
let scriptError = null;
|
|
3153
|
-
let testResults = undefined;
|
|
3154
|
-
|
|
3155
|
-
// ============================================================
|
|
3156
|
-
// USER TEST CODE (embedded at generation time)
|
|
3157
|
-
// ============================================================
|
|
3158
|
-
|
|
3159
|
-
${tests ? `
|
|
3160
|
-
// Register tests
|
|
3161
|
-
try {
|
|
3162
|
-
${tests}
|
|
3163
|
-
} catch (e) {
|
|
3164
|
-
console.error('Test registration error:', e.message);
|
|
3165
|
-
}
|
|
3166
|
-
` : '// No test code provided'}
|
|
3167
|
-
|
|
3168
|
-
// Execute user script
|
|
3169
|
-
${script ? `
|
|
3170
|
-
try {
|
|
3171
|
-
scriptResult = await (async () => {
|
|
3172
|
-
${script}
|
|
3173
|
-
})();
|
|
3174
|
-
} catch (e) {
|
|
3175
|
-
console.error('Script error:', e.message);
|
|
3176
|
-
scriptError = e.message;
|
|
3177
|
-
}
|
|
3178
|
-
` : '// No script code provided'}
|
|
3179
|
-
|
|
3180
|
-
// Run tests if any were registered
|
|
3181
|
-
${tests ? `
|
|
3182
|
-
try {
|
|
3183
|
-
testResults = await testService.run();
|
|
3184
|
-
} catch (e) {
|
|
3185
|
-
console.error('Test run error:', e.message);
|
|
3186
|
-
testResults = { total: 0, passed: 0, failed: 1, skipped: 0, tests: [], duration: 0, error: e.message };
|
|
3187
|
-
}
|
|
3188
|
-
` : ''}
|
|
3189
|
-
|
|
3190
|
-
const hasTests = ${tests ? 'true' : 'false'};
|
|
3191
|
-
const success = scriptError === null && (!hasTests || (testResults && testResults.failed === 0));
|
|
3192
|
-
|
|
3193
|
-
return Response.json({
|
|
3194
|
-
success,
|
|
3195
|
-
value: scriptResult,
|
|
3196
|
-
logs,
|
|
3197
|
-
testResults: hasTests ? testResults : undefined,
|
|
3198
|
-
error: scriptError || undefined,
|
|
3199
|
-
duration: 0
|
|
3200
|
-
});
|
|
3201
|
-
}
|
|
3202
|
-
};
|
|
3203
|
-
`
|
|
3204
|
-
}
|
|
3205
|
-
|
|
3206
|
-
/**
|
|
3207
|
-
* Generate worker code for development (embedded test framework)
|
|
3208
|
-
*
|
|
3209
|
-
* This version bundles the test framework directly into the worker,
|
|
3210
|
-
* avoiding the need for RPC service bindings in local development.
|
|
3211
|
-
*/
|
|
3212
|
-
export function generateDevWorkerCode(options: {
|
|
3213
|
-
module?: string | undefined
|
|
3214
|
-
tests?: string | undefined
|
|
3215
|
-
script?: string | undefined
|
|
3216
|
-
sdk?: SDKConfig | boolean | undefined
|
|
3217
|
-
imports?: string[] | undefined
|
|
3218
|
-
fetch?: null | undefined
|
|
3219
|
-
}): string {
|
|
3220
|
-
const { module: rawModule = '', tests = '', script: rawScript = '', sdk, imports = [], fetch: fetchOption } = options
|
|
3221
|
-
const sdkConfig = sdk === true ? {} : (sdk || null)
|
|
3222
|
-
const module = rawModule ? transformModuleCode(rawModule) : ''
|
|
3223
|
-
const script = rawScript ? wrapScriptForReturn(rawScript) : ''
|
|
3224
|
-
const exportNames = getExportNames(rawModule)
|
|
3225
|
-
const blockFetch = fetchOption === null
|
|
3226
|
-
|
|
3227
|
-
// Hoisted imports (from MDX test files) - placed at true module top level
|
|
3228
|
-
const hoistedImports = imports.length > 0 ? imports.join('\n') + '\n' : ''
|
|
3229
|
-
|
|
3230
|
-
return `
|
|
3231
|
-
// Sandbox Worker Entry Point (Dev Mode - embedded test framework)
|
|
3232
|
-
${hoistedImports}
|
|
3233
|
-
const logs = [];
|
|
3234
|
-
const testResults = { total: 0, passed: 0, failed: 0, skipped: 0, tests: [], duration: 0 };
|
|
3235
|
-
const pendingTests = [];
|
|
3236
|
-
|
|
3237
|
-
${blockFetch ? `
|
|
3238
|
-
// Block fetch when fetch: null is specified
|
|
3239
|
-
const __originalFetch__ = globalThis.fetch;
|
|
3240
|
-
globalThis.fetch = async (...args) => {
|
|
3241
|
-
throw new Error('Network access blocked: fetch is disabled in this sandbox');
|
|
3242
|
-
};
|
|
3243
|
-
` : ''}
|
|
3244
|
-
|
|
3245
|
-
${sdkConfig ? generateShouldCode() : ''}
|
|
3246
|
-
|
|
3247
|
-
${sdkConfig ? generateSDKCode(sdkConfig) : '// SDK not enabled'}
|
|
3248
|
-
|
|
3249
|
-
// Capture console output
|
|
3250
|
-
const originalConsole = { ...console };
|
|
3251
|
-
const captureConsole = (level) => (...args) => {
|
|
3252
|
-
logs.push({
|
|
3253
|
-
level,
|
|
3254
|
-
message: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' '),
|
|
3255
|
-
timestamp: Date.now()
|
|
3256
|
-
});
|
|
3257
|
-
originalConsole[level](...args);
|
|
3258
|
-
};
|
|
3259
|
-
console.log = captureConsole('log');
|
|
3260
|
-
console.warn = captureConsole('warn');
|
|
3261
|
-
console.error = captureConsole('error');
|
|
3262
|
-
console.info = captureConsole('info');
|
|
3263
|
-
console.debug = captureConsole('debug');
|
|
3264
|
-
|
|
3265
|
-
// Test framework (vitest-compatible API)
|
|
3266
|
-
let currentDescribe = '';
|
|
3267
|
-
let beforeEachFns = [];
|
|
3268
|
-
let afterEachFns = [];
|
|
3269
|
-
|
|
3270
|
-
const describe = (name, fn) => {
|
|
3271
|
-
const prev = currentDescribe;
|
|
3272
|
-
const prevBeforeEach = [...beforeEachFns];
|
|
3273
|
-
const prevAfterEach = [...afterEachFns];
|
|
3274
|
-
currentDescribe = currentDescribe ? currentDescribe + ' > ' + name : name;
|
|
3275
|
-
try { fn(); } finally {
|
|
3276
|
-
currentDescribe = prev;
|
|
3277
|
-
beforeEachFns = prevBeforeEach;
|
|
3278
|
-
afterEachFns = prevAfterEach;
|
|
3279
|
-
}
|
|
3280
|
-
};
|
|
3281
|
-
|
|
3282
|
-
// Hooks
|
|
3283
|
-
const beforeEach = (fn) => { beforeEachFns.push(fn); };
|
|
3284
|
-
const afterEach = (fn) => { afterEachFns.push(fn); };
|
|
3285
|
-
|
|
3286
|
-
const it = (name, fn) => {
|
|
3287
|
-
const fullName = currentDescribe ? currentDescribe + ' > ' + name : name;
|
|
3288
|
-
const hooks = { before: [...beforeEachFns], after: [...afterEachFns] };
|
|
3289
|
-
pendingTests.push({ name: fullName, fn, hooks });
|
|
3290
|
-
};
|
|
3291
|
-
const test = it;
|
|
3292
|
-
|
|
3293
|
-
it.skip = (name, fn) => {
|
|
3294
|
-
const fullName = currentDescribe ? currentDescribe + ' > ' + name : name;
|
|
3295
|
-
pendingTests.push({ name: fullName, fn: null, skip: true });
|
|
3296
|
-
};
|
|
3297
|
-
test.skip = it.skip;
|
|
3298
|
-
|
|
3299
|
-
it.only = (name, fn) => {
|
|
3300
|
-
const fullName = currentDescribe ? currentDescribe + ' > ' + name : name;
|
|
3301
|
-
const hooks = { before: [...beforeEachFns], after: [...afterEachFns] };
|
|
3302
|
-
pendingTests.push({ name: fullName, fn, hooks, only: true });
|
|
3303
|
-
};
|
|
3304
|
-
test.only = it.only;
|
|
3305
|
-
|
|
3306
|
-
// Deep equality check
|
|
3307
|
-
const deepEqual = (a, b) => {
|
|
3308
|
-
if (a === b) return true;
|
|
3309
|
-
if (a == null || b == null) return false;
|
|
3310
|
-
if (typeof a !== typeof b) return false;
|
|
3311
|
-
if (typeof a !== 'object') return false;
|
|
3312
|
-
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
3313
|
-
if (Array.isArray(a)) {
|
|
3314
|
-
if (a.length !== b.length) return false;
|
|
3315
|
-
return a.every((v, i) => deepEqual(v, b[i]));
|
|
3316
|
-
}
|
|
3317
|
-
const keysA = Object.keys(a);
|
|
3318
|
-
const keysB = Object.keys(b);
|
|
3319
|
-
if (keysA.length !== keysB.length) return false;
|
|
3320
|
-
return keysA.every(k => deepEqual(a[k], b[k]));
|
|
3321
|
-
};
|
|
3322
|
-
|
|
3323
|
-
// Expect implementation with vitest-compatible matchers
|
|
3324
|
-
const expect = (actual) => {
|
|
3325
|
-
const matchers = {
|
|
3326
|
-
toBe: (expected) => {
|
|
3327
|
-
if (actual !== expected) {
|
|
3328
|
-
throw new Error(\`Expected \${JSON.stringify(expected)} but got \${JSON.stringify(actual)}\`);
|
|
3329
|
-
}
|
|
3330
|
-
},
|
|
3331
|
-
toEqual: (expected) => {
|
|
3332
|
-
if (!deepEqual(actual, expected)) {
|
|
3333
|
-
throw new Error(\`Expected \${JSON.stringify(expected)} but got \${JSON.stringify(actual)}\`);
|
|
3334
|
-
}
|
|
3335
|
-
},
|
|
3336
|
-
toStrictEqual: (expected) => {
|
|
3337
|
-
if (!deepEqual(actual, expected)) {
|
|
3338
|
-
throw new Error(\`Expected \${JSON.stringify(expected)} but got \${JSON.stringify(actual)}\`);
|
|
3339
|
-
}
|
|
3340
|
-
},
|
|
3341
|
-
toBeTruthy: () => {
|
|
3342
|
-
if (!actual) throw new Error(\`Expected truthy but got \${JSON.stringify(actual)}\`);
|
|
3343
|
-
},
|
|
3344
|
-
toBeFalsy: () => {
|
|
3345
|
-
if (actual) throw new Error(\`Expected falsy but got \${JSON.stringify(actual)}\`);
|
|
3346
|
-
},
|
|
3347
|
-
toBeNull: () => {
|
|
3348
|
-
if (actual !== null) throw new Error(\`Expected null but got \${JSON.stringify(actual)}\`);
|
|
3349
|
-
},
|
|
3350
|
-
toBeUndefined: () => {
|
|
3351
|
-
if (actual !== undefined) throw new Error(\`Expected undefined but got \${JSON.stringify(actual)}\`);
|
|
3352
|
-
},
|
|
3353
|
-
toBeDefined: () => {
|
|
3354
|
-
if (actual === undefined) throw new Error('Expected defined but got undefined');
|
|
3355
|
-
},
|
|
3356
|
-
toBeNaN: () => {
|
|
3357
|
-
if (!Number.isNaN(actual)) throw new Error(\`Expected NaN but got \${actual}\`);
|
|
3358
|
-
},
|
|
3359
|
-
toContain: (item) => {
|
|
3360
|
-
if (Array.isArray(actual)) {
|
|
3361
|
-
if (!actual.includes(item)) throw new Error(\`Expected array to contain \${JSON.stringify(item)}\`);
|
|
3362
|
-
} else if (typeof actual === 'string') {
|
|
3363
|
-
if (!actual.includes(item)) throw new Error(\`Expected string to contain "\${item}"\`);
|
|
3364
|
-
} else {
|
|
3365
|
-
throw new Error('toContain only works on arrays and strings');
|
|
3366
|
-
}
|
|
3367
|
-
},
|
|
3368
|
-
toContainEqual: (item) => {
|
|
3369
|
-
if (!Array.isArray(actual)) throw new Error('toContainEqual only works on arrays');
|
|
3370
|
-
if (!actual.some(v => deepEqual(v, item))) {
|
|
3371
|
-
throw new Error(\`Expected array to contain \${JSON.stringify(item)}\`);
|
|
3372
|
-
}
|
|
3373
|
-
},
|
|
3374
|
-
toHaveLength: (length) => {
|
|
3375
|
-
if (actual?.length !== length) {
|
|
3376
|
-
throw new Error(\`Expected length \${length} but got \${actual?.length}\`);
|
|
3377
|
-
}
|
|
3378
|
-
},
|
|
3379
|
-
toHaveProperty: function(path, value) {
|
|
3380
|
-
const parts = typeof path === 'string' ? path.split('.') : [path];
|
|
3381
|
-
let obj = actual;
|
|
3382
|
-
for (const part of parts) {
|
|
3383
|
-
if (obj == null || !(part in obj)) {
|
|
3384
|
-
throw new Error(\`Expected object to have property "\${path}"\`);
|
|
3385
|
-
}
|
|
3386
|
-
obj = obj[part];
|
|
3387
|
-
}
|
|
3388
|
-
if (arguments.length > 1 && !deepEqual(obj, value)) {
|
|
3389
|
-
throw new Error(\`Expected property "\${path}" to be \${JSON.stringify(value)} but got \${JSON.stringify(obj)}\`);
|
|
3390
|
-
}
|
|
3391
|
-
},
|
|
3392
|
-
toMatchObject: (expected) => {
|
|
3393
|
-
if (typeof actual !== 'object' || actual === null) {
|
|
3394
|
-
throw new Error('toMatchObject expects an object');
|
|
3395
|
-
}
|
|
3396
|
-
for (const key of Object.keys(expected)) {
|
|
3397
|
-
if (!deepEqual(actual[key], expected[key])) {
|
|
3398
|
-
throw new Error(\`Expected property "\${key}" to be \${JSON.stringify(expected[key])} but got \${JSON.stringify(actual[key])}\`);
|
|
3399
|
-
}
|
|
3400
|
-
}
|
|
3401
|
-
},
|
|
3402
|
-
toThrow: (expected) => {
|
|
3403
|
-
if (typeof actual !== 'function') throw new Error('toThrow expects a function');
|
|
3404
|
-
let threw = false;
|
|
3405
|
-
let error;
|
|
3406
|
-
try {
|
|
3407
|
-
actual();
|
|
3408
|
-
} catch (e) {
|
|
3409
|
-
threw = true;
|
|
3410
|
-
error = e;
|
|
3411
|
-
}
|
|
3412
|
-
if (!threw) throw new Error('Expected function to throw');
|
|
3413
|
-
if (expected !== undefined) {
|
|
3414
|
-
if (typeof expected === 'string' && !error.message.includes(expected)) {
|
|
3415
|
-
throw new Error(\`Expected error message to contain "\${expected}" but got "\${error.message}"\`);
|
|
3416
|
-
}
|
|
3417
|
-
if (expected instanceof RegExp && !expected.test(error.message)) {
|
|
3418
|
-
throw new Error(\`Expected error message to match \${expected} but got "\${error.message}"\`);
|
|
3419
|
-
}
|
|
3420
|
-
if (typeof expected === 'function' && !(error instanceof expected)) {
|
|
3421
|
-
throw new Error(\`Expected error to be instance of \${expected.name}\`);
|
|
3422
|
-
}
|
|
3423
|
-
}
|
|
3424
|
-
},
|
|
3425
|
-
toBeGreaterThan: (n) => {
|
|
3426
|
-
if (!(actual > n)) throw new Error(\`Expected \${actual} to be greater than \${n}\`);
|
|
3427
|
-
},
|
|
3428
|
-
toBeLessThan: (n) => {
|
|
3429
|
-
if (!(actual < n)) throw new Error(\`Expected \${actual} to be less than \${n}\`);
|
|
3430
|
-
},
|
|
3431
|
-
toBeGreaterThanOrEqual: (n) => {
|
|
3432
|
-
if (!(actual >= n)) throw new Error(\`Expected \${actual} to be >= \${n}\`);
|
|
3433
|
-
},
|
|
3434
|
-
toBeLessThanOrEqual: (n) => {
|
|
3435
|
-
if (!(actual <= n)) throw new Error(\`Expected \${actual} to be <= \${n}\`);
|
|
3436
|
-
},
|
|
3437
|
-
toBeCloseTo: (n, digits = 2) => {
|
|
3438
|
-
const diff = Math.abs(actual - n);
|
|
3439
|
-
const threshold = Math.pow(10, -digits) / 2;
|
|
3440
|
-
if (diff > threshold) {
|
|
3441
|
-
throw new Error(\`Expected \${actual} to be close to \${n}\`);
|
|
3442
|
-
}
|
|
3443
|
-
},
|
|
3444
|
-
toMatch: (pattern) => {
|
|
3445
|
-
if (typeof actual !== 'string') throw new Error('toMatch expects a string');
|
|
3446
|
-
const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
|
|
3447
|
-
if (!regex.test(actual)) {
|
|
3448
|
-
throw new Error(\`Expected "\${actual}" to match \${pattern}\`);
|
|
3449
|
-
}
|
|
3450
|
-
},
|
|
3451
|
-
toBeInstanceOf: (cls) => {
|
|
3452
|
-
if (!(actual instanceof cls)) {
|
|
3453
|
-
throw new Error(\`Expected instance of \${cls.name}\`);
|
|
3454
|
-
}
|
|
3455
|
-
},
|
|
3456
|
-
toBeTypeOf: (type) => {
|
|
3457
|
-
if (typeof actual !== type) {
|
|
3458
|
-
throw new Error(\`Expected typeof to be "\${type}" but got "\${typeof actual}"\`);
|
|
3459
|
-
}
|
|
3460
|
-
},
|
|
3461
|
-
};
|
|
3462
|
-
|
|
3463
|
-
matchers.not = {
|
|
3464
|
-
toBe: (expected) => {
|
|
3465
|
-
if (actual === expected) throw new Error(\`Expected not \${JSON.stringify(expected)}\`);
|
|
3466
|
-
},
|
|
3467
|
-
toEqual: (expected) => {
|
|
3468
|
-
if (deepEqual(actual, expected)) {
|
|
3469
|
-
throw new Error(\`Expected not equal to \${JSON.stringify(expected)}\`);
|
|
3470
|
-
}
|
|
3471
|
-
},
|
|
3472
|
-
toBeTruthy: () => {
|
|
3473
|
-
if (actual) throw new Error('Expected not truthy');
|
|
3474
|
-
},
|
|
3475
|
-
toBeFalsy: () => {
|
|
3476
|
-
if (!actual) throw new Error('Expected not falsy');
|
|
3477
|
-
},
|
|
3478
|
-
toBeNull: () => {
|
|
3479
|
-
if (actual === null) throw new Error('Expected not null');
|
|
3480
|
-
},
|
|
3481
|
-
toBeUndefined: () => {
|
|
3482
|
-
if (actual === undefined) throw new Error('Expected not undefined');
|
|
3483
|
-
},
|
|
3484
|
-
toBeDefined: () => {
|
|
3485
|
-
if (actual !== undefined) throw new Error('Expected undefined');
|
|
3486
|
-
},
|
|
3487
|
-
toContain: (item) => {
|
|
3488
|
-
if (Array.isArray(actual) && actual.includes(item)) {
|
|
3489
|
-
throw new Error(\`Expected array not to contain \${JSON.stringify(item)}\`);
|
|
3490
|
-
}
|
|
3491
|
-
if (typeof actual === 'string' && actual.includes(item)) {
|
|
3492
|
-
throw new Error(\`Expected string not to contain "\${item}"\`);
|
|
3493
|
-
}
|
|
3494
|
-
},
|
|
3495
|
-
toHaveProperty: (path) => {
|
|
3496
|
-
const parts = typeof path === 'string' ? path.split('.') : [path];
|
|
3497
|
-
let obj = actual;
|
|
3498
|
-
try {
|
|
3499
|
-
for (const part of parts) {
|
|
3500
|
-
if (obj == null || !(part in obj)) return;
|
|
3501
|
-
obj = obj[part];
|
|
3502
|
-
}
|
|
3503
|
-
throw new Error(\`Expected object not to have property "\${path}"\`);
|
|
3504
|
-
} catch {}
|
|
3505
|
-
},
|
|
3506
|
-
toThrow: () => {
|
|
3507
|
-
if (typeof actual !== 'function') throw new Error('toThrow expects a function');
|
|
3508
|
-
try {
|
|
3509
|
-
actual();
|
|
3510
|
-
} catch (e) {
|
|
3511
|
-
throw new Error('Expected function not to throw');
|
|
3512
|
-
}
|
|
3513
|
-
},
|
|
3514
|
-
toMatch: (pattern) => {
|
|
3515
|
-
if (typeof actual !== 'string') throw new Error('toMatch expects a string');
|
|
3516
|
-
const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
|
|
3517
|
-
if (regex.test(actual)) {
|
|
3518
|
-
throw new Error(\`Expected "\${actual}" not to match \${pattern}\`);
|
|
3519
|
-
}
|
|
3520
|
-
},
|
|
3521
|
-
};
|
|
3522
|
-
|
|
3523
|
-
matchers.resolves = new Proxy({}, {
|
|
3524
|
-
get: (_, prop) => async (...args) => {
|
|
3525
|
-
const resolved = await actual;
|
|
3526
|
-
return expect(resolved)[prop](...args);
|
|
3527
|
-
}
|
|
3528
|
-
});
|
|
3529
|
-
|
|
3530
|
-
matchers.rejects = new Proxy({}, {
|
|
3531
|
-
get: (_, prop) => async (...args) => {
|
|
3532
|
-
try {
|
|
3533
|
-
await actual;
|
|
3534
|
-
throw new Error('Expected promise to reject');
|
|
3535
|
-
} catch (e) {
|
|
3536
|
-
if (e.message === 'Expected promise to reject') throw e;
|
|
3537
|
-
return expect(e)[prop](...args);
|
|
3538
|
-
}
|
|
3539
|
-
}
|
|
3540
|
-
});
|
|
3541
|
-
|
|
3542
|
-
return matchers;
|
|
3543
|
-
};
|
|
3544
|
-
|
|
3545
|
-
// ============================================================
|
|
3546
|
-
// USER MODULE CODE (embedded at generation time)
|
|
3547
|
-
// ============================================================
|
|
3548
|
-
// Module exports object - exports become top-level variables
|
|
3549
|
-
const exports = {};
|
|
3550
|
-
|
|
3551
|
-
${module ? `
|
|
3552
|
-
// Execute module code
|
|
3553
|
-
try {
|
|
3554
|
-
${module}
|
|
3555
|
-
} catch (e) {
|
|
3556
|
-
console.error('Module error:', e.message);
|
|
3557
|
-
}
|
|
3558
|
-
` : '// No module code provided'}
|
|
3559
|
-
|
|
3560
|
-
// Expose all exports as top-level variables for tests and scripts
|
|
3561
|
-
// This allows: export const add = (a, b) => a + b; then later: add(1, 2)
|
|
3562
|
-
${rawModule ? `
|
|
3563
|
-
const { ${exportNames} } = exports;
|
|
3564
|
-
`.trim() : ''}
|
|
3565
|
-
|
|
3566
|
-
// ============================================================
|
|
3567
|
-
// USER TEST CODE (embedded at generation time)
|
|
3568
|
-
// ============================================================
|
|
3569
|
-
${tests ? `
|
|
3570
|
-
// Register tests
|
|
3571
|
-
try {
|
|
3572
|
-
${tests}
|
|
3573
|
-
} catch (e) {
|
|
3574
|
-
console.error('Test registration error:', e.message);
|
|
3575
|
-
}
|
|
3576
|
-
` : '// No test code provided'}
|
|
3577
|
-
|
|
3578
|
-
// ============================================================
|
|
3579
|
-
// SIMPLE RPC HANDLER (dev mode - no capnweb dependency)
|
|
3580
|
-
// ============================================================
|
|
3581
|
-
async function handleRpc(request) {
|
|
3582
|
-
try {
|
|
3583
|
-
const { method, args = [] } = await request.json();
|
|
3584
|
-
if (method === 'list') {
|
|
3585
|
-
return Response.json({ result: Object.keys(exports) });
|
|
3586
|
-
}
|
|
3587
|
-
if (method === 'get') {
|
|
3588
|
-
const [name] = args;
|
|
3589
|
-
const value = exports[name];
|
|
3590
|
-
if (typeof value === 'function') {
|
|
3591
|
-
return Response.json({ result: { type: 'function', name } });
|
|
3592
|
-
}
|
|
3593
|
-
return Response.json({ result: value });
|
|
3594
|
-
}
|
|
3595
|
-
// Call an exported function
|
|
3596
|
-
const fn = exports[method];
|
|
3597
|
-
if (typeof fn !== 'function') {
|
|
3598
|
-
return Response.json({ error: \`Export "\${method}" is not a function\` }, { status: 400 });
|
|
3599
|
-
}
|
|
3600
|
-
const result = await fn(...args);
|
|
3601
|
-
return Response.json({ result });
|
|
3602
|
-
} catch (e) {
|
|
3603
|
-
return Response.json({ error: e.message }, { status: 500 });
|
|
3604
|
-
}
|
|
3605
|
-
}
|
|
3606
|
-
|
|
3607
|
-
// ============================================================
|
|
3608
|
-
// WORKER ENTRY POINT
|
|
3609
|
-
// ============================================================
|
|
3610
|
-
export default {
|
|
3611
|
-
async fetch(request, env) {
|
|
3612
|
-
const url = new URL(request.url);
|
|
3613
|
-
|
|
3614
|
-
// Route: GET / - Return info about exports
|
|
3615
|
-
if (request.method === 'GET' && url.pathname === '/') {
|
|
3616
|
-
return Response.json({
|
|
3617
|
-
exports: Object.keys(exports),
|
|
3618
|
-
rpc: '/rpc',
|
|
3619
|
-
execute: '/execute'
|
|
3620
|
-
});
|
|
3621
|
-
}
|
|
3622
|
-
|
|
3623
|
-
// Route: POST /rpc - Simple RPC to module exports
|
|
3624
|
-
if (url.pathname === '/rpc' && request.method === 'POST') {
|
|
3625
|
-
return handleRpc(request);
|
|
3626
|
-
}
|
|
3627
|
-
|
|
3628
|
-
// Route: GET /:name - Simple JSON endpoint to access exports
|
|
3629
|
-
if (request.method === 'GET' && url.pathname !== '/execute') {
|
|
3630
|
-
const name = url.pathname.slice(1);
|
|
3631
|
-
const value = exports[name];
|
|
3632
|
-
|
|
3633
|
-
// Check if export exists
|
|
3634
|
-
if (!(name in exports)) {
|
|
3635
|
-
return Response.json({ error: \`Export "\${name}" not found\` }, { status: 404 });
|
|
3636
|
-
}
|
|
3637
|
-
|
|
3638
|
-
// If it's not a function, just return the value
|
|
3639
|
-
if (typeof value !== 'function') {
|
|
3640
|
-
return Response.json({ result: value });
|
|
3641
|
-
}
|
|
3642
|
-
|
|
3643
|
-
// It's a function - parse args and call it
|
|
3644
|
-
try {
|
|
3645
|
-
const args = [];
|
|
3646
|
-
const argsParam = url.searchParams.get('args');
|
|
3647
|
-
if (argsParam) {
|
|
3648
|
-
try {
|
|
3649
|
-
const parsed = JSON.parse(argsParam);
|
|
3650
|
-
if (Array.isArray(parsed)) args.push(...parsed);
|
|
3651
|
-
else args.push(parsed);
|
|
3652
|
-
} catch {
|
|
3653
|
-
args.push(argsParam);
|
|
3654
|
-
}
|
|
3655
|
-
} else {
|
|
3656
|
-
const params = Object.fromEntries(url.searchParams.entries());
|
|
3657
|
-
if (Object.keys(params).length > 0) {
|
|
3658
|
-
for (const [key, val] of Object.entries(params)) {
|
|
3659
|
-
const num = Number(val);
|
|
3660
|
-
params[key] = !isNaN(num) && val !== '' ? num : val;
|
|
3661
|
-
}
|
|
3662
|
-
args.push(params);
|
|
3663
|
-
}
|
|
3664
|
-
}
|
|
3665
|
-
const result = await value(...args);
|
|
3666
|
-
return Response.json({ result });
|
|
3667
|
-
} catch (e) {
|
|
3668
|
-
return Response.json({ error: e.message }, { status: 500 });
|
|
3669
|
-
}
|
|
3670
|
-
}
|
|
3671
|
-
|
|
3672
|
-
// Route: /execute - Run tests and scripts
|
|
3673
|
-
let scriptResult = undefined;
|
|
3674
|
-
let scriptError = null;
|
|
3675
|
-
|
|
3676
|
-
// Execute user script
|
|
3677
|
-
${script ? `
|
|
3678
|
-
try {
|
|
3679
|
-
scriptResult = await (async () => {
|
|
3680
|
-
${script}
|
|
3681
|
-
})();
|
|
3682
|
-
} catch (e) {
|
|
3683
|
-
console.error('Script error:', e.message);
|
|
3684
|
-
scriptError = e.message;
|
|
3685
|
-
}
|
|
3686
|
-
` : '// No script code provided'}
|
|
3687
|
-
|
|
3688
|
-
// Run all pending tests
|
|
3689
|
-
const testStart = Date.now();
|
|
3690
|
-
const hasOnly = pendingTests.some(t => t.only);
|
|
3691
|
-
const testsToRun = hasOnly ? pendingTests.filter(t => t.only || t.skip) : pendingTests;
|
|
3692
|
-
|
|
3693
|
-
for (const { name, fn, hooks, skip } of testsToRun) {
|
|
3694
|
-
testResults.total++;
|
|
3695
|
-
|
|
3696
|
-
if (skip) {
|
|
3697
|
-
testResults.skipped++;
|
|
3698
|
-
testResults.tests.push({ name, passed: true, skipped: true, duration: 0 });
|
|
3699
|
-
continue;
|
|
3700
|
-
}
|
|
3701
|
-
|
|
3702
|
-
const start = Date.now();
|
|
3703
|
-
try {
|
|
3704
|
-
// Run beforeEach hooks
|
|
3705
|
-
if (hooks?.before) {
|
|
3706
|
-
for (const hook of hooks.before) {
|
|
3707
|
-
const hookResult = hook();
|
|
3708
|
-
if (hookResult && typeof hookResult.then === 'function') {
|
|
3709
|
-
await hookResult;
|
|
3710
|
-
}
|
|
3711
|
-
}
|
|
3712
|
-
}
|
|
3713
|
-
|
|
3714
|
-
// Run the test
|
|
3715
|
-
const result = fn();
|
|
3716
|
-
if (result && typeof result.then === 'function') {
|
|
3717
|
-
await result;
|
|
3718
|
-
}
|
|
3719
|
-
|
|
3720
|
-
// Run afterEach hooks
|
|
3721
|
-
if (hooks?.after) {
|
|
3722
|
-
for (const hook of hooks.after) {
|
|
3723
|
-
const hookResult = hook();
|
|
3724
|
-
if (hookResult && typeof hookResult.then === 'function') {
|
|
3725
|
-
await hookResult;
|
|
3726
|
-
}
|
|
3727
|
-
}
|
|
3728
|
-
}
|
|
3729
|
-
|
|
3730
|
-
testResults.passed++;
|
|
3731
|
-
testResults.tests.push({ name, passed: true, duration: Date.now() - start });
|
|
3732
|
-
} catch (e) {
|
|
3733
|
-
testResults.failed++;
|
|
3734
|
-
testResults.tests.push({
|
|
3735
|
-
name,
|
|
3736
|
-
passed: false,
|
|
3737
|
-
error: e.message || String(e),
|
|
3738
|
-
duration: Date.now() - start
|
|
3739
|
-
});
|
|
3740
|
-
}
|
|
3741
|
-
}
|
|
3742
|
-
|
|
3743
|
-
testResults.duration = Date.now() - testStart;
|
|
3744
|
-
|
|
3745
|
-
const hasTests = ${tests ? 'true' : 'false'};
|
|
3746
|
-
const success = scriptError === null && (!hasTests || testResults.failed === 0);
|
|
3747
|
-
|
|
3748
|
-
return Response.json({
|
|
3749
|
-
success,
|
|
3750
|
-
value: scriptResult,
|
|
3751
|
-
logs,
|
|
3752
|
-
testResults: hasTests ? testResults : undefined,
|
|
3753
|
-
error: scriptError || undefined,
|
|
3754
|
-
duration: 0
|
|
3755
|
-
});
|
|
3756
|
-
}
|
|
3757
|
-
};
|
|
3758
|
-
`
|
|
3759
|
-
}
|