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
|
@@ -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
|
+
}
|