diff --git a/backend/src/build-system/__tests__/test-file-create-and-path.spec.ts b/backend/src/build-system/__tests__/test-file-create-and-path.spec.ts new file mode 100644 index 00000000..1a9513c4 --- /dev/null +++ b/backend/src/build-system/__tests__/test-file-create-and-path.spec.ts @@ -0,0 +1,86 @@ +import * as path from 'path'; +import * as os from 'os'; +import { existsSync, rmdirSync } from 'fs-extra'; +import * as pathUtil from '../../config/common-path'; +import { saveGeneratedCode } from 'src/build-system/utils/files'; +import { + getRootDir, + getProjectsDir, + getProjectPath, +} from 'src/config/common-path'; + +describe('Path Utilities', () => { + const APP_NAME = 'codefox'; + const ROOT_DIR = path.join(os.homedir(), `.${APP_NAME}`); + + const cleanUp = () => { + // if (existsSync(ROOT_DIR)) { + // rmdirSync(ROOT_DIR, { recursive: true }); + // } + }; + + beforeEach(() => { + cleanUp(); + }); + + afterAll(() => { + cleanUp(); + }); + + it('should return a valid root directory', () => { + const rootDir = getRootDir(); + expect(rootDir).toBeDefined(); + expect(rootDir).toContain('.codefox'); + }); + + it('should return a valid projects directory', () => { + const projectsDir = getProjectsDir(); + expect(projectsDir).toBeDefined(); + expect(projectsDir).toContain('projects'); + }); + + it('should return a valid project path for a given ID', () => { + const projectId = 'test-project'; + const projectPath = getProjectPath(projectId); + expect(projectPath).toBeDefined(); + expect(projectPath).toContain(projectId); + }); + + it('should resolve paths correctly', () => { + const rootDir = pathUtil.getRootDir(); + const projectsDir = pathUtil.getProjectsDir(); + expect(rootDir).toBeDefined(); + expect(projectsDir).toBeDefined(); + }); + + it('should create and return the root directory', async () => { + const rootDir = pathUtil.getRootDir(); + + await generateAndSaveCode(); + expect(rootDir).toBe(ROOT_DIR); + expect(existsSync(ROOT_DIR)).toBe(true); + }); +}); + +async function generateAndSaveCode() { + const generatedCode = ` + import { Controller, Get } from '@nestjs/common'; + + @Controller('example') + export class ExampleController { + @Get() + getHello(): string { + return 'Hello World!'; + } + } + `; + + const fileName = 'example.controller.ts'; + + try { + const filePath = await saveGeneratedCode(fileName, generatedCode); + console.log(`Generated code saved at: ${filePath}`); + } catch (error) { + console.error('Failed to save generated code:', error); + } +} diff --git a/backend/src/build-system/context.ts b/backend/src/build-system/context.ts index cb98691d..c1b8883b 100644 --- a/backend/src/build-system/context.ts +++ b/backend/src/build-system/context.ts @@ -1,4 +1,3 @@ -import { ModelProvider } from 'src/common/model-provider'; import { BuildHandlerManager } from './hanlder-manager'; import { BuildExecutionState, @@ -8,8 +7,15 @@ import { } from './types'; import { Logger } from '@nestjs/common'; import { VirtualDirectory } from './virtual-dir'; +import { ModelProvider } from 'src/common/model-provider'; -export type GlobalDataKeys = 'projectName' | 'description' | 'platform'; +import { v4 as uuidv4 } from 'uuid'; + +export type GlobalDataKeys = + | 'projectName' + | 'description' + | 'platform' + | 'projectUUID'; type ContextData = { [key in GlobalDataKeys]: string; } & Record; @@ -37,6 +43,9 @@ export class BuilderContext { this.model = ModelProvider.getInstance(); this.logger = new Logger(`builder-context-${id}`); this.virtualDirectory = new VirtualDirectory(); + + const projectUUID = uuidv4(); + this.setData('projectUUID', projectUUID); } canExecute(nodeId: string): boolean { diff --git a/backend/src/build-system/handlers/file-manager/file-arch/index.ts b/backend/src/build-system/handlers/file-manager/file-arch/index.ts index 840b1fc8..838c4dad 100644 --- a/backend/src/build-system/handlers/file-manager/file-arch/index.ts +++ b/backend/src/build-system/handlers/file-manager/file-arch/index.ts @@ -2,7 +2,7 @@ import { BuildHandler, BuildResult } from 'src/build-system/types'; import { BuilderContext } from 'src/build-system/context'; import { generateFileArchPrompt } from './prompt'; import { Logger } from '@nestjs/common'; -import { FileUtil } from 'src/build-system/utils/util'; +import { extractJsonFromMarkdown } from 'src/build-system/utils/strings'; export class FileArchGenerateHandler implements BuildHandler { readonly id = 'op:FILE_ARCH::STATE:GENERATE'; @@ -50,7 +50,7 @@ export class FileArchGenerateHandler implements BuildHandler { ); // validation test - jsonData = FileUtil.extractJsonFromMarkdown(fileArchContent); + jsonData = extractJsonFromMarkdown(fileArchContent); if (jsonData == null) { retry += 1; this.logger.error('Extract Json From Markdown fail'); diff --git a/backend/src/build-system/handlers/file-manager/file-generate/index.ts b/backend/src/build-system/handlers/file-manager/file-generate/index.ts index 8a5a34ed..7fbb26ff 100644 --- a/backend/src/build-system/handlers/file-manager/file-generate/index.ts +++ b/backend/src/build-system/handlers/file-manager/file-generate/index.ts @@ -5,18 +5,22 @@ import * as toposort from 'toposort'; import { VirtualDirectory } from '../../../virtual-dir'; import { BuilderContext } from 'src/build-system/context'; import { BuildHandler, BuildResult } from 'src/build-system/types'; -import { FileUtil } from 'src/build-system/utils/util'; +import { extractJsonFromMarkdown } from 'src/build-system/utils/strings'; +import { getProjectPath } from 'src/config/common-path'; +import * as normalizePath from 'normalize-path'; -export class FileGeneratorHandler { +export class FileGeneratorHandler implements BuildHandler { + readonly id = 'op:FILE_GENERATE::STATE:GENERATE'; private readonly logger = new Logger('FileGeneratorHandler'); private virtualDir: VirtualDirectory; async run(context: BuilderContext, args: unknown): Promise { this.virtualDir = context.virtualDirectory; const fileArch = args[0] as string; + const uuid = context.getData('projectUUID'); + + const projectSrcPath = getProjectPath(uuid); - // change here - const projectSrcPath = ''; this.generateFiles(JSON.stringify(fileArch, null, 2), projectSrcPath); return { @@ -34,7 +38,7 @@ export class FileGeneratorHandler { markdownContent: string, projectSrcPath: string, ): Promise<{ success: boolean; data: string }> { - const jsonData = FileUtil.extractJsonFromMarkdown(markdownContent); + const jsonData = extractJsonFromMarkdown(markdownContent); // Build the dependency graph and detect cycles before any file operations const { graph, nodes } = this.buildDependencyGraph(jsonData); this.detectCycles(graph); @@ -47,7 +51,7 @@ export class FileGeneratorHandler { // Generate files in the correct order for (const file of sortedFiles) { - const fullPath = path.resolve(projectSrcPath, file); + const fullPath = normalizePath(path.resolve(projectSrcPath, file)); this.logger.log(`Generating file in dependency order: ${fullPath}`); // TODO(allen) await this.createFile(fullPath); diff --git a/backend/src/build-system/hanlder-manager.ts b/backend/src/build-system/hanlder-manager.ts index ee2ac723..5e1617f8 100644 --- a/backend/src/build-system/hanlder-manager.ts +++ b/backend/src/build-system/hanlder-manager.ts @@ -9,6 +9,7 @@ import { FileArchGenerateHandler } from './handlers/file-manager/file-arch'; import { BackendCodeHandler } from './handlers/backend/code-generate'; import { DBSchemaHandler } from './handlers/database/schemas/schemas'; import { DatabaseRequirementHandler } from './handlers/database/requirements-document'; +import { FileGeneratorHandler } from './handlers/file-manager/file-generate'; export class BuildHandlerManager { private static instance: BuildHandlerManager; @@ -30,6 +31,7 @@ export class BuildHandlerManager { new BackendCodeHandler(), new DBSchemaHandler(), new DatabaseRequirementHandler(), + new FileGeneratorHandler(), ]; for (const handler of builtInHandlers) { diff --git a/backend/src/build-system/utils/files.ts b/backend/src/build-system/utils/files.ts new file mode 100644 index 00000000..3a72318a --- /dev/null +++ b/backend/src/build-system/utils/files.ts @@ -0,0 +1,26 @@ +import { Logger } from '@nestjs/common'; +import fs from 'fs-extra'; + +const logger = new Logger('file-utils'); +/** + * Saves the given content to the specified file path using fs-extra. + * Ensures that all directories in the path exist before writing the file. + * + * @param filePath - The complete file path including the file name. + * @param content - The content to be written to the file. + * @returns The file path where the content was written. + * @throws Will throw an error if the file could not be written. + */ +export async function saveGeneratedCode( + filePath: string, + content: string, +): Promise { + try { + // fs-extra's outputFile creates all directories if they don't exist + await fs.outputFile(filePath, content, 'utf8'); + return filePath; + } catch (error) { + logger.error('Error saving generated code:', error); + throw error; + } +} diff --git a/backend/src/build-system/utils/strings.ts b/backend/src/build-system/utils/strings.ts new file mode 100644 index 00000000..6ba1a896 --- /dev/null +++ b/backend/src/build-system/utils/strings.ts @@ -0,0 +1,26 @@ +import { Logger } from '@nestjs/common'; + +const logger = new Logger('common-utils'); + +/** + * Extract JSON data from Markdown content. + * @param markdownContent The Markdown content containing the JSON. + */ +export function extractJsonFromMarkdown(markdownContent: string): { + files: Record; +} { + const jsonMatch = /([\s\S]*?)<\/GENERATEDCODE>/m.exec( + markdownContent, + ); + if (!jsonMatch) { + logger.error('No JSON found in the provided Markdown content.'); + return null; + } + + try { + return JSON.parse(jsonMatch[1]); + } catch (error) { + logger.error('Invalid JSON format in the Markdown content: ' + error); + return null; + } +} diff --git a/backend/src/build-system/utils/util.ts b/backend/src/build-system/utils/util.ts deleted file mode 100644 index 7283fedc..00000000 --- a/backend/src/build-system/utils/util.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Logger } from '@nestjs/common'; - -export class FileUtil { - private static readonly logger = new Logger('FileUtil'); - - /** - * Extract JSON data from Markdown content. - * @param markdownContent The Markdown content containing the JSON. - */ - static extractJsonFromMarkdown(markdownContent: string): { - files: Record; - } { - const jsonMatch = /([\s\S]*?)<\/GENERATEDCODE>/m.exec( - markdownContent, - ); - if (!jsonMatch) { - FileUtil.logger.error('No JSON found in the provided Markdown content.'); - return null; - } - - try { - return JSON.parse(jsonMatch[1]); - } catch (error) { - FileUtil.logger.error( - 'Invalid JSON format in the Markdown content: ' + error, - ); - return null; - } - } -}