/** * Runtime parser for test file naming convention * Supports: test.runtime1+runtime2.modifier.ts * Examples: * - test.node.ts * - test.chromium.ts * - test.node+chromium.ts * - test.deno+bun.ts * - test.chromium.nonci.ts */ export type Runtime = 'node' | 'chromium' | 'deno' | 'bun'; export type Modifier = 'nonci'; export interface ParsedFilename { baseName: string; runtimes: Runtime[]; modifiers: Modifier[]; extension: string; isLegacy: boolean; original: string; } export interface ParserConfig { strictUnknownRuntime?: boolean; // default: true defaultRuntimes?: Runtime[]; // default: ['node'] } const KNOWN_RUNTIMES: Set = new Set(['node', 'chromium', 'deno', 'bun']); const KNOWN_MODIFIERS: Set = new Set(['nonci']); const VALID_EXTENSIONS: Set = new Set(['ts', 'tsx', 'mts', 'cts']); // Legacy mappings for backwards compatibility const LEGACY_RUNTIME_MAP: Record = { browser: ['chromium'], both: ['node', 'chromium'], }; /** * Parse a test filename to extract runtimes, modifiers, and detect legacy patterns * Algorithm: Right-to-left token analysis from the extension */ export function parseTestFilename( filePath: string, config: ParserConfig = {} ): ParsedFilename { const strictUnknownRuntime = config.strictUnknownRuntime ?? true; const defaultRuntimes = config.defaultRuntimes ?? ['node']; // Extract just the filename from the path const fileName = filePath.split('/').pop() || filePath; const original = fileName; // Step 1: Extract and validate extension const lastDot = fileName.lastIndexOf('.'); if (lastDot === -1) { throw new Error(`Invalid test file: no extension found in "${fileName}"`); } const extension = fileName.substring(lastDot + 1); if (!VALID_EXTENSIONS.has(extension)) { throw new Error( `Invalid test file extension ".${extension}" in "${fileName}". ` + `Valid extensions: ${Array.from(VALID_EXTENSIONS).join(', ')}` ); } // Step 2: Split remaining basename by dots const withoutExtension = fileName.substring(0, lastDot); const tokens = withoutExtension.split('.'); if (tokens.length === 0) { throw new Error(`Invalid test file: empty basename in "${fileName}"`); } // Step 3: Parse from right to left let isLegacy = false; const modifiers: Modifier[] = []; let runtimes: Runtime[] = []; let runtimeTokenIndex = -1; // Scan from right to left for (let i = tokens.length - 1; i >= 0; i--) { const token = tokens[i]; // Check if this is a known modifier if (KNOWN_MODIFIERS.has(token)) { modifiers.unshift(token as Modifier); continue; } // Check if this is a legacy runtime token if (LEGACY_RUNTIME_MAP[token]) { isLegacy = true; runtimes = LEGACY_RUNTIME_MAP[token]; runtimeTokenIndex = i; break; } // Check if this is a runtime chain (may contain + separators) if (token.includes('+')) { const runtimeCandidates = token.split('+').map(r => r.trim()).filter(Boolean); const validRuntimes: Runtime[] = []; const invalidRuntimes: string[] = []; for (const candidate of runtimeCandidates) { if (KNOWN_RUNTIMES.has(candidate)) { // Dedupe: only add if not already in list if (!validRuntimes.includes(candidate as Runtime)) { validRuntimes.push(candidate as Runtime); } } else { invalidRuntimes.push(candidate); } } if (invalidRuntimes.length > 0) { if (strictUnknownRuntime) { throw new Error( `Unknown runtime(s) in "${fileName}": ${invalidRuntimes.join(', ')}. ` + `Valid runtimes: ${Array.from(KNOWN_RUNTIMES).join(', ')}` ); } else { console.warn( `⚠️ Warning: Unknown runtime(s) in "${fileName}": ${invalidRuntimes.join(', ')}. ` + `Defaulting to: ${defaultRuntimes.join('+')}` ); runtimes = [...defaultRuntimes]; runtimeTokenIndex = i; break; } } if (validRuntimes.length > 0) { runtimes = validRuntimes; runtimeTokenIndex = i; break; } } // Check if this is a single runtime token if (KNOWN_RUNTIMES.has(token)) { runtimes = [token as Runtime]; runtimeTokenIndex = i; break; } // If we've scanned past modifiers and haven't found a runtime, stop looking if (modifiers.length > 0) { break; } } // Step 4: Determine base name // Everything before the runtime token (if found) is the base name const baseNameTokens = runtimeTokenIndex >= 0 ? tokens.slice(0, runtimeTokenIndex) : tokens; const baseName = baseNameTokens.join('.'); // Step 5: Apply defaults if no runtime was detected if (runtimes.length === 0) { runtimes = [...defaultRuntimes]; } return { baseName: baseName || 'test', runtimes, modifiers, extension, isLegacy, original, }; } /** * Check if a filename uses legacy naming convention */ export function isLegacyFilename(fileName: string): boolean { const tokens = fileName.split('.'); for (const token of tokens) { if (LEGACY_RUNTIME_MAP[token]) { return true; } } return false; } /** * Get the suggested new filename for a legacy filename */ export function getLegacyMigrationTarget(fileName: string): string | null { const parsed = parseTestFilename(fileName, { strictUnknownRuntime: false }); if (!parsed.isLegacy) { return null; } // Reconstruct filename with new naming const parts = [parsed.baseName]; if (parsed.runtimes.length > 0) { parts.push(parsed.runtimes.join('+')); } if (parsed.modifiers.length > 0) { parts.push(...parsed.modifiers); } parts.push(parsed.extension); return parts.join('.'); }