ai-evaluate 2.1.8 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/evaluate.d.ts.map +1 -1
  2. package/dist/evaluate.js.map +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/miniflare-pool.d.ts.map +1 -1
  6. package/dist/miniflare-pool.js.map +1 -1
  7. package/dist/node.d.ts.map +1 -1
  8. package/dist/node.js.map +1 -1
  9. package/dist/static/index.d.ts +111 -0
  10. package/dist/static/index.d.ts.map +1 -0
  11. package/dist/static/index.js +347 -0
  12. package/dist/static/index.js.map +1 -0
  13. package/dist/type-guards.d.ts.map +1 -1
  14. package/dist/type-guards.js.map +1 -1
  15. package/dist/worker-template/core.d.ts.map +1 -1
  16. package/dist/worker-template/core.js +1 -1
  17. package/dist/worker-template/core.js.map +1 -1
  18. package/package.json +17 -4
  19. package/public/capnweb.mjs +220 -0
  20. package/public/index.mjs +426 -0
  21. package/public/scaffold.mjs +198 -0
  22. package/.turbo/turbo-build.log +0 -4
  23. package/.turbo/turbo-test.log +0 -54
  24. package/.turbo/turbo-typecheck.log +0 -4
  25. package/CHANGELOG.md +0 -48
  26. package/example/package.json +0 -20
  27. package/example/src/index.ts +0 -221
  28. package/example/wrangler.jsonc +0 -25
  29. package/src/capnweb-bundle.ts +0 -2596
  30. package/src/evaluate.ts +0 -329
  31. package/src/index.ts +0 -23
  32. package/src/miniflare-pool.ts +0 -395
  33. package/src/node.ts +0 -245
  34. package/src/repl.ts +0 -228
  35. package/src/shared.ts +0 -186
  36. package/src/type-guards.ts +0 -323
  37. package/src/types.ts +0 -196
  38. package/src/validation.ts +0 -120
  39. package/src/worker-template/code-transforms.ts +0 -32
  40. package/src/worker-template/core.ts +0 -557
  41. package/src/worker-template/helpers.ts +0 -90
  42. package/src/worker-template/index.ts +0 -23
  43. package/src/worker-template/sdk-generator.ts +0 -2515
  44. package/src/worker-template/test-generator.ts +0 -358
  45. package/test/evaluate-extended.test.js +0 -429
  46. package/test/evaluate-extended.test.ts +0 -469
  47. package/test/evaluate.test.js +0 -235
  48. package/test/evaluate.test.ts +0 -253
  49. package/test/index.test.js +0 -77
  50. package/test/index.test.ts +0 -95
  51. package/test/miniflare-pool.test.ts +0 -246
  52. package/test/node.test.ts +0 -467
  53. package/test/security.test.ts +0 -1009
  54. package/test/shared.test.ts +0 -105
  55. package/test/type-guards.test.ts +0 -303
  56. package/test/validation.test.ts +0 -240
  57. package/test/worker-template.test.js +0 -365
  58. package/test/worker-template.test.ts +0 -432
  59. package/tsconfig.json +0 -22
  60. package/vitest.config.js +0 -21
  61. package/vitest.config.ts +0 -28
@@ -1,395 +0,0 @@
1
- /**
2
- * Miniflare instance pool for improved performance
3
- *
4
- * Reuses Miniflare instances between evaluations instead of creating/disposing
5
- * for each evaluation, providing 4-5x performance improvement.
6
- *
7
- * Uses Miniflare's setOptions() to update the worker script between uses,
8
- * avoiding the expensive instance creation/teardown cycle.
9
- */
10
-
11
- import type {
12
- Miniflare as MiniflareType,
13
- MiniflareOptions as MiniflareOptionsType,
14
- } from 'miniflare'
15
-
16
- /**
17
- * Pool configuration options
18
- */
19
- export interface PoolConfig {
20
- /** Number of instances to maintain in the pool (default: 3) */
21
- size?: number
22
- /** Milliseconds before disposing idle instance (default: 30000) */
23
- maxIdleTime?: number
24
- }
25
-
26
- /**
27
- * Outbound service handler type for network control
28
- * - () => never: Block all network access (throw error)
29
- * - (request: Request) => Response | Promise<Response>: Custom handler (allowlist, proxy, etc.)
30
- */
31
- export type OutboundServiceHandler =
32
- | (() => never)
33
- | ((request: Request) => Response | Promise<Response>)
34
-
35
- /**
36
- * Options for updating a pooled instance's worker
37
- */
38
- export interface WorkerOptions {
39
- script: string
40
- compatibilityDate?: string
41
- outboundService?: OutboundServiceHandler
42
- }
43
-
44
- /**
45
- * A pooled Miniflare instance with metadata
46
- */
47
- interface PooledInstance {
48
- instance: MiniflareType
49
- inUse: boolean
50
- lastUsed: number
51
- createdAt: number
52
- }
53
-
54
- // Type for the Miniflare constructor
55
- type MiniflareConstructor = new (config: MiniflareOptionsType) => MiniflareType
56
-
57
- /**
58
- * Global pool state (singleton per process)
59
- */
60
- let pool: PooledInstance[] = []
61
- let poolConfig: Required<PoolConfig> = {
62
- size: 3,
63
- maxIdleTime: 30000,
64
- }
65
- let idleCleanupInterval: NodeJS.Timeout | null = null
66
- let MiniflareClass: MiniflareConstructor | null = null
67
- let isShuttingDown = false
68
-
69
- // Default worker script for warm instances
70
- const WARM_WORKER_SCRIPT = `
71
- export default {
72
- async fetch(request, env) {
73
- return new Response('ready', { status: 200 });
74
- }
75
- };
76
- `
77
-
78
- /**
79
- * Configure the Miniflare pool
80
- *
81
- * @example
82
- * ```ts
83
- * import { configurePool } from 'ai-evaluate/node'
84
- *
85
- * configurePool({
86
- * size: 5, // Keep 5 warm instances
87
- * maxIdleTime: 60000 // Dispose after 60s idle
88
- * })
89
- * ```
90
- */
91
- export function configurePool(config: PoolConfig): void {
92
- poolConfig = {
93
- size: config.size ?? poolConfig.size,
94
- maxIdleTime: config.maxIdleTime ?? poolConfig.maxIdleTime,
95
- }
96
- startIdleCleanup()
97
- }
98
-
99
- /**
100
- * Get the current pool configuration
101
- */
102
- export function getPoolConfig(): Required<PoolConfig> {
103
- return { ...poolConfig }
104
- }
105
-
106
- /**
107
- * Get pool statistics for monitoring
108
- */
109
- export function getPoolStats(): {
110
- size: number
111
- available: number
112
- inUse: number
113
- config: Required<PoolConfig>
114
- } {
115
- const available = pool.filter((p) => !p.inUse).length
116
- return {
117
- size: pool.length,
118
- available,
119
- inUse: pool.length - available,
120
- config: { ...poolConfig },
121
- }
122
- }
123
-
124
- /**
125
- * Initialize the Miniflare class (lazy load)
126
- */
127
- async function getMiniflareClass(): Promise<MiniflareConstructor> {
128
- if (!MiniflareClass) {
129
- const { Miniflare } = await import('miniflare')
130
- MiniflareClass = Miniflare as MiniflareConstructor
131
- }
132
- return MiniflareClass
133
- }
134
-
135
- /**
136
- * Create a new Miniflare instance with a warm worker
137
- */
138
- async function createInstance(): Promise<MiniflareType> {
139
- const Miniflare = await getMiniflareClass()
140
- return new Miniflare({
141
- modules: true,
142
- script: WARM_WORKER_SCRIPT,
143
- compatibilityDate: '2026-01-01',
144
- })
145
- }
146
-
147
- /**
148
- * Start the idle cleanup interval
149
- */
150
- function startIdleCleanup(): void {
151
- if (idleCleanupInterval) {
152
- clearInterval(idleCleanupInterval)
153
- }
154
-
155
- idleCleanupInterval = setInterval(async () => {
156
- if (isShuttingDown) return
157
-
158
- const now = Date.now()
159
- const toDispose: PooledInstance[] = []
160
-
161
- // Find idle instances beyond the idle timeout
162
- for (let i = pool.length - 1; i >= 0; i--) {
163
- const item = pool[i]
164
- if (!item.inUse && now - item.lastUsed > poolConfig.maxIdleTime) {
165
- // Keep at least one warm instance
166
- if (pool.filter((p) => !p.inUse && !toDispose.includes(p)).length > 1) {
167
- toDispose.push(item)
168
- pool.splice(i, 1)
169
- }
170
- }
171
- }
172
-
173
- // Dispose old instances
174
- for (const item of toDispose) {
175
- try {
176
- await item.instance.dispose()
177
- } catch {
178
- // Ignore disposal errors
179
- }
180
- }
181
- }, 5000) // Check every 5 seconds
182
-
183
- // Don't keep the process alive just for cleanup
184
- if (idleCleanupInterval.unref) {
185
- idleCleanupInterval.unref()
186
- }
187
- }
188
-
189
- /**
190
- * Acquire a Miniflare instance from the pool and configure it with a worker
191
- *
192
- * If a free instance is available, it will be reconfigured and returned.
193
- * Otherwise, a new instance will be created (up to pool size limit).
194
- * If pool is exhausted, creates a temporary instance.
195
- *
196
- * @param workerOptions - Configuration for the worker to run
197
- * @returns Object with the configured instance and a release function
198
- */
199
- export async function acquireInstance(workerOptions: WorkerOptions): Promise<{
200
- instance: MiniflareType
201
- release: () => Promise<void>
202
- isPooled: boolean
203
- }> {
204
- if (isShuttingDown) {
205
- throw new Error('Pool is shutting down')
206
- }
207
-
208
- // Start idle cleanup if not started
209
- if (!idleCleanupInterval) {
210
- startIdleCleanup()
211
- }
212
-
213
- const { script, compatibilityDate = '2026-01-01', outboundService } = workerOptions
214
-
215
- // Build the options for setOptions
216
- const updateOptions: MiniflareOptionsType = {
217
- modules: true,
218
- script,
219
- compatibilityDate,
220
- }
221
-
222
- // Only add outboundService if it's defined (for blocking network)
223
- if (outboundService !== undefined) {
224
- updateOptions.outboundService = outboundService
225
- }
226
-
227
- // Try to find an available instance
228
- const available = pool.find((p) => !p.inUse)
229
- if (available) {
230
- available.inUse = true
231
- // Reconfigure the instance with the new worker script
232
- await available.instance.setOptions(updateOptions)
233
- return {
234
- instance: available.instance,
235
- release: async () => {
236
- available.inUse = false
237
- available.lastUsed = Date.now()
238
- },
239
- isPooled: true,
240
- }
241
- }
242
-
243
- // Create new instance if pool not full
244
- if (pool.length < poolConfig.size) {
245
- const Miniflare = await getMiniflareClass()
246
- const instance = new Miniflare(updateOptions)
247
- const pooled: PooledInstance = {
248
- instance,
249
- inUse: true,
250
- lastUsed: Date.now(),
251
- createdAt: Date.now(),
252
- }
253
- pool.push(pooled)
254
- return {
255
- instance,
256
- release: async () => {
257
- pooled.inUse = false
258
- pooled.lastUsed = Date.now()
259
- },
260
- isPooled: true,
261
- }
262
- }
263
-
264
- // Pool exhausted - create temporary instance
265
- const Miniflare = await getMiniflareClass()
266
- const tempInstance = new Miniflare(updateOptions)
267
- return {
268
- instance: tempInstance,
269
- release: async () => {
270
- // Dispose temporary instance immediately
271
- await tempInstance.dispose()
272
- },
273
- isPooled: false,
274
- }
275
- }
276
-
277
- /**
278
- * Pre-warm the pool with instances
279
- *
280
- * Call this at application startup to avoid cold start latency.
281
- *
282
- * @example
283
- * ```ts
284
- * import { warmPool } from 'ai-evaluate/node'
285
- *
286
- * // Pre-warm 3 instances at startup
287
- * await warmPool(3)
288
- * ```
289
- */
290
- export async function warmPool(count?: number): Promise<void> {
291
- const targetCount = count ?? poolConfig.size
292
- const toCreate = Math.max(0, targetCount - pool.length)
293
-
294
- const promises: Promise<void>[] = []
295
- for (let i = 0; i < toCreate; i++) {
296
- promises.push(
297
- (async () => {
298
- const instance = await createInstance()
299
- pool.push({
300
- instance,
301
- inUse: false,
302
- lastUsed: Date.now(),
303
- createdAt: Date.now(),
304
- })
305
- })()
306
- )
307
- }
308
-
309
- await Promise.all(promises)
310
-
311
- // Start idle cleanup if not already started
312
- if (!idleCleanupInterval) {
313
- startIdleCleanup()
314
- }
315
- }
316
-
317
- /**
318
- * Dispose all instances and clean up the pool
319
- *
320
- * Call this before process exit to ensure clean shutdown.
321
- *
322
- * @example
323
- * ```ts
324
- * import { disposePool } from 'ai-evaluate/node'
325
- *
326
- * process.on('beforeExit', async () => {
327
- * await disposePool()
328
- * })
329
- * ```
330
- */
331
- export async function disposePool(): Promise<void> {
332
- isShuttingDown = true
333
-
334
- if (idleCleanupInterval) {
335
- clearInterval(idleCleanupInterval)
336
- idleCleanupInterval = null
337
- }
338
-
339
- const instances = [...pool]
340
- pool = []
341
-
342
- await Promise.all(
343
- instances.map(async (item) => {
344
- try {
345
- await item.instance.dispose()
346
- } catch {
347
- // Ignore disposal errors
348
- }
349
- })
350
- )
351
-
352
- isShuttingDown = false
353
- }
354
-
355
- /**
356
- * Reset the pool (for testing purposes)
357
- */
358
- export async function resetPool(): Promise<void> {
359
- await disposePool()
360
- poolConfig = {
361
- size: 3,
362
- maxIdleTime: 30000,
363
- }
364
- MiniflareClass = null
365
- }
366
-
367
- // Register cleanup on process exit
368
- if (typeof process !== 'undefined') {
369
- const cleanup = () => {
370
- isShuttingDown = true
371
- if (idleCleanupInterval) {
372
- clearInterval(idleCleanupInterval)
373
- }
374
- // Synchronous disposal attempt - best effort
375
- for (const item of pool) {
376
- try {
377
- // Fire and forget - we're exiting anyway
378
- item.instance.dispose().catch(() => {})
379
- } catch {
380
- // Ignore errors during shutdown
381
- }
382
- }
383
- pool = []
384
- }
385
-
386
- process.on('exit', cleanup)
387
- process.on('SIGINT', () => {
388
- cleanup()
389
- process.exit(0)
390
- })
391
- process.on('SIGTERM', () => {
392
- cleanup()
393
- process.exit(0)
394
- })
395
- }
package/src/node.ts DELETED
@@ -1,245 +0,0 @@
1
- /**
2
- * Evaluate code in a sandboxed environment (Node.js version)
3
- *
4
- * Uses Cloudflare worker_loaders when available, falls back to Miniflare for local dev.
5
- * For Workers-only builds, import from 'ai-evaluate' instead.
6
- */
7
-
8
- import type {
9
- EvaluateOptions,
10
- EvaluateResult,
11
- WorkerLoader,
12
- SandboxEnv,
13
- FetchConfig,
14
- } from './types.js'
15
- import { generateWorkerCode, generateDevWorkerCode } from './worker-template/index.js'
16
- import { isDomainAllowed, normalizeImports } from './shared.js'
17
-
18
- /**
19
- * Check if code contains JSX syntax that needs transformation
20
- */
21
- function containsJSX(code: string): boolean {
22
- if (!code) return false
23
- const jsxPattern = /<[A-Z][a-zA-Z0-9]*[\s/>]|<[a-z][a-z0-9-]*[\s/>]|<>|<\/>/
24
- const jsxReturnPattern = /return\s*\(\s*<|return\s+<[A-Za-z]/
25
- return jsxPattern.test(code) || jsxReturnPattern.test(code)
26
- }
27
-
28
- /**
29
- * Transform JSX in code using esbuild
30
- */
31
- async function transformJSX(code: string): Promise<string> {
32
- if (!code || !containsJSX(code)) return code
33
-
34
- try {
35
- const { transform } = await import('esbuild')
36
- const result = await transform(code, {
37
- loader: 'tsx',
38
- jsxFactory: 'h',
39
- jsxFragment: 'Fragment',
40
- target: 'esnext',
41
- format: 'esm',
42
- })
43
- return result.code
44
- } catch (error) {
45
- console.error('JSX transform failed:', error)
46
- return code
47
- }
48
- }
49
-
50
- /**
51
- * Evaluate code in a sandboxed worker (Node.js version with Miniflare fallback)
52
- */
53
- export async function evaluate(
54
- options: EvaluateOptions,
55
- env?: SandboxEnv
56
- ): Promise<EvaluateResult> {
57
- const start = Date.now()
58
-
59
- try {
60
- // Transform JSX in module, tests, and script before evaluation
61
- const transformedModule = options.module ? await transformJSX(options.module) : undefined
62
- const transformedTests = options.tests ? await transformJSX(options.tests) : undefined
63
- const transformedScript = options.script ? await transformJSX(options.script) : undefined
64
-
65
- const transformedOptions: EvaluateOptions = {
66
- ...options,
67
- module: transformedModule,
68
- tests: transformedTests,
69
- script: transformedScript,
70
- imports: normalizeImports(options.imports),
71
- }
72
-
73
- // Use worker_loaders if available (Cloudflare Workers)
74
- // Check lowercase first (preferred), then legacy uppercase
75
- const loader = env?.loader || env?.LOADER
76
- const testService = env?.test || env?.TEST
77
- if (loader && testService) {
78
- return await evaluateWithWorkerLoader(transformedOptions, loader, testService, start)
79
- }
80
-
81
- // Fall back to Miniflare (Node.js/local development)
82
- return await evaluateWithMiniflare(transformedOptions, start)
83
- } catch (error) {
84
- return {
85
- success: false,
86
- logs: [],
87
- error: error instanceof Error ? error.message : String(error),
88
- duration: Date.now() - start,
89
- }
90
- }
91
- }
92
-
93
- /**
94
- * Evaluate using Cloudflare worker_loaders binding
95
- */
96
- async function evaluateWithWorkerLoader(
97
- options: EvaluateOptions,
98
- loader: WorkerLoader,
99
- testService: unknown,
100
- start: number
101
- ): Promise<EvaluateResult> {
102
- const workerCode = generateWorkerCode({
103
- module: options.module,
104
- tests: options.tests,
105
- script: options.script,
106
- sdk: options.sdk,
107
- imports: options.imports,
108
- })
109
- const id = `sandbox-${Date.now()}-${Math.random().toString(36).slice(2)}`
110
-
111
- const worker = loader.get(id, async () => ({
112
- mainModule: 'worker.js',
113
- modules: {
114
- 'worker.js': workerCode,
115
- },
116
- compatibilityDate: '2026-01-01',
117
- globalOutbound: options.fetch === null ? null : undefined,
118
- bindings: {
119
- TEST: testService,
120
- },
121
- }))
122
-
123
- const entrypoint = worker.getEntrypoint()
124
- const response = await entrypoint.fetch(new Request('http://sandbox/execute'))
125
- const result = (await response.json()) as EvaluateResult
126
-
127
- return {
128
- ...result,
129
- duration: Date.now() - start,
130
- }
131
- }
132
-
133
- /**
134
- * Determine if network access should be blocked based on fetch options
135
- * fetch: false | null -> block
136
- */
137
- function shouldBlockNetwork(options: EvaluateOptions): boolean {
138
- return options.fetch === false || options.fetch === null
139
- }
140
-
141
- /**
142
- * Get allowlist domains if fetch is an array
143
- * fetch: string[] -> allowlist
144
- */
145
- function getAllowlistDomains(options: EvaluateOptions): string[] | null {
146
- if (Array.isArray(options.fetch)) {
147
- return options.fetch
148
- }
149
- return null
150
- }
151
-
152
- /**
153
- * Evaluate using Miniflare (for Node.js/development)
154
- */
155
- async function evaluateWithMiniflare(
156
- options: EvaluateOptions,
157
- start: number
158
- ): Promise<EvaluateResult> {
159
- const { Miniflare } = await import('miniflare')
160
-
161
- const workerCode = generateDevWorkerCode({
162
- module: options.module,
163
- tests: options.tests,
164
- script: options.script,
165
- sdk: options.sdk,
166
- imports: options.imports,
167
- fetch: options.fetch, // Pass fetch option to worker template
168
- })
169
-
170
- // Determine outbound service configuration based on fetch option
171
- const blockNetwork = shouldBlockNetwork(options)
172
- const allowlistDomains = getAllowlistDomains(options)
173
-
174
- // Build outboundService based on mode:
175
- // - block: throw error for all requests
176
- // - allowlist: check domain against allowlist
177
- // - allow (default): no outboundService (allow all)
178
- type OutboundServiceFn = (() => never) | ((request: Request) => Response | Promise<Response>)
179
- let outboundService: OutboundServiceFn | undefined
180
- if (blockNetwork) {
181
- outboundService = () => {
182
- throw new Error('Network access blocked: fetch is disabled in this sandbox')
183
- }
184
- } else if (allowlistDomains) {
185
- outboundService = (request: Request) => {
186
- const url = request.url
187
- if (!isDomainAllowed(url, allowlistDomains)) {
188
- const hostname = new URL(url).hostname
189
- throw new Error(`Network access blocked: domain not in allowlist. Attempted: ${hostname}`)
190
- }
191
- // Allow the request by returning a fetched response
192
- return fetch(request)
193
- }
194
- }
195
-
196
- const mf = new Miniflare({
197
- modules: true,
198
- script: workerCode,
199
- compatibilityDate: '2026-01-01',
200
- // Configure outbound service based on fetch mode
201
- ...(outboundService && { outboundService }),
202
- })
203
-
204
- try {
205
- const timeout = options.timeout || 5000
206
- const controller = new AbortController()
207
- const timeoutId = setTimeout(() => controller.abort(), timeout)
208
-
209
- try {
210
- const response = await mf.dispatchFetch('http://sandbox/execute', {
211
- signal: controller.signal,
212
- })
213
- clearTimeout(timeoutId)
214
- const result = (await response.json()) as EvaluateResult
215
-
216
- return {
217
- ...result,
218
- duration: Date.now() - start,
219
- }
220
- } catch (err) {
221
- clearTimeout(timeoutId)
222
- if ((err as Error).name === 'AbortError') {
223
- return {
224
- success: false,
225
- logs: [],
226
- error: `Timeout: Script execution exceeded ${timeout}ms`,
227
- duration: Date.now() - start,
228
- }
229
- }
230
- throw err
231
- }
232
- } finally {
233
- await mf.dispose()
234
- }
235
- }
236
-
237
- /**
238
- * Create an evaluate function bound to a specific environment
239
- */
240
- export function createEvaluator(env?: SandboxEnv) {
241
- return (options: EvaluateOptions) => evaluate(options, env)
242
- }
243
-
244
- // Re-export types
245
- export type { EvaluateOptions, EvaluateResult, SandboxEnv } from './types.js'