import * as plugins from './tstest.plugins.js'; import type { DenoOptions, RuntimeOptions } from './tstest.classes.runtime.adapter.js'; import type { Runtime } from './tstest.classes.runtime.parser.js'; import { DENO_DEFAULT_PERMISSIONS } from './tstest.classes.runtime.deno.js'; type DirectiveScope = Runtime | 'global'; export interface ITestFileDirective { scope: DirectiveScope; key: string; value?: string; } export interface IParsedDirectives { deno: ITestFileDirective[]; node: ITestFileDirective[]; bun: ITestFileDirective[]; chromium: ITestFileDirective[]; global: ITestFileDirective[]; } const VALID_SCOPES = new Set(['deno', 'node', 'bun', 'chromium']); const DENO_PERMISSION_MAP: Record = { allowAll: '--allow-all', allowRun: '--allow-run', allowFfi: '--allow-ffi', allowHrtime: '--allow-hrtime', allowRead: '--allow-read', allowWrite: '--allow-write', allowNet: '--allow-net', allowEnv: '--allow-env', allowSys: '--allow-sys', }; function createEmptyDirectives(): IParsedDirectives { return { deno: [], node: [], bun: [], chromium: [], global: [] }; } /** * Parse tstest directives from file content. * Scans comments at the top of the file (before any code). */ export function parseDirectivesFromContent(content: string): IParsedDirectives { const result = createEmptyDirectives(); const lines = content.split('\n'); const maxLines = Math.min(lines.length, 30); for (let i = 0; i < maxLines; i++) { const line = lines[i].trim(); // Skip empty lines if (line === '') continue; // Stop at first non-comment line if (!line.startsWith('//')) break; // Match tstest directive: // tstest: const match = line.match(/^\/\/\s*tstest:(.+)$/); if (!match) continue; const parts = match[1].split(':'); if (parts.length < 2) { console.warn(`Warning: malformed tstest directive: "${line}"`); continue; } const scopeStr = parts[0].trim(); const key = parts[1].trim(); const value = parts.length > 2 ? parts.slice(2).join(':').trim() : undefined; // Handle global directives (env, timeout) if (scopeStr === 'env' || scopeStr === 'timeout') { result.global.push({ scope: 'global', key: scopeStr, value: key + (value !== undefined ? ':' + value : ''), }); continue; } if (!VALID_SCOPES.has(scopeStr)) { console.warn(`Warning: unknown tstest directive scope "${scopeStr}" in: "${line}"`); continue; } const scope = scopeStr as Runtime; result[scope].push({ scope, key, value }); } return result; } /** * Parse directives from a test file on disk. */ export async function parseDirectivesFromFile(filePath: string): Promise { try { const content = plugins.fs.readFileSync(filePath, 'utf8'); return parseDirectivesFromContent(content); } catch { return createEmptyDirectives(); } } /** * Merge directives from 00init.ts and the test file. * Test file directives are appended (take effect after init directives). */ export function mergeDirectives(init: IParsedDirectives, testFile: IParsedDirectives): IParsedDirectives { return { deno: [...init.deno, ...testFile.deno], node: [...init.node, ...testFile.node], bun: [...init.bun, ...testFile.bun], chromium: [...init.chromium, ...testFile.chromium], global: [...init.global, ...testFile.global], }; } /** * Check if any directives exist for any scope. */ export function hasDirectives(directives: IParsedDirectives): boolean { return ( directives.deno.length > 0 || directives.node.length > 0 || directives.bun.length > 0 || directives.chromium.length > 0 || directives.global.length > 0 ); } /** * Convert parsed directives into DenoOptions. */ function directivesToDenoOptions(directives: IParsedDirectives): DenoOptions | undefined { const denoDirectives = directives.deno; if (denoDirectives.length === 0 && directives.global.length === 0) return undefined; const options: DenoOptions = {}; const extraPermissions: string[] = []; const extraArgs: string[] = []; const env: Record = {}; let useAllowAll = false; for (const d of denoDirectives) { if (d.key === 'allowAll') { useAllowAll = true; } else if (DENO_PERMISSION_MAP[d.key]) { extraPermissions.push(DENO_PERMISSION_MAP[d.key]); } else if (d.key === 'flag' && d.value) { extraArgs.push(d.value); } } // Process global directives for (const d of directives.global) { if (d.key === 'env' && d.value) { const eqIndex = d.value.indexOf('='); if (eqIndex > 0) { env[d.value.substring(0, eqIndex)] = d.value.substring(eqIndex + 1); } } } if (useAllowAll) { // --allow-all replaces individual permissions, but keep compatibility flags options.permissions = ['--allow-all', '--node-modules-dir', '--sloppy-imports']; } else if (extraPermissions.length > 0) { // Start with defaults and add extra permissions (deduplicated) const allPermissions = [...DENO_DEFAULT_PERMISSIONS]; for (const p of extraPermissions) { if (!allPermissions.includes(p)) { allPermissions.push(p); } } options.permissions = allPermissions; } if (extraArgs.length > 0) options.extraArgs = extraArgs; if (Object.keys(env).length > 0) options.env = env; // Return undefined if nothing was set if (!options.permissions && !options.extraArgs && !options.env) return undefined; return options; } /** * Convert parsed directives into RuntimeOptions for Node/Bun (flag directives only). */ function directivesToGenericOptions(directives: ITestFileDirective[], globalDirectives: ITestFileDirective[]): RuntimeOptions | undefined { const extraArgs: string[] = []; const env: Record = {}; for (const d of directives) { if (d.key === 'flag' && d.value) { extraArgs.push(d.value); } } for (const d of globalDirectives) { if (d.key === 'env' && d.value) { const eqIndex = d.value.indexOf('='); if (eqIndex > 0) { env[d.value.substring(0, eqIndex)] = d.value.substring(eqIndex + 1); } } } if (extraArgs.length === 0 && Object.keys(env).length === 0) return undefined; const options: RuntimeOptions = {}; if (extraArgs.length > 0) options.extraArgs = extraArgs; if (Object.keys(env).length > 0) options.env = env; return options; } /** * Convert parsed directives into RuntimeOptions for a specific runtime. */ export function directivesToRuntimeOptions(directives: IParsedDirectives, runtime: Runtime): RuntimeOptions | undefined { if (runtime === 'deno') { return directivesToDenoOptions(directives); } return directivesToGenericOptions(directives[runtime] || [], directives.global); }