Spaces:
Running
Running
| import * as fs from 'node:fs'; | |
| import * as path from 'node:path'; | |
| import { validateDashboard } from '../lib/dashboard-validator.js'; | |
| import { log } from './logger.js'; | |
| /** Directories to skip when recursively scanning for JSON files */ | |
| const SKIP_DIRS = new Set([ | |
| 'node_modules', '.git', 'dist', 'build', '.next', '.cache', | |
| '__pycache__', '.turbo', 'target', 'obj', | |
| ]); | |
| function synthesizeMeta() { | |
| return { | |
| lastAnalyzedAt: new Date().toISOString(), | |
| gitCommitHash: '', | |
| version: '0.0.0', | |
| analyzedFiles: 0, | |
| }; | |
| } | |
| function extractProjectName(data) { | |
| if (data.project && | |
| typeof data.project === 'object' && | |
| !Array.isArray(data.project) && | |
| 'name' in data.project && | |
| typeof data.project.name === 'string') { | |
| return data.project.name; | |
| } | |
| return null; | |
| } | |
| function tryParseJsonFile(filePath) { | |
| try { | |
| const content = fs.readFileSync(filePath, 'utf-8'); | |
| const parsed = JSON.parse(content); | |
| if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) { | |
| return { error: 'not a JSON object' }; | |
| } | |
| return { data: parsed }; | |
| } | |
| catch (e) { | |
| const message = e instanceof Error ? e.message : String(e); | |
| return { error: message }; | |
| } | |
| } | |
| function handleSingleFile(absolutePath) { | |
| const parseResult = tryParseJsonFile(absolutePath); | |
| if ('error' in parseResult) { | |
| return { success: false, error: `Failed to parse ${path.basename(absolutePath)}: ${parseResult.error}` }; | |
| } | |
| const validation = validateDashboard(parseResult.data); | |
| if (!validation.valid) { | |
| return { | |
| success: false, | |
| error: `${path.basename(absolutePath)} is not a valid dashboard: ${validation.errors.join(', ')}`, | |
| }; | |
| } | |
| const projectName = extractProjectName(parseResult.data) || path.basename(absolutePath, '.json'); | |
| const dirName = path.basename(path.dirname(absolutePath)); | |
| const fileName = path.basename(absolutePath); | |
| const manifest = [ | |
| { | |
| dirName, | |
| meta: synthesizeMeta(), | |
| graphFiles: [{ fileName, projectName }], | |
| }, | |
| ]; | |
| const fileMapping = { | |
| [`${dirName}/${fileName}`]: absolutePath, | |
| }; | |
| return { success: true, manifest, fileMapping }; | |
| } | |
| function handleProjectDirectory(dirPath, dirName) { | |
| const metaPath = path.join(dirPath, 'meta.json'); | |
| const metaResult = tryParseJsonFile(metaPath); | |
| let meta; | |
| if ('error' in metaResult) { | |
| log('warn', `Skipping ${dirName}/meta.json: ${metaResult.error}`); | |
| return null; | |
| } | |
| meta = metaResult.data; | |
| const graphFiles = []; | |
| const fileMapping = {}; | |
| // Recursively collect all valid graph .json files from this directory and subdirectories | |
| function scanDir(currentPath, relativePrefix) { | |
| let entries; | |
| try { | |
| entries = fs.readdirSync(currentPath); | |
| } | |
| catch { | |
| return; | |
| } | |
| for (const entry of entries) { | |
| const fullPath = path.join(currentPath, entry); | |
| let stat; | |
| try { | |
| stat = fs.statSync(fullPath); | |
| } | |
| catch { | |
| continue; | |
| } | |
| if (stat.isDirectory()) { | |
| // Skip common non-source directories | |
| if (SKIP_DIRS.has(entry)) | |
| continue; | |
| // Recurse into subdirectory | |
| const childPrefix = relativePrefix ? `${relativePrefix}/${entry}` : entry; | |
| scanDir(fullPath, childPrefix); | |
| } | |
| else if (stat.isFile() && entry.endsWith('.json') && entry !== 'meta.json') { | |
| const parseResult = tryParseJsonFile(fullPath); | |
| if ('error' in parseResult) { | |
| continue; // Not valid JSON β silently skip | |
| } | |
| const validation = validateDashboard(parseResult.data); | |
| if (!validation.valid) { | |
| continue; // Not a dashboard graph file β silently skip | |
| } | |
| const projectName = extractProjectName(parseResult.data) || path.basename(entry, '.json'); | |
| // Use relative path from the project directory as the fileName for nested files | |
| const relativeFileName = relativePrefix ? `${relativePrefix}/${entry}` : entry; | |
| graphFiles.push({ fileName: relativeFileName, projectName }); | |
| fileMapping[`${dirName}/${relativeFileName}`] = fullPath; | |
| } | |
| } | |
| } | |
| scanDir(dirPath, ''); | |
| if (graphFiles.length === 0) { | |
| log('warn', `Skipping ${dirName}: no valid graph files`); | |
| return null; | |
| } | |
| return { | |
| entry: { dirName, meta, graphFiles }, | |
| fileMapping, | |
| }; | |
| } | |
| /** | |
| * Recursively collects all directories containing .json files, | |
| * skipping common non-source directories. | |
| * Returns a map of relative directory path β absolute directory path. | |
| */ | |
| function collectJsonDirs(basePath, relativePath = '') { | |
| const result = new Map(); | |
| const currentPath = relativePath ? path.join(basePath, relativePath) : basePath; | |
| let entries; | |
| try { | |
| entries = fs.readdirSync(currentPath); | |
| } | |
| catch { | |
| return result; | |
| } | |
| // Check if this directory has any .json files | |
| let hasJsonFiles = false; | |
| for (const entry of entries) { | |
| if (entry.endsWith('.json')) { | |
| const filePath = path.join(currentPath, entry); | |
| try { | |
| const stat = fs.statSync(filePath); | |
| if (stat.isFile()) { | |
| hasJsonFiles = true; | |
| break; | |
| } | |
| } | |
| catch { | |
| // skip | |
| } | |
| } | |
| } | |
| if (hasJsonFiles) { | |
| result.set(relativePath, currentPath); | |
| } | |
| // Recurse into subdirectories | |
| for (const entry of entries) { | |
| if (SKIP_DIRS.has(entry)) | |
| continue; | |
| const entryPath = path.join(currentPath, entry); | |
| try { | |
| const stat = fs.statSync(entryPath); | |
| if (stat.isDirectory()) { | |
| const childRelative = relativePath ? `${relativePath}/${entry}` : entry; | |
| const childResults = collectJsonDirs(basePath, childRelative); | |
| for (const [rel, abs] of childResults) { | |
| result.set(rel, abs); | |
| } | |
| } | |
| } | |
| catch { | |
| // skip | |
| } | |
| } | |
| return result; | |
| } | |
| function handleFlatDirectory(dirPath) { | |
| const dirName = path.basename(dirPath); | |
| const manifest = []; | |
| const fileMapping = {}; | |
| // Recursively collect all directories with .json files | |
| const jsonDirs = collectJsonDirs(dirPath); | |
| for (const [relativeDirPath, absoluteDirPath] of jsonDirs) { | |
| const entries = fs.readdirSync(absoluteDirPath); | |
| const graphFiles = []; | |
| for (const entry of entries) { | |
| if (!entry.endsWith('.json')) | |
| continue; | |
| const filePath = path.join(absoluteDirPath, entry); | |
| try { | |
| const stat = fs.statSync(filePath); | |
| if (!stat.isFile()) | |
| continue; | |
| } | |
| catch { | |
| continue; | |
| } | |
| const parseResult = tryParseJsonFile(filePath); | |
| if ('error' in parseResult) { | |
| continue; // Not valid JSON β silently skip | |
| } | |
| const validation = validateDashboard(parseResult.data); | |
| if (!validation.valid) { | |
| continue; // Not a dashboard graph file β silently skip | |
| } | |
| const projectName = extractProjectName(parseResult.data) || path.basename(entry, '.json'); | |
| graphFiles.push({ fileName: entry, projectName }); | |
| // Build the file mapping key: for top-level files use dirName/fileName, | |
| // for nested files use dirName/relativePath/fileName | |
| const mappingKey = relativeDirPath | |
| ? `${dirName}/${relativeDirPath}/${entry}` | |
| : `${dirName}/${entry}`; | |
| fileMapping[mappingKey] = filePath; | |
| } | |
| if (graphFiles.length > 0) { | |
| // Use relative path as the manifest entry dirName for nested dirs, | |
| // or the base dirName for top-level files | |
| const entryDirName = relativeDirPath | |
| ? `${dirName}/${relativeDirPath}` | |
| : dirName; | |
| manifest.push({ | |
| dirName: entryDirName, | |
| meta: synthesizeMeta(), | |
| graphFiles, | |
| }); | |
| } | |
| } | |
| if (manifest.length === 0) { | |
| return { success: false, error: `No valid dashboard files found in ${dirPath}` }; | |
| } | |
| return { success: true, manifest, fileMapping }; | |
| } | |
| /** | |
| * Recursively finds all directories containing a meta.json file, | |
| * returning their paths relative to the base directory. | |
| * Skips common non-source directories. | |
| */ | |
| function findDirsWithMetaJson(basePath, currentPath) { | |
| const results = []; | |
| let entries; | |
| try { | |
| entries = fs.readdirSync(currentPath); | |
| } | |
| catch { | |
| return results; | |
| } | |
| for (const entry of entries) { | |
| if (SKIP_DIRS.has(entry)) | |
| continue; | |
| const entryPath = path.join(currentPath, entry); | |
| try { | |
| const stat = fs.statSync(entryPath); | |
| if (!stat.isDirectory()) | |
| continue; | |
| if (fs.existsSync(path.join(entryPath, 'meta.json'))) { | |
| // Use relative path from basePath | |
| const relativePath = path.relative(basePath, entryPath); | |
| results.push(relativePath); | |
| } | |
| // Continue recursing to find deeper directories with meta.json | |
| const childResults = findDirsWithMetaJson(basePath, entryPath); | |
| results.push(...childResults); | |
| } | |
| catch { | |
| // Skip entries that can't be stat'd | |
| } | |
| } | |
| return results; | |
| } | |
| function handleNestedDirectory(dirPath, subdirs) { | |
| const manifest = []; | |
| const fileMapping = {}; | |
| for (const subdir of subdirs) { | |
| const subdirPath = path.join(dirPath, subdir); | |
| // Use the relative path as dirName (handles nested paths like "a/b/c") | |
| const dirName = subdir.replace(/\\/g, '/'); | |
| const result = handleProjectDirectory(subdirPath, dirName); | |
| if (result) { | |
| manifest.push(result.entry); | |
| Object.assign(fileMapping, result.fileMapping); | |
| } | |
| } | |
| if (manifest.length === 0) { | |
| return { success: false, error: `No valid dashboard files found in ${dirPath}` }; | |
| } | |
| return { success: true, manifest, fileMapping }; | |
| } | |
| export function resolveInput(inputPath) { | |
| const absolutePath = path.resolve(inputPath); | |
| let stat; | |
| try { | |
| stat = fs.statSync(absolutePath); | |
| } | |
| catch { | |
| return { success: false, error: `Path not found: ${inputPath}` }; | |
| } | |
| // Case 1: Single file | |
| if (stat.isFile()) { | |
| return handleSingleFile(absolutePath); | |
| } | |
| // Case 2: Directory | |
| if (stat.isDirectory()) { | |
| // Check if directory itself has meta.json β project directory | |
| const hasMetaJson = fs.existsSync(path.join(absolutePath, 'meta.json')); | |
| if (hasMetaJson) { | |
| const dirName = path.basename(absolutePath); | |
| const result = handleProjectDirectory(absolutePath, dirName); | |
| if (!result) { | |
| return { success: false, error: `No valid dashboard files found in ${absolutePath}` }; | |
| } | |
| return { success: true, manifest: [result.entry], fileMapping: result.fileMapping }; | |
| } | |
| // Recursively check for subdirectories containing meta.json β nested directory | |
| const subdirsWithMeta = findDirsWithMetaJson(absolutePath, absolutePath); | |
| if (subdirsWithMeta.length > 0) { | |
| return handleNestedDirectory(absolutePath, subdirsWithMeta); | |
| } | |
| // Flat directory: has .json files but no meta.json, no nested projects | |
| return handleFlatDirectory(absolutePath); | |
| } | |
| return { success: false, error: `Path is not a file or directory: ${inputPath}` }; | |
| } | |