Initial open-source release

This commit is contained in:
Will Chen
2025-04-11 09:37:05 -07:00
commit 43f67e0739
208 changed files with 45476 additions and 0 deletions

218
src/utils/codebase.ts Normal file
View File

@@ -0,0 +1,218 @@
import fs from "node:fs";
import path from "node:path";
import { isIgnored } from "isomorphic-git";
// File extensions to include in the extraction
const ALLOWED_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".css", ".html"];
// Directories to always exclude
const EXCLUDED_DIRS = ["node_modules", ".git", "dist", "build"];
// Files to always include, regardless of extension
const ALWAYS_INCLUDE_FILES = ["package.json"];
// Maximum file size to include (in bytes) - 100KB
const MAX_FILE_SIZE = 100 * 1024;
/**
* Check if a path should be ignored based on git ignore rules
*/
async function isGitIgnored(
filePath: string,
baseDir: string
): Promise<boolean> {
try {
const relativePath = path.relative(baseDir, filePath);
return await isIgnored({ fs, dir: baseDir, filepath: relativePath });
} catch (error) {
console.error(`Error checking if path is git ignored: ${filePath}`, error);
return false;
}
}
/**
* Recursively walk a directory and collect all relevant files
*/
async function collectFiles(
dir: string,
baseDir: string,
maxFiles = 100
): Promise<string[]> {
const files: string[] = [];
// Stop if we've reached the file limit
if (files.length >= maxFiles) {
return files;
}
// Check if directory exists
if (!fs.existsSync(dir)) {
return files;
}
try {
// Read directory contents
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
// Stop if we've reached the file limit
if (files.length >= maxFiles) {
break;
}
const fullPath = path.join(dir, entry.name);
// Skip excluded directories
if (entry.isDirectory() && EXCLUDED_DIRS.includes(entry.name)) {
continue;
}
// Skip if the entry is git ignored
if (await isGitIgnored(fullPath, baseDir)) {
continue;
}
if (entry.isDirectory()) {
// Recursively process subdirectories
const subDirFiles = await collectFiles(
fullPath,
baseDir,
maxFiles - files.length
);
files.push(...subDirFiles);
} else if (entry.isFile()) {
// Check file extension and filename
const ext = path.extname(entry.name).toLowerCase();
const shouldAlwaysInclude = ALWAYS_INCLUDE_FILES.includes(entry.name);
// Skip files that are too large
try {
const stats = fs.statSync(fullPath);
if (stats.size > MAX_FILE_SIZE) {
continue;
}
} catch (error) {
console.error(`Error checking file size: ${fullPath}`, error);
continue;
}
if (ALLOWED_EXTENSIONS.includes(ext) || shouldAlwaysInclude) {
files.push(fullPath);
}
}
}
} catch (error) {
console.error(`Error reading directory ${dir}:`, error);
}
return files;
}
/**
* Format a file for inclusion in the codebase extract
*/
function formatFile(filePath: string, baseDir: string): string {
try {
const relativePath = path.relative(baseDir, filePath);
// Skip large configuration files or generated code (just include the path)
if (
relativePath.includes(path.join("src", "components", "ui")) ||
relativePath.includes("eslint.config") ||
relativePath.includes("tsconfig.json") ||
relativePath.includes("package-lock.json") ||
// These should already be excluded based on file type, but
// just in case, we'll redact the contents here.
relativePath.includes(".env")
) {
return `<dyad-file path="${relativePath}">
// Contents omitted for brevity
</dyad-file>
`;
}
const content = fs.readFileSync(filePath, "utf-8");
return `<dyad-file path="${relativePath}">
${content}
</dyad-file>
`;
} catch (error) {
console.error(`Error reading file: ${filePath}`, error);
return `<dyad-file path="${path.relative(baseDir, filePath)}">
// Error reading file: ${error}
</dyad-file>
`;
}
}
/**
* Extract and format codebase files as a string to be included in prompts
* @param appPath - Path to the codebase to extract
* @param maxFiles - Maximum number of files to include (default: 30)
* @returns A string containing formatted file contents
*/
export async function extractCodebase(
appPath: string,
maxFiles = 30
): Promise<string> {
if (!fs.existsSync(appPath)) {
return `# Error: Directory ${appPath} does not exist`;
}
// Collect all relevant files
const files = await collectFiles(appPath, appPath, maxFiles);
// Sort files to prioritize important files
const sortedFiles = sortFilesByImportance(files, appPath);
// Format files
let output = "";
for (const file of sortedFiles) {
output += formatFile(file, appPath);
}
return output;
}
/**
* Sort files by their importance for context
*/
function sortFilesByImportance(files: string[], baseDir: string): string[] {
// Define patterns for important files
const highPriorityPatterns = [
new RegExp(`(^|/)${ALWAYS_INCLUDE_FILES[0]}$`),
/tsconfig\.json$/,
/README\.md$/,
/index\.(ts|js)x?$/,
/main\.(ts|js)x?$/,
/app\.(ts|js)x?$/,
];
// Custom sorting function
return [...files].sort((a, b) => {
const relativeA = path.relative(baseDir, a);
const relativeB = path.relative(baseDir, b);
// Check if file A matches any high priority pattern
const aIsHighPriority = highPriorityPatterns.some((pattern) =>
pattern.test(relativeA)
);
// Check if file B matches any high priority pattern
const bIsHighPriority = highPriorityPatterns.some((pattern) =>
pattern.test(relativeB)
);
// Sort by priority first
if (aIsHighPriority && !bIsHighPriority) return -1;
if (!aIsHighPriority && bIsHighPriority) return 1;
// If both are same priority, sort alphabetically
return relativeA.localeCompare(relativeB);
});
}