diff --git a/plugins/promptfoo/src/generator/config-imports.test.ts b/plugins/promptfoo/src/generator/config-imports.test.ts new file mode 100644 index 0000000..c419233 --- /dev/null +++ b/plugins/promptfoo/src/generator/config-imports.test.ts @@ -0,0 +1,42 @@ +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; + +import { afterEach, describe, expect, it } from 'vitest'; + +import { writeProviderFile } from './config.js'; + +const tempDirs: string[] = []; + +afterEach(() => { + for (const dir of tempDirs.splice(0)) { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +describe('writeProviderFile dependency detection', () => { + it('extracts package imports without regex backtracking', () => { + const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'crabcode-config-')); + tempDirs.push(outputDir); + + writeProviderFile({ + outputDir, + filename: 'provider.js', + code: [ + 'import \t\taxios from "axios";', + 'import { WebSocket } from "ws";', + 'import localThing from "./local.js";', + 'import fs from "node:fs";', + ].join('\n'), + }); + + const packageJson = JSON.parse( + fs.readFileSync(path.join(outputDir, 'package.json'), 'utf-8') + ) as { dependencies: Record }; + + expect(packageJson.dependencies).toEqual({ + axios: '^1.6.0', + ws: '^8.18.0', + }); + }); +}); diff --git a/plugins/promptfoo/src/generator/config.ts b/plugins/promptfoo/src/generator/config.ts index bc749e8..d4dfed8 100644 --- a/plugins/promptfoo/src/generator/config.ts +++ b/plugins/promptfoo/src/generator/config.ts @@ -167,12 +167,12 @@ export function writeProviderFile(options: { function detectDependencies(code: string): Record { const deps: Record = {}; - // Match: import X from 'package' or import { X } from 'package' - const importRegex = /import\s+(?:[\w{}\s,*]+)\s+from\s+['"]([^'"./][^'"]*)['"]/g; - let match; + for (const line of code.split(/\r?\n/)) { + const pkg = extractImportSpecifier(line); + if (!pkg) { + continue; + } - while ((match = importRegex.exec(code)) !== null) { - const pkg = match[1]; // Skip node built-ins if (!pkg.startsWith('node:')) { // Common package versions @@ -188,6 +188,33 @@ function detectDependencies(code: string): Record { return deps; } +function extractImportSpecifier(line: string): string | null { + const trimmed = line.trim(); + if (!trimmed.startsWith('import ')) { + return null; + } + + const fromIndex = trimmed.indexOf(' from '); + const specifierStart = fromIndex >= 0 ? fromIndex + 6 : 'import '.length; + const quote = trimmed[specifierStart]; + + if (quote !== '"' && quote !== "'") { + return null; + } + + const specifierEnd = trimmed.indexOf(quote, specifierStart + 1); + if (specifierEnd === -1) { + return null; + } + + const specifier = trimmed.slice(specifierStart + 1, specifierEnd); + if (specifier.startsWith('.') || specifier.startsWith('/')) { + return null; + } + + return specifier; +} + /** * Generate a simple HTTP provider config */