UNPKG

aios-core

Version:

Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework

397 lines (323 loc) 11.2 kB
/** * Import Path Update Module * * Scans and updates require/import paths after migration. * * @module cli/commands/migrate/update-imports * @version 1.0.0 * @story 2.14 - Migration Script v2.0 → v4.0.4 */ const fs = require('fs'); const path = require('path'); const { MODULE_MAPPING, categorizeFile } = require('./analyze'); /** * Regex patterns for import detection */ const IMPORT_PATTERNS = { // require('./path') or require('../path') commonjs: /require\s*\(\s*['"`](\.[^'"`]+)['"`]\s*\)/g, // import from './path' or import from '../path' esm: /from\s+['"`](\.[^'"`]+)['"`]/g, // import('./path') dynamic imports dynamicImport: /import\s*\(\s*['"`](\.[^'"`]+)['"`]\s*\)/g, // /// <reference path="./path" /> tsReference: /\/\/\/\s*<reference\s+path=['"`](\.[^'"`]+)['"`]\s*\/>/g, }; /** * Build path transformation map for the migration * @param {Object} plan - Migration plan * @returns {Map<string, string>} Old path to new path map */ function buildPathTransformMap(plan) { const transformMap = new Map(); for (const [moduleName, moduleData] of Object.entries(plan.modules)) { for (const file of moduleData.files) { // Store both with and without extension const oldPath = file.relativePath; const newPath = path.join(moduleName, file.relativePath); transformMap.set(oldPath, newPath); // Also store without .js extension for require compatibility if (oldPath.endsWith('.js')) { const noExt = oldPath.slice(0, -3); const newNoExt = newPath.slice(0, -3); transformMap.set(noExt, newNoExt); } } } return transformMap; } /** * Scan a file for import statements * @param {string} filePath - Path to file * @returns {Promise<Object[]>} Array of import info */ async function scanFileImports(filePath) { const content = await fs.promises.readFile(filePath, 'utf8'); const imports = []; for (const [patternName, pattern] of Object.entries(IMPORT_PATTERNS)) { // Reset regex lastIndex pattern.lastIndex = 0; let match; while ((match = pattern.exec(content)) !== null) { imports.push({ type: patternName, fullMatch: match[0], importPath: match[1], index: match.index, length: match[0].length, }); } } return imports; } /** * Transform an import path from v2.0 to v4.0.4 structure * @param {string} importPath - Original import path * @param {string} currentFileModule - Module of the file containing the import * @param {string} currentFilePath - Path of the file containing the import * @param {Map} transformMap - Path transformation map * @returns {string|null} Transformed path or null if no change needed */ function transformImportPath(importPath, currentFileModule, currentFilePath, transformMap) { // Resolve the import path relative to the current file const currentDir = path.dirname(currentFilePath); const resolvedPath = path.normalize(path.join(currentDir, importPath)); // Check if this path needs transformation // We need to detect if it's pointing to a file that moved // For paths starting with ../, they might reference files now in different modules if (importPath.startsWith('../') || importPath.startsWith('./')) { // This is a complex transformation - need to figure out what file it's importing // and where that file now lives // For now, we'll use heuristics: // 1. If the path goes up to .aios-core root and then into a folder, update it const parts = importPath.split('/'); let upCount = 0; const restParts = []; for (const part of parts) { if (part === '..') { upCount++; } else if (part !== '.') { restParts.push(part); } } // If going up enough to reach .aios-core root if (restParts.length > 0) { const targetDir = restParts[0]; // Find which module this directory belongs to for (const [moduleName, config] of Object.entries(MODULE_MAPPING)) { if (config.directories.includes(targetDir)) { // This needs to be updated to go through the module directory // New path: go up, then into module, then the rest // Calculate how to get from current location to target const newParts = []; // From current file's module directory // Current structure after migration: .aios-core/{module}/... // Need to go to: .aios-core/{targetModule}/... if (currentFileModule === moduleName) { // Same module, just adjust the path return importPath; // No change needed within same module } else { // Different module, need to go up and into the other module // Example: from development/agents/ to core/registry/ // Need: ../../core/registry/... const currentDepth = currentFilePath.split(path.sep).length - 1; const ups = '../'.repeat(currentDepth + 1); // +1 to get out of module dir return `${ups}${moduleName}/${restParts.join('/')}`; } } } } } return null; // No transformation needed } /** * Update imports in a single file * @param {string} filePath - Path to file * @param {Map} transformMap - Path transformation map * @param {string} fileModule - Module this file belongs to * @param {Object} options - Options * @returns {Promise<Object>} Update result */ async function updateFileImports(filePath, transformMap, fileModule, options = {}) { const { dryRun = false, verbose = false } = options; const result = { file: filePath, imports: [], updated: 0, unchanged: 0, errors: [], }; try { let content = await fs.promises.readFile(filePath, 'utf8'); const originalContent = content; const imports = await scanFileImports(filePath); for (const imp of imports) { const relativePath = path.relative( path.dirname(filePath).split('.aios-core')[1]?.slice(1) || '', '', ); const newPath = transformImportPath( imp.importPath, fileModule, filePath, transformMap, ); if (newPath && newPath !== imp.importPath) { // Replace in content const oldStatement = imp.fullMatch; const newStatement = oldStatement.replace(imp.importPath, newPath); content = content.replace(oldStatement, newStatement); result.imports.push({ old: imp.importPath, new: newPath, type: imp.type, }); result.updated++; } else { result.unchanged++; } } // Write updated content if changes were made if (content !== originalContent && !dryRun) { await fs.promises.writeFile(filePath, content, 'utf8'); } result.modified = content !== originalContent; } catch (error) { result.errors.push(error.message); } return result; } /** * Scan and update all imports in the migrated structure * @param {string} aiosCoreDir - Path to .aios-core * @param {Object} plan - Migration plan * @param {Object} options - Options * @returns {Promise<Object>} Update results */ async function updateAllImports(aiosCoreDir, plan, options = {}) { const { verbose = false, dryRun = false, onProgress = () => {} } = options; const transformMap = buildPathTransformMap(plan); const result = { totalFiles: 0, filesModified: 0, importsUpdated: 0, errors: [], details: [], }; onProgress({ phase: 'scan', message: 'Scanning for import statements...' }); // Process each module const modules = ['core', 'development', 'product', 'infrastructure']; for (const moduleName of modules) { const moduleDir = path.join(aiosCoreDir, moduleName); if (!fs.existsSync(moduleDir)) continue; // Get all JS/TS files in module const files = await getJsFiles(moduleDir); for (const file of files) { result.totalFiles++; const updateResult = await updateFileImports( file, transformMap, moduleName, { dryRun, verbose }, ); if (updateResult.modified) { result.filesModified++; } result.importsUpdated += updateResult.updated; if (updateResult.errors.length > 0) { result.errors.push(...updateResult.errors.map(e => ({ file, error: e, }))); } if (verbose && updateResult.updated > 0) { result.details.push(updateResult); onProgress({ phase: 'file', message: ` → ${path.relative(aiosCoreDir, file)} (${updateResult.updated} imports)`, }); } } } onProgress({ phase: 'complete', message: `Found ${result.importsUpdated} imports to update`, }); return result; } /** * Get all JavaScript/TypeScript files in a directory recursively * @param {string} dir - Directory path * @param {string[]} [files] - Accumulated files * @returns {Promise<string[]>} Array of file paths */ async function getJsFiles(dir, files = []) { const entries = await fs.promises.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { await getJsFiles(fullPath, files); } else if (/\.(js|ts|mjs|cjs|jsx|tsx)$/.test(entry.name)) { files.push(fullPath); } } return files; } /** * Verify no broken imports exist after migration * @param {string} aiosCoreDir - Path to .aios-core * @returns {Promise<Object>} Verification result */ async function verifyImports(aiosCoreDir) { const result = { valid: true, totalImports: 0, brokenImports: [], warnings: [], }; const modules = ['core', 'development', 'product', 'infrastructure']; for (const moduleName of modules) { const moduleDir = path.join(aiosCoreDir, moduleName); if (!fs.existsSync(moduleDir)) continue; const files = await getJsFiles(moduleDir); for (const file of files) { const imports = await scanFileImports(file); for (const imp of imports) { result.totalImports++; // Try to resolve the import if (imp.importPath.startsWith('.')) { const resolvedPath = path.resolve(path.dirname(file), imp.importPath); // Check various extensions const extensions = ['', '.js', '.ts', '.json', '/index.js', '/index.ts']; let found = false; for (const ext of extensions) { if (fs.existsSync(resolvedPath + ext)) { found = true; break; } } if (!found) { result.brokenImports.push({ file: path.relative(aiosCoreDir, file), import: imp.importPath, type: imp.type, resolvedPath, }); result.valid = false; } } } } } return result; } module.exports = { IMPORT_PATTERNS, buildPathTransformMap, scanFileImports, transformImportPath, updateFileImports, updateAllImports, getJsFiles, verifyImports, };