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.
Files changed (100) hide show
  1. package/README.md +90 -3
  2. package/dist/capnweb-bundle.d.ts +10 -0
  3. package/dist/capnweb-bundle.d.ts.map +1 -0
  4. package/dist/capnweb-bundle.js +2596 -0
  5. package/dist/capnweb-bundle.js.map +1 -0
  6. package/dist/evaluate.d.ts +1 -1
  7. package/dist/evaluate.d.ts.map +1 -1
  8. package/dist/evaluate.js +186 -7
  9. package/dist/evaluate.js.map +1 -1
  10. package/dist/index.d.ts +2 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/miniflare-pool.d.ts +109 -0
  15. package/dist/miniflare-pool.d.ts.map +1 -0
  16. package/dist/miniflare-pool.js +308 -0
  17. package/dist/miniflare-pool.js.map +1 -0
  18. package/dist/node.d.ts.map +1 -1
  19. package/dist/node.js +42 -10
  20. package/dist/node.js.map +1 -1
  21. package/dist/shared.d.ts +66 -0
  22. package/dist/shared.d.ts.map +1 -0
  23. package/dist/shared.js +169 -0
  24. package/dist/shared.js.map +1 -0
  25. package/dist/type-guards.d.ts +21 -0
  26. package/dist/type-guards.d.ts.map +1 -0
  27. package/dist/type-guards.js +216 -0
  28. package/dist/type-guards.js.map +1 -0
  29. package/dist/types.d.ts +17 -2
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/validation.d.ts +26 -0
  32. package/dist/validation.d.ts.map +1 -0
  33. package/dist/validation.js +104 -0
  34. package/dist/validation.js.map +1 -0
  35. package/dist/worker-template/code-transforms.d.ts +9 -0
  36. package/dist/worker-template/code-transforms.d.ts.map +1 -0
  37. package/dist/worker-template/code-transforms.js +28 -0
  38. package/dist/worker-template/code-transforms.js.map +1 -0
  39. package/{src/worker-template.d.ts → dist/worker-template/core.d.ts} +7 -15
  40. package/dist/worker-template/core.d.ts.map +1 -0
  41. package/dist/worker-template/core.js +502 -0
  42. package/dist/worker-template/core.js.map +1 -0
  43. package/dist/worker-template/helpers.d.ts +14 -0
  44. package/dist/worker-template/helpers.d.ts.map +1 -0
  45. package/dist/worker-template/helpers.js +79 -0
  46. package/dist/worker-template/helpers.js.map +1 -0
  47. package/dist/worker-template/index.d.ts +14 -0
  48. package/dist/worker-template/index.d.ts.map +1 -0
  49. package/dist/worker-template/index.js +19 -0
  50. package/dist/worker-template/index.js.map +1 -0
  51. package/dist/worker-template/sdk-generator.d.ts +17 -0
  52. package/dist/worker-template/sdk-generator.d.ts.map +1 -0
  53. package/{src/worker-template.js → dist/worker-template/sdk-generator.js} +377 -1506
  54. package/dist/worker-template/sdk-generator.js.map +1 -0
  55. package/dist/worker-template/test-generator.d.ts +16 -0
  56. package/dist/worker-template/test-generator.d.ts.map +1 -0
  57. package/dist/worker-template/test-generator.js +357 -0
  58. package/dist/worker-template/test-generator.js.map +1 -0
  59. package/dist/worker-template.d.ts +2 -2
  60. package/dist/worker-template.d.ts.map +1 -1
  61. package/dist/worker-template.js +64 -31
  62. package/dist/worker-template.js.map +1 -1
  63. package/example/package.json +7 -3
  64. package/example/src/index.ts +194 -40
  65. package/example/wrangler.jsonc +18 -2
  66. package/package.json +1 -3
  67. package/src/capnweb-bundle.ts +2596 -0
  68. package/src/evaluate.ts +216 -7
  69. package/src/index.ts +3 -1
  70. package/src/miniflare-pool.ts +395 -0
  71. package/src/node.ts +56 -11
  72. package/src/shared.ts +186 -0
  73. package/src/type-guards.ts +323 -0
  74. package/src/types.ts +18 -2
  75. package/src/validation.ts +120 -0
  76. package/src/worker-template/code-transforms.ts +32 -0
  77. package/src/worker-template/core.ts +557 -0
  78. package/src/worker-template/helpers.ts +90 -0
  79. package/src/worker-template/index.ts +23 -0
  80. package/src/{worker-template.ts → worker-template/sdk-generator.ts} +322 -1566
  81. package/src/worker-template/test-generator.ts +358 -0
  82. package/test/miniflare-pool.test.ts +246 -0
  83. package/test/node.test.ts +467 -0
  84. package/test/security.test.ts +1009 -0
  85. package/test/shared.test.ts +105 -0
  86. package/test/type-guards.test.ts +303 -0
  87. package/test/validation.test.ts +240 -0
  88. package/test/worker-template.test.ts +21 -19
  89. package/src/evaluate.js +0 -187
  90. package/src/index.js +0 -10
  91. package/src/node.d.ts +0 -17
  92. package/src/node.d.ts.map +0 -1
  93. package/src/node.js +0 -168
  94. package/src/node.js.map +0 -1
  95. package/src/types.d.ts +0 -172
  96. package/src/types.d.ts.map +0 -1
  97. package/src/types.js +0 -4
  98. package/src/types.js.map +0 -1
  99. package/src/worker-template.d.ts.map +0 -1
  100. package/src/worker-template.js.map +0 -1
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Input validation for EvaluateOptions
3
+ *
4
+ * Validates options to prevent resource exhaustion and provide clear error messages.
5
+ */
6
+
7
+ import type { EvaluateOptions } from './types.js'
8
+
9
+ /**
10
+ * Validation limits for EvaluateOptions
11
+ */
12
+ export const MAX_SCRIPT_SIZE = 1024 * 1024 // 1MB
13
+ export const MAX_IMPORTS = 100
14
+ export const MAX_TIMEOUT = 60000 // 60 seconds
15
+ export const DEFAULT_TIMEOUT = 5000 // 5 seconds
16
+
17
+ /**
18
+ * Validation error thrown when options fail validation
19
+ */
20
+ export class ValidationError extends Error {
21
+ constructor(message: string) {
22
+ super(message)
23
+ this.name = 'ValidationError'
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Validate a URL string
29
+ */
30
+ function isValidUrl(urlString: string): boolean {
31
+ try {
32
+ const url = new URL(urlString)
33
+ return url.protocol === 'http:' || url.protocol === 'https:'
34
+ } catch {
35
+ return false
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Validate EvaluateOptions
41
+ *
42
+ * @throws ValidationError if any validation fails
43
+ */
44
+ export function validateOptions(options: EvaluateOptions): void {
45
+ // Validate timeout
46
+ if (options.timeout !== undefined) {
47
+ if (typeof options.timeout !== 'number') {
48
+ throw new ValidationError('timeout must be a number')
49
+ }
50
+ if (!Number.isFinite(options.timeout)) {
51
+ throw new ValidationError('timeout must be a finite number')
52
+ }
53
+ if (options.timeout <= 0) {
54
+ throw new ValidationError('timeout must be a positive number')
55
+ }
56
+ if (options.timeout > MAX_TIMEOUT) {
57
+ throw new ValidationError(`timeout exceeds maximum allowed value of ${MAX_TIMEOUT}ms`)
58
+ }
59
+ }
60
+
61
+ // Validate script length
62
+ if (options.script !== undefined && options.script !== null) {
63
+ if (typeof options.script !== 'string') {
64
+ throw new ValidationError('script must be a string')
65
+ }
66
+ const scriptBytes = new TextEncoder().encode(options.script).length
67
+ if (scriptBytes > MAX_SCRIPT_SIZE) {
68
+ throw new ValidationError(
69
+ `script size (${scriptBytes} bytes) exceeds maximum allowed size of ${MAX_SCRIPT_SIZE} bytes (1MB)`
70
+ )
71
+ }
72
+ }
73
+
74
+ // Validate module length
75
+ if (options.module !== undefined && options.module !== null) {
76
+ if (typeof options.module !== 'string') {
77
+ throw new ValidationError('module must be a string')
78
+ }
79
+ const moduleBytes = new TextEncoder().encode(options.module).length
80
+ if (moduleBytes > MAX_SCRIPT_SIZE) {
81
+ throw new ValidationError(
82
+ `module size (${moduleBytes} bytes) exceeds maximum allowed size of ${MAX_SCRIPT_SIZE} bytes (1MB)`
83
+ )
84
+ }
85
+ }
86
+
87
+ // Validate tests length
88
+ if (options.tests !== undefined && options.tests !== null) {
89
+ if (typeof options.tests !== 'string') {
90
+ throw new ValidationError('tests must be a string')
91
+ }
92
+ const testsBytes = new TextEncoder().encode(options.tests).length
93
+ if (testsBytes > MAX_SCRIPT_SIZE) {
94
+ throw new ValidationError(
95
+ `tests size (${testsBytes} bytes) exceeds maximum allowed size of ${MAX_SCRIPT_SIZE} bytes (1MB)`
96
+ )
97
+ }
98
+ }
99
+
100
+ // Validate imports
101
+ if (options.imports !== undefined && options.imports !== null) {
102
+ if (!Array.isArray(options.imports)) {
103
+ throw new ValidationError('imports must be an array')
104
+ }
105
+ if (options.imports.length > MAX_IMPORTS) {
106
+ throw new ValidationError(
107
+ `imports count (${options.imports.length}) exceeds maximum allowed count of ${MAX_IMPORTS}`
108
+ )
109
+ }
110
+ for (let i = 0; i < options.imports.length; i++) {
111
+ const importUrl = options.imports[i]
112
+ if (typeof importUrl !== 'string') {
113
+ throw new ValidationError(`imports[${i}] must be a string`)
114
+ }
115
+ if (!isValidUrl(importUrl)) {
116
+ throw new ValidationError(`imports[${i}] is not a valid URL: ${importUrl}`)
117
+ }
118
+ }
119
+ }
120
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Module transformation and export detection utilities
3
+ */
4
+
5
+ /**
6
+ * Transform module code to work in sandbox
7
+ * Converts ES module exports to CommonJS-style for the sandbox
8
+ */
9
+ export function transformModuleCode(moduleCode: string): string {
10
+ let code = moduleCode
11
+
12
+ // Transform: export const foo = ... -> const foo = ...; exports.foo = foo;
13
+ code = code.replace(/export\s+(const|let|var)\s+(\w+)\s*=/g, '$1 $2 = exports.$2 =')
14
+
15
+ // Transform: export function foo(...) -> function foo(...) exports.foo = foo;
16
+ // Also handles async generators: export async function* foo
17
+ code = code.replace(/export\s+(async\s+)?function(\*?)\s+(\w+)/g, '$1function$2 $3')
18
+ // Add exports for functions after their definition
19
+ const funcNames = [...moduleCode.matchAll(/export\s+(?:async\s+)?function\*?\s+(\w+)/g)]
20
+ for (const [, name] of funcNames) {
21
+ code += `\nexports.${name} = ${name};`
22
+ }
23
+
24
+ // Transform: export class Foo -> class Foo; exports.Foo = Foo;
25
+ code = code.replace(/export\s+class\s+(\w+)/g, 'class $1')
26
+ const classNames = [...moduleCode.matchAll(/export\s+class\s+(\w+)/g)]
27
+ for (const [, name] of classNames) {
28
+ code += `\nexports.${name} = ${name};`
29
+ }
30
+
31
+ return code
32
+ }
@@ -0,0 +1,557 @@
1
+ /**
2
+ * Worker scaffold and main template generation
3
+ *
4
+ * This module contains the main generateWorkerCode and generateDevWorkerCode functions
5
+ * that produce the complete worker code for sandbox execution.
6
+ */
7
+
8
+ import type { SDKConfig, FetchConfig } from '../types.js'
9
+ import { getExportNames, wrapScriptForReturn } from './helpers.js'
10
+ import { transformModuleCode } from './code-transforms.js'
11
+ import { generateSDKCode, generateShouldCode } from './sdk-generator.js'
12
+ import { generateTestFrameworkCode, generateTestRunnerCode } from './test-generator.js'
13
+ import { generateDomainCheckCode } from '../shared.js'
14
+
15
+ /**
16
+ * Generate worker code for production (uses RPC to ai-tests)
17
+ */
18
+ export function generateWorkerCode(options: {
19
+ module?: string | undefined
20
+ tests?: string | undefined
21
+ script?: string | undefined
22
+ sdk?: SDKConfig | boolean | undefined
23
+ imports?: string[] | undefined
24
+ fetch?: FetchConfig
25
+ }): string {
26
+ const {
27
+ module: rawModule = '',
28
+ tests = '',
29
+ script: rawScript = '',
30
+ sdk,
31
+ imports = [],
32
+ fetch: fetchOption,
33
+ } = options
34
+ const sdkConfig = sdk === true ? {} : sdk || null
35
+ const module = rawModule ? transformModuleCode(rawModule) : ''
36
+ const script = rawScript ? wrapScriptForReturn(rawScript) : ''
37
+ const exportNames = getExportNames(rawModule)
38
+
39
+ // Hoisted imports (from MDX test files) - placed at true module top level
40
+ const hoistedImports = imports.length > 0 ? imports.join('\n') + '\n' : ''
41
+
42
+ // Generate fetch control code for allowlist (block is handled by globalOutbound)
43
+ const allowlistDomains = Array.isArray(fetchOption) ? fetchOption : null
44
+ const fetchControlCode = allowlistDomains ? generateDomainCheckCode(allowlistDomains) : ''
45
+
46
+ return `
47
+ // Sandbox Worker Entry Point
48
+ import { RpcTarget, newWorkersRpcResponse } from 'capnweb.js';
49
+ ${hoistedImports}
50
+ const logs = [];
51
+
52
+ ${fetchControlCode}
53
+
54
+ ${sdkConfig ? generateShouldCode() : ''}
55
+
56
+ ${sdkConfig ? generateSDKCode(sdkConfig) : '// SDK not enabled'}
57
+
58
+ // Capture console output
59
+ const originalConsole = { ...console };
60
+ const captureConsole = (level) => (...args) => {
61
+ logs.push({
62
+ level,
63
+ message: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' '),
64
+ timestamp: Date.now()
65
+ });
66
+ originalConsole[level](...args);
67
+ };
68
+ console.log = captureConsole('log');
69
+ console.warn = captureConsole('warn');
70
+ console.error = captureConsole('error');
71
+ console.info = captureConsole('info');
72
+ console.debug = captureConsole('debug');
73
+
74
+ // ============================================================
75
+ // USER MODULE CODE (embedded at generation time)
76
+ // ============================================================
77
+ // Module exports object - exports become top-level variables
78
+ const exports = {};
79
+
80
+ ${
81
+ module
82
+ ? `
83
+ // Execute module code
84
+ try {
85
+ ${module}
86
+ } catch (e) {
87
+ console.error('Module error:', e.message);
88
+ }
89
+ `
90
+ : '// No module code provided'
91
+ }
92
+
93
+ // Expose all exports as top-level variables for tests and scripts
94
+ // This allows: export const add = (a, b) => a + b; then later: add(1, 2)
95
+ ${
96
+ rawModule
97
+ ? `
98
+ const { ${exportNames} } = exports;
99
+ `.trim()
100
+ : ''
101
+ }
102
+
103
+ // ============================================================
104
+ // RPC SERVER - Expose exports via capnweb
105
+ // ============================================================
106
+ class ExportsRpcTarget extends RpcTarget {
107
+ // Dynamically expose all exports as RPC methods
108
+ constructor() {
109
+ super();
110
+ for (const [key, value] of Object.entries(exports)) {
111
+ if (typeof value === 'function') {
112
+ this[key] = value;
113
+ }
114
+ }
115
+ }
116
+
117
+ // List available exports
118
+ list() {
119
+ return Object.keys(exports);
120
+ }
121
+
122
+ // Get an export by name
123
+ get(name) {
124
+ return exports[name];
125
+ }
126
+ }
127
+
128
+ // ============================================================
129
+ // WORKER ENTRY POINT
130
+ // ============================================================
131
+ export default {
132
+ async fetch(request, env) {
133
+ const url = new URL(request.url);
134
+
135
+ // Route: GET / - Return info about exports
136
+ if (request.method === 'GET' && url.pathname === '/') {
137
+ return Response.json({
138
+ exports: Object.keys(exports),
139
+ rpc: '/rpc',
140
+ execute: '/execute'
141
+ });
142
+ }
143
+
144
+ // Route: /rpc - capnweb RPC to module exports
145
+ if (url.pathname === '/rpc') {
146
+ return newWorkersRpcResponse(request, new ExportsRpcTarget());
147
+ }
148
+
149
+ // Route: GET /:name - Simple JSON endpoint to access exports
150
+ if (request.method === 'GET' && url.pathname !== '/execute') {
151
+ const name = url.pathname.slice(1); // Remove leading /
152
+ const value = exports[name];
153
+
154
+ // Check if export exists
155
+ if (!(name in exports)) {
156
+ return Response.json({ error: \`Export "\${name}" not found\` }, { status: 404 });
157
+ }
158
+
159
+ // If it's not a function, just return the value
160
+ if (typeof value !== 'function') {
161
+ return Response.json({ result: value });
162
+ }
163
+
164
+ // It's a function - parse args and call it
165
+ try {
166
+ const args = [];
167
+ const argsParam = url.searchParams.get('args');
168
+ if (argsParam) {
169
+ // Support JSON array: ?args=[1,2,3]
170
+ try {
171
+ const parsed = JSON.parse(argsParam);
172
+ if (Array.isArray(parsed)) {
173
+ args.push(...parsed);
174
+ } else {
175
+ args.push(parsed);
176
+ }
177
+ } catch {
178
+ // Not JSON, use as single string arg
179
+ args.push(argsParam);
180
+ }
181
+ } else {
182
+ // Support named params: ?a=1&b=2 -> passed as object
183
+ const params = Object.fromEntries(url.searchParams.entries());
184
+ if (Object.keys(params).length > 0) {
185
+ // Try to parse numeric values
186
+ for (const [key, val] of Object.entries(params)) {
187
+ const num = Number(val);
188
+ params[key] = !isNaN(num) && val !== '' ? num : val;
189
+ }
190
+ args.push(params);
191
+ }
192
+ }
193
+
194
+ const result = await value(...args);
195
+ return Response.json({ result });
196
+ } catch (e) {
197
+ return Response.json({ error: e.message }, { status: 500 });
198
+ }
199
+ }
200
+
201
+ // Route: /execute - Run tests and scripts
202
+ // Check for TEST service binding
203
+ if (!env.TEST) {
204
+ return Response.json({
205
+ success: false,
206
+ error: 'TEST service binding not available. Ensure ai-tests worker is bound.',
207
+ logs,
208
+ duration: 0
209
+ });
210
+ }
211
+
212
+ // Connect to get the TestServiceCore via RPC
213
+ const testService = await env.TEST.connect();
214
+
215
+ // Create global test functions that proxy to the RPC service
216
+ const describe = (name, fn) => testService.describe(name, fn);
217
+ const it = (name, fn) => testService.it(name, fn);
218
+ const test = (name, fn) => testService.test(name, fn);
219
+ const expect = (value, message) => testService.expect(value, message);
220
+ const should = (value) => testService.should(value);
221
+ const assert = testService.assert;
222
+ const beforeEach = (fn) => testService.beforeEach(fn);
223
+ const afterEach = (fn) => testService.afterEach(fn);
224
+ const beforeAll = (fn) => testService.beforeAll(fn);
225
+ const afterAll = (fn) => testService.afterAll(fn);
226
+
227
+ // Add skip/only modifiers
228
+ it.skip = (name, fn) => testService.skip(name, fn);
229
+ it.only = (name, fn) => testService.only(name, fn);
230
+ test.skip = it.skip;
231
+ test.only = it.only;
232
+
233
+ let scriptResult = undefined;
234
+ let scriptError = null;
235
+ let testResults = undefined;
236
+
237
+ // ============================================================
238
+ // USER TEST CODE (embedded at generation time)
239
+ // ============================================================
240
+
241
+ ${
242
+ tests
243
+ ? `
244
+ // Register tests
245
+ try {
246
+ ${tests}
247
+ } catch (e) {
248
+ console.error('Test registration error:', e.message);
249
+ }
250
+ `
251
+ : '// No test code provided'
252
+ }
253
+
254
+ // Execute user script
255
+ ${
256
+ script
257
+ ? `
258
+ try {
259
+ scriptResult = await (async () => {
260
+ ${script}
261
+ })();
262
+ } catch (e) {
263
+ console.error('Script error:', e.message);
264
+ scriptError = e.message;
265
+ }
266
+ `
267
+ : '// No script code provided'
268
+ }
269
+
270
+ // Run tests if any were registered
271
+ ${
272
+ tests
273
+ ? `
274
+ try {
275
+ testResults = await testService.run();
276
+ } catch (e) {
277
+ console.error('Test run error:', e.message);
278
+ testResults = { total: 0, passed: 0, failed: 1, skipped: 0, tests: [], duration: 0, error: e.message };
279
+ }
280
+ `
281
+ : ''
282
+ }
283
+
284
+ const hasTests = ${tests ? 'true' : 'false'};
285
+ const success = scriptError === null && (!hasTests || (testResults && testResults.failed === 0));
286
+
287
+ return Response.json({
288
+ success,
289
+ value: scriptResult,
290
+ logs,
291
+ testResults: hasTests ? testResults : undefined,
292
+ error: scriptError || undefined,
293
+ duration: 0
294
+ });
295
+ }
296
+ };
297
+ `
298
+ }
299
+
300
+ /**
301
+ * Generate worker code for development (embedded test framework)
302
+ *
303
+ * This version bundles the test framework directly into the worker,
304
+ * avoiding the need for RPC service bindings in local development.
305
+ */
306
+ export function generateDevWorkerCode(options: {
307
+ module?: string | undefined
308
+ tests?: string | undefined
309
+ script?: string | undefined
310
+ sdk?: SDKConfig | boolean | undefined
311
+ imports?: string[] | undefined
312
+ fetch?: null | FetchConfig | undefined
313
+ }): string {
314
+ const {
315
+ module: rawModule = '',
316
+ tests = '',
317
+ script: rawScript = '',
318
+ sdk,
319
+ imports = [],
320
+ fetch: fetchOption,
321
+ } = options
322
+ const sdkConfig = sdk === true ? {} : sdk || null
323
+ const module = rawModule ? transformModuleCode(rawModule) : ''
324
+ const script = rawScript ? wrapScriptForReturn(rawScript) : ''
325
+ const exportNames = getExportNames(rawModule)
326
+
327
+ // Determine fetch handling mode
328
+ // - false or null -> block all network
329
+ // - string[] -> domain allowlist
330
+ // - true or undefined -> allow all (no wrapper needed)
331
+ const blockFetch = fetchOption === false || fetchOption === null
332
+ const allowlistDomains = Array.isArray(fetchOption) ? fetchOption : null
333
+
334
+ // Hoisted imports (from MDX test files) - placed at true module top level
335
+ const hoistedImports = imports.length > 0 ? imports.join('\n') + '\n' : ''
336
+
337
+ // Generate fetch control code based on mode
338
+ let fetchControlCode = ''
339
+ if (blockFetch) {
340
+ fetchControlCode = `
341
+ // Block fetch when fetch: false or null is specified
342
+ const __originalFetch__ = globalThis.fetch;
343
+ globalThis.fetch = async (...args) => {
344
+ throw new Error('Network access blocked: fetch is disabled in this sandbox');
345
+ };
346
+ `
347
+ } else if (allowlistDomains) {
348
+ fetchControlCode = generateDomainCheckCode(allowlistDomains)
349
+ }
350
+
351
+ return `
352
+ // Sandbox Worker Entry Point (Dev Mode - embedded test framework)
353
+ ${hoistedImports}
354
+ const logs = [];
355
+ const testResults = { total: 0, passed: 0, failed: 0, skipped: 0, tests: [], duration: 0 };
356
+ const pendingTests = [];
357
+
358
+ ${fetchControlCode}
359
+
360
+ ${sdkConfig ? generateShouldCode() : ''}
361
+
362
+ ${sdkConfig ? generateSDKCode(sdkConfig) : '// SDK not enabled'}
363
+
364
+ // Capture console output
365
+ const originalConsole = { ...console };
366
+ const captureConsole = (level) => (...args) => {
367
+ logs.push({
368
+ level,
369
+ message: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' '),
370
+ timestamp: Date.now()
371
+ });
372
+ originalConsole[level](...args);
373
+ };
374
+ console.log = captureConsole('log');
375
+ console.warn = captureConsole('warn');
376
+ console.error = captureConsole('error');
377
+ console.info = captureConsole('info');
378
+ console.debug = captureConsole('debug');
379
+
380
+ ${generateTestFrameworkCode()}
381
+
382
+ // ============================================================
383
+ // USER MODULE CODE (embedded at generation time)
384
+ // ============================================================
385
+ // Module exports object - exports become top-level variables
386
+ const exports = {};
387
+
388
+ ${
389
+ module
390
+ ? `
391
+ // Execute module code
392
+ try {
393
+ ${module}
394
+ } catch (e) {
395
+ console.error('Module error:', e.message);
396
+ }
397
+ `
398
+ : '// No module code provided'
399
+ }
400
+
401
+ // Expose all exports as top-level variables for tests and scripts
402
+ // This allows: export const add = (a, b) => a + b; then later: add(1, 2)
403
+ ${
404
+ rawModule
405
+ ? `
406
+ const { ${exportNames} } = exports;
407
+ `.trim()
408
+ : ''
409
+ }
410
+
411
+ // ============================================================
412
+ // USER TEST CODE (embedded at generation time)
413
+ // ============================================================
414
+ ${
415
+ tests
416
+ ? `
417
+ // Register tests
418
+ try {
419
+ ${tests}
420
+ } catch (e) {
421
+ console.error('Test registration error:', e.message);
422
+ }
423
+ `
424
+ : '// No test code provided'
425
+ }
426
+
427
+ // ============================================================
428
+ // SIMPLE RPC HANDLER (dev mode - no capnweb dependency)
429
+ // ============================================================
430
+ async function handleRpc(request) {
431
+ try {
432
+ const { method, args = [] } = await request.json();
433
+ if (method === 'list') {
434
+ return Response.json({ result: Object.keys(exports) });
435
+ }
436
+ if (method === 'get') {
437
+ const [name] = args;
438
+ const value = exports[name];
439
+ if (typeof value === 'function') {
440
+ return Response.json({ result: { type: 'function', name } });
441
+ }
442
+ return Response.json({ result: value });
443
+ }
444
+ // Call an exported function
445
+ const fn = exports[method];
446
+ if (typeof fn !== 'function') {
447
+ return Response.json({ error: \`Export "\${method}" is not a function\` }, { status: 400 });
448
+ }
449
+ const result = await fn(...args);
450
+ return Response.json({ result });
451
+ } catch (e) {
452
+ return Response.json({ error: e.message }, { status: 500 });
453
+ }
454
+ }
455
+
456
+ // ============================================================
457
+ // WORKER ENTRY POINT
458
+ // ============================================================
459
+ export default {
460
+ async fetch(request, env) {
461
+ const url = new URL(request.url);
462
+
463
+ // Route: GET / - Return info about exports
464
+ if (request.method === 'GET' && url.pathname === '/') {
465
+ return Response.json({
466
+ exports: Object.keys(exports),
467
+ rpc: '/rpc',
468
+ execute: '/execute'
469
+ });
470
+ }
471
+
472
+ // Route: POST /rpc - Simple RPC to module exports
473
+ if (url.pathname === '/rpc' && request.method === 'POST') {
474
+ return handleRpc(request);
475
+ }
476
+
477
+ // Route: GET /:name - Simple JSON endpoint to access exports
478
+ if (request.method === 'GET' && url.pathname !== '/execute') {
479
+ const name = url.pathname.slice(1);
480
+ const value = exports[name];
481
+
482
+ // Check if export exists
483
+ if (!(name in exports)) {
484
+ return Response.json({ error: \`Export "\${name}" not found\` }, { status: 404 });
485
+ }
486
+
487
+ // If it's not a function, just return the value
488
+ if (typeof value !== 'function') {
489
+ return Response.json({ result: value });
490
+ }
491
+
492
+ // It's a function - parse args and call it
493
+ try {
494
+ const args = [];
495
+ const argsParam = url.searchParams.get('args');
496
+ if (argsParam) {
497
+ try {
498
+ const parsed = JSON.parse(argsParam);
499
+ if (Array.isArray(parsed)) args.push(...parsed);
500
+ else args.push(parsed);
501
+ } catch {
502
+ args.push(argsParam);
503
+ }
504
+ } else {
505
+ const params = Object.fromEntries(url.searchParams.entries());
506
+ if (Object.keys(params).length > 0) {
507
+ for (const [key, val] of Object.entries(params)) {
508
+ const num = Number(val);
509
+ params[key] = !isNaN(num) && val !== '' ? num : val;
510
+ }
511
+ args.push(params);
512
+ }
513
+ }
514
+ const result = await value(...args);
515
+ return Response.json({ result });
516
+ } catch (e) {
517
+ return Response.json({ error: e.message }, { status: 500 });
518
+ }
519
+ }
520
+
521
+ // Route: /execute - Run tests and scripts
522
+ let scriptResult = undefined;
523
+ let scriptError = null;
524
+
525
+ // Execute user script
526
+ ${
527
+ script
528
+ ? `
529
+ try {
530
+ scriptResult = await (async () => {
531
+ ${script}
532
+ })();
533
+ } catch (e) {
534
+ console.error('Script error:', e.message);
535
+ scriptError = e.message;
536
+ }
537
+ `
538
+ : '// No script code provided'
539
+ }
540
+
541
+ ${generateTestRunnerCode()}
542
+
543
+ const hasTests = ${tests ? 'true' : 'false'};
544
+ const success = scriptError === null && (!hasTests || testResults.failed === 0);
545
+
546
+ return Response.json({
547
+ success,
548
+ value: scriptResult,
549
+ logs,
550
+ testResults: hasTests ? testResults : undefined,
551
+ error: scriptError || undefined,
552
+ duration: 0
553
+ });
554
+ }
555
+ };
556
+ `
557
+ }