embedia
Version:
Zero-configuration AI chatbot integration CLI - direct file copy with embedded API keys
263 lines (224 loc) • 8.81 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const { glob } = require('glob');
class ProjectAnalyzer {
async analyzeProject(projectPath) {
const analysis = {
framework: await this.detectFramework(projectPath),
routerType: await this.detectRouterType(projectPath),
structure: await this.mapProjectStructure(projectPath),
styling: await this.detectStylingSystem(projectPath),
packageManager: await this.detectPackageManager(projectPath),
typescript: await this.isTypeScriptProject(projectPath),
conflicts: await this.detectPotentialConflicts(projectPath),
reactVersion: await this.analyzeReactSetup(projectPath)
};
return analysis;
}
async readPackageJson(projectPath) {
try {
const packageJsonPath = path.join(projectPath, 'package.json');
if (await fs.pathExists(packageJsonPath)) {
return await fs.readJson(packageJsonPath);
}
} catch (error) {
console.error('Error reading package.json:', error);
}
return {};
}
async detectFramework(projectPath) {
const packageJson = await this.readPackageJson(projectPath);
if (packageJson.dependencies?.next || packageJson.devDependencies?.next) {
const version = packageJson.dependencies?.next || packageJson.devDependencies?.next;
return {
name: 'next',
version: version.replace(/[\^~]/, ''),
majorVersion: parseInt(version.match(/\d+/)[0])
};
}
// Check for other frameworks
if (packageJson.dependencies?.gatsby) return { name: 'gatsby', version: packageJson.dependencies.gatsby };
if (packageJson.dependencies?.['@remix-run/react']) return { name: 'remix', version: packageJson.dependencies['@remix-run/react'] };
if (packageJson.dependencies?.nuxt) return { name: 'nuxt', version: packageJson.dependencies.nuxt };
if (packageJson.dependencies?.vite && packageJson.dependencies?.react) return { name: 'vite', version: packageJson.dependencies.vite };
if (packageJson.dependencies?.['react-scripts']) return { name: 'create-react-app', version: packageJson.dependencies['react-scripts'] };
// Check for plain React
if (packageJson.dependencies?.react) return { name: 'react', version: packageJson.dependencies.react };
return { name: 'unknown', version: null };
}
async detectRouterType(projectPath) {
// For Next.js projects
const hasAppDir = await fs.pathExists(path.join(projectPath, 'app')) ||
await fs.pathExists(path.join(projectPath, 'src/app'));
const hasPagesDir = await fs.pathExists(path.join(projectPath, 'pages')) ||
await fs.pathExists(path.join(projectPath, 'src/pages'));
if (hasAppDir && !hasPagesDir) return 'app';
if (!hasAppDir && hasPagesDir) return 'pages';
if (hasAppDir && hasPagesDir) return 'hybrid';
return 'unknown';
}
async detectStylingSystem(projectPath) {
const systems = {
tailwind: false,
cssModules: false,
styledComponents: false,
emotion: false,
sass: false,
cssInJs: false
};
// Check for Tailwind
if (await fs.pathExists(path.join(projectPath, 'tailwind.config.js')) ||
await fs.pathExists(path.join(projectPath, 'tailwind.config.ts'))) {
systems.tailwind = true;
}
// Check package.json for styling libraries
const packageJson = await this.readPackageJson(projectPath);
if (packageJson.dependencies?.['styled-components']) systems.styledComponents = true;
if (packageJson.dependencies?.['@emotion/react']) systems.emotion = true;
if (packageJson.dependencies?.sass || packageJson.devDependencies?.sass) systems.sass = true;
// Check for CSS modules by looking at import patterns
const hasModules = await this.searchForPattern(projectPath, /\.module\.(css|scss|sass)/);
if (hasModules.length > 0) systems.cssModules = true;
return systems;
}
async detectPackageManager(projectPath) {
if (await fs.pathExists(path.join(projectPath, 'yarn.lock'))) return 'yarn';
if (await fs.pathExists(path.join(projectPath, 'pnpm-lock.yaml'))) return 'pnpm';
if (await fs.pathExists(path.join(projectPath, 'bun.lockb'))) return 'bun';
return 'npm';
}
async isTypeScriptProject(projectPath) {
const hasTsConfig = await fs.pathExists(path.join(projectPath, 'tsconfig.json'));
const packageJson = await this.readPackageJson(projectPath);
const hasTypeScript = !!(packageJson.devDependencies?.typescript || packageJson.dependencies?.typescript);
return {
isTypeScript: hasTsConfig || hasTypeScript,
hasTsConfig,
version: packageJson.devDependencies?.typescript || packageJson.dependencies?.typescript
};
}
async mapProjectStructure(projectPath) {
const structure = {
srcDir: await fs.pathExists(path.join(projectPath, 'src')),
componentsPath: null,
apiPath: null,
publicPath: null
};
// Find components directory
const componentPaths = ['components', 'src/components'];
for (const compPath of componentPaths) {
if (await fs.pathExists(path.join(projectPath, compPath))) {
structure.componentsPath = compPath;
break;
}
}
// Find API directory
const apiPaths = ['app/api', 'src/app/api', 'pages/api', 'src/pages/api'];
for (const apiPath of apiPaths) {
if (await fs.pathExists(path.join(projectPath, apiPath))) {
structure.apiPath = apiPath;
break;
}
}
// Find public directory
if (await fs.pathExists(path.join(projectPath, 'public'))) {
structure.publicPath = 'public';
}
return structure;
}
async searchForPattern(projectPath, pattern) {
const files = await glob('**/*.{js,jsx,ts,tsx}', {
cwd: projectPath,
ignore: ['node_modules/**', '.next/**', 'build/**', 'dist/**']
});
const matches = [];
for (const file of files) {
try {
const content = await fs.readFile(path.join(projectPath, file), 'utf-8');
if (pattern.test(content)) {
matches.push(file);
}
} catch (error) {
// Ignore read errors
}
}
return matches;
}
async detectPotentialConflicts(projectPath) {
const conflicts = [];
// Check for existing chat implementations
const chatPatterns = [
/import.*[Cc]hat/,
/[Cc]hatbot/,
/[Mm]essag(e|ing)/,
/[Cc]onversation/
];
for (const pattern of chatPatterns) {
const files = await this.searchForPattern(projectPath, pattern);
if (files.length > 0) {
conflicts.push({
type: 'existing_chat',
severity: 'warning',
message: 'Found existing chat implementation',
files: files.slice(0, 5) // Limit to first 5 matches
});
}
}
// Check for API route conflicts
const apiPaths = [
'app/api/chat',
'src/app/api/chat',
'pages/api/chat',
'app/api/embedia',
'pages/api/embedia'
];
for (const apiPath of apiPaths) {
if (await fs.pathExists(path.join(projectPath, apiPath))) {
conflicts.push({
type: 'api_route_conflict',
severity: 'error',
message: `API route already exists at ${apiPath}`,
path: apiPath
});
}
}
// Check for global style conflicts
const globalStyleFiles = ['globals.css', 'global.css', 'app.css'];
for (const styleFile of globalStyleFiles) {
const stylePath = path.join(projectPath, 'styles', styleFile);
if (await fs.pathExists(stylePath)) {
const content = await fs.readFile(stylePath, 'utf-8');
if (content.includes('position: fixed') && content.includes('z-index')) {
conflicts.push({
type: 'style_conflict',
severity: 'warning',
message: 'Global styles may conflict with chat widget',
file: `styles/${styleFile}`
});
}
}
}
return conflicts;
}
async analyzeReactSetup(projectPath) {
const packageJson = await this.readPackageJson(projectPath);
const react = packageJson.dependencies?.react || packageJson.devDependencies?.react;
if (!react) {
return {
hasReact: false,
requiresInstall: true
};
}
const version = react.replace(/[\^~]/, '');
const major = parseInt(version.split('.')[0]);
return {
hasReact: true,
version: version,
majorVersion: major,
isCompatible: major >= 18,
usesNewJSXTransform: major >= 17,
requiresInstall: false
};
}
}
module.exports = ProjectAnalyzer;