UNPKG

@moonwall/cli

Version:

Testing framework for the Moon family of projects

168 lines 6.17 kB
import { Lang, parse, findInFiles } from "@ast-grep/napi"; /** * Extracts the "id" property value from an object literal AST node. * Traverses children to find a "pair" node where the key is "id". */ function extractIdFromObject(objNode) { for (const child of objNode.children()) { if (child.kind() === "pair") { const key = child.field("key"); const value = child.field("value"); if (key?.text() === "id" && value) { // Remove quotes from the string literal return value.text().replace(/^['"`]|['"`]$/g, ""); } } } return undefined; } /** * Finds the "id" property node within an object literal for replacement. */ function findIdValueNode(objNode) { for (const child of objNode.children()) { if (child.kind() === "pair") { const key = child.field("key"); const value = child.field("value"); if (key?.text() === "id" && value) { return value; } } } return undefined; } /** * Extracts suite and test IDs from a Moonwall test file using AST parsing. * This is more robust than regex as it handles comments, multiline formatting, etc. */ export function extractTestIds(fileContent) { const ast = parse(Lang.TypeScript, fileContent); const root = ast.root(); // Find describeSuite({ id: "...", ... }) const describeSuiteCall = root.find("describeSuite($OPTS)"); let suiteId; if (describeSuiteCall) { const optsNode = describeSuiteCall.getMatch("OPTS"); if (optsNode && optsNode.kind() === "object") { suiteId = extractIdFromObject(optsNode); } } // Find all it({ id: "...", ... }) calls const testIds = []; const itCalls = root.findAll("it($OPTS)"); for (const itCall of itCalls) { const optsNode = itCall.getMatch("OPTS"); if (optsNode && optsNode.kind() === "object") { const testId = extractIdFromObject(optsNode); if (testId) { testIds.push(testId); } } } return { suiteId, testIds }; } /** * Replaces the suite ID in a Moonwall test file. * Returns the modified file content, or undefined if no describeSuite was found. */ export function replaceSuiteId(fileContent, newId) { const ast = parse(Lang.TypeScript, fileContent); const root = ast.root(); // Find describeSuite({ id: "...", ... }) const describeSuiteCall = root.find("describeSuite($OPTS)"); if (!describeSuiteCall) { return undefined; } const optsNode = describeSuiteCall.getMatch("OPTS"); if (!optsNode || optsNode.kind() !== "object") { return undefined; } // Find the id property value node const idValueNode = findIdValueNode(optsNode); if (!idValueNode) { return undefined; } // Determine the quote style used in the original const originalText = idValueNode.text(); const quoteChar = originalText[0]; // Create edit to replace the ID value const edit = idValueNode.replace(`${quoteChar}${newId}${quoteChar}`); return root.commitEdits([edit]); } /** * Checks if a file contains a describeSuite call (is a Moonwall test file). */ export function hasSuiteDefinition(fileContent) { const ast = parse(Lang.TypeScript, fileContent); const root = ast.root(); return root.find("describeSuite($$$)") !== null; } /** * Finds all test files matching the pattern using ast-grep's parallel file search. * Uses Rust threads for efficient parsing and searching. * * @param testDirs - Directories to search for test files * @param includeGlobs - Glob patterns for files to include (e.g., ["*test*.ts", "*spec*.ts"]) * @param idPattern - Regex pattern to match against suite/test IDs * @returns Promise resolving to array of matching file paths */ export async function findTestFilesMatchingPattern(testDirs, includeGlobs, idPattern) { const matches = []; let processedCount = 0; let expectedCount; await new Promise((resolve, reject) => { findInFiles(Lang.TypeScript, { paths: testDirs, matcher: { rule: { pattern: "describeSuite($OPTS)" } }, languageGlobs: includeGlobs, }, (err, nodes) => { if (err) { reject(err); return; } if (nodes.length === 0) return; // Get file info from the first node const node = nodes[0]; const filePath = node.getRoot().filename(); const root = node.getRoot().root(); // Extract suite ID from the describeSuite call const optsNode = node.getMatch("OPTS"); if (!optsNode || optsNode.kind() !== "object") return; const suiteId = extractIdFromObject(optsNode); if (!suiteId) return; // Extract test IDs const testIds = []; const itCalls = root.findAll("it($OPTS)"); for (const itCall of itCalls) { const itOpts = itCall.getMatch("OPTS"); if (itOpts && itOpts.kind() === "object") { const testId = extractIdFromObject(itOpts); if (testId) testIds.push(testId); } } // Check if any ID matches the pattern const allIds = [suiteId, ...testIds.map((tid) => suiteId + tid)]; if (allIds.some((id) => idPattern.test(id))) { matches.push(filePath); } processedCount++; if (expectedCount !== undefined && processedCount >= expectedCount) { resolve(); } }) .then((count) => { expectedCount = count; // If all files already processed or no files found, resolve immediately if (count === 0 || processedCount >= count) { resolve(); } }) .catch(reject); }); return matches; } //# sourceMappingURL=testIdParser.js.map