From 0d8900a18cade10f95a7a414e1e189f45a966574 Mon Sep 17 00:00:00 2001 From: ZHallen122 <106571949+ZHallen122@users.noreply.github.com> Date: Tue, 10 Dec 2024 22:07:17 -0500 Subject: [PATCH] feat(backend): adding file generator precheck with testing (#64) Co-authored-by: Jackson Chen <90215880+Sma1lboy@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../__tests__/datamap-structure.md | 275 ------------------ .../__tests__/db-requirement-document.md | 230 --------------- .../__tests__/file-structure-document.md | 77 ----- .../__tests__/test-file-create.spec.ts | 56 ++++ .../__tests__/test-generate-doc.spec.ts | 42 ++- .../__tests__/testVirtualDir.spec.ts | 58 ++++ backend/src/build-system/context.ts | 14 +- backend/src/build-system/hanlder-manager.ts | 2 + .../src/build-system/node/file-arch/index.ts | 90 +++++- .../build-system/node/file-generate/index.ts | 183 ++++++++++++ .../node/frontend-file-structure/index.ts | 54 +++- .../node/frontend-file-structure/prompt.ts | 64 +++- .../node/ux-sitemap-document/uxsmd.ts | 2 +- .../node/ux-sitemap-structure/index.ts | 13 +- .../node/ux-sitemap-structure/prompt.ts | 3 +- backend/src/build-system/util.ts | 30 ++ backend/src/build-system/virtual-dir.ts | 115 ++++++++ 17 files changed, 692 insertions(+), 616 deletions(-) delete mode 100644 backend/src/build-system/__tests__/datamap-structure.md delete mode 100644 backend/src/build-system/__tests__/db-requirement-document.md delete mode 100644 backend/src/build-system/__tests__/file-structure-document.md create mode 100644 backend/src/build-system/__tests__/test-file-create.spec.ts create mode 100644 backend/src/build-system/__tests__/testVirtualDir.spec.ts create mode 100644 backend/src/build-system/node/file-generate/index.ts create mode 100644 backend/src/build-system/util.ts create mode 100644 backend/src/build-system/virtual-dir.ts diff --git a/backend/src/build-system/__tests__/datamap-structure.md b/backend/src/build-system/__tests__/datamap-structure.md deleted file mode 100644 index 6141554..0000000 --- a/backend/src/build-system/__tests__/datamap-structure.md +++ /dev/null @@ -1,275 +0,0 @@ -# Detailed UX Structure Map: Spotify-like Music Web Application - -## **1. Home Page** - -### Page Purpose - -Provide users with quick access to featured playlists, personalized recommendations, new releases, and music genres. - -### Core Elements - -- **Header**: - - Logo: Navigate back to the home page. - - Search Bar: Quick access to search functionality. - - Navigation Links: Home, Search, Library, Account. -- **Main Content Area**: - - **Featured Playlists**: Grid of curated playlists with cover images and titles. - - Interactions: Click to navigate to the playlist page. - - **Personalized Recommendations**: Horizontal scroll carousel of songs and albums tailored to the user. - - Dynamic Content: Updates based on user listening history. - - **New Releases**: Grid displaying recently released albums or singles. - - Interactions: Click to open album details. - - **Genres**: List of genres represented as clickable tiles. -- **Footer**: - - Persistent music player bar with play/pause, skip, and volume controls. - -### Content Display - -- Visual hierarchy prioritizing featured content and recommendations. -- Dynamic updates to reflect user preferences and real-time data. - -### Navigation and Routes - -- `/home`: Main route for the home page. -- Links to `/playlist/:id`, `/album/:id`, `/genre/:id`. - -### Restrictions - -- Requires user login to display personalized recommendations. - ---- - -## **2. Search Page** - -### Page Purpose - -Allow users to search for specific songs, albums, artists, or playlists and view categorized results. - -### Core Elements - -- **Header**: - - Logo, Navigation Links, and Search Bar (persistent from Home Page). -- **Search Results Section**: - - **Songs**: List of matching songs with play buttons. - - **Albums**: Grid of albums with cover art and titles. - - **Artists**: Horizontal scroll carousel of artists. - - **Playlists**: List of curated and user-created playlists. -- **No Results Message**: Display when no matching content is found. - -### Content Display - -- Results categorized and clearly separated. -- Real-time search suggestions displayed as user types. - -### Navigation and Routes - -- `/search`: Search page route. -- Links to `/song/:id`, `/album/:id`, `/artist/:id`, `/playlist/:id`. - -### Restrictions - -- Accessible without login but limited to generic content unless logged in. - ---- - -## **3. Library Page** - -### Page Purpose - -Provide users access to their playlists, liked songs, and recently played music. - -### Core Elements - -- **Header**: - - Persistent header with navigation and search. -- **Library Sections**: - - **My Playlists**: List of user-created playlists. - - Actions: Edit, delete, reorder. - - **Liked Songs**: List of favorited songs with play buttons. - - **Recently Played**: List of previously played songs and playlists. - -### Content Display - -- Organize content with clear sections and labels. -- Show metadata (e.g., song duration, artist) for user context. - -### Navigation and Routes - -- `/library`: Library main page route. -- Links to `/playlist/:id`, `/song/:id`. - -### Restrictions - -- Requires login to access user-specific content. - ---- - -## **4. Playlist/Album/Genre Pages** - -### Page Purpose - -Allow users to view and play content within a specific playlist, album, or genre. - -### Core Elements - -- **Header**: - - Back Button: Navigate to the previous page. - - Playlist/Album/Genre Name: Display prominently. -- **Content Section**: - - **Playlist/Album Info**: Cover art, title, creator name, description. - - **Track List**: List of songs with play buttons and metadata. - - **Action Buttons**: - - Play All: Start playing all tracks in order. - - Add to Library: Save playlist/album to user library (if applicable). -- **Footer**: - - Persistent music player bar. - -### Content Display - -- Highlight album or playlist cover and metadata for context. -- Easy access to play all tracks or individual songs. - -### Navigation and Routes - -- `/playlist/:id`, `/album/:id`, `/genre/:id`. - -### Restrictions - -- Accessible without login, but restricted to sample tracks if not logged in. - ---- - -## **5. Player Page (Now Playing)** - -### Page Purpose - -Provide full controls for the currently playing track. - -### Core Elements - -- **Header**: - - Back Button: Navigate to the previous page. -- **Main Player Controls**: - - Song Info: Display song title, artist, album cover. - - Play/Pause, Skip, Seek Bar, Volume Controls. -- **Lyrics Section**: - - Toggle to view lyrics for the current track (if available). -- **Queue Section**: - - List of upcoming tracks with reordering options. - -### Content Display - -- Centralized focus on the current track with controls easily accessible. - -### Navigation and Routes - -- `/player`. - -### Restrictions - -- Requires active playback session. - ---- - -## **6. Account Page** - -### Page Purpose - -Allow users to manage profile settings and preferences. - -### Core Elements - -- **Header**: - - Persistent navigation and search. -- **Profile Info Section**: - - Display user profile details (username, email). - - Edit button for updating profile. -- **Preferences Section**: - - Toggle for dark mode/light mode. - - Audio quality selection. -- **Logout Button**: - - Prominent button to securely log out. - -### Content Display - -- Clear sections for profile details and app preferences. -- Easy access to frequently updated settings. - -### Navigation and Routes - -- `/account`. - -### Restrictions - -- Requires login. - ---- - -## **7. Onboarding Pages** - -### Page Purpose - -Guide new users through account setup and app features. - -### Core Elements - -- **Welcome Screen**: - - Brief intro to the app. - - Buttons to log in or sign up. -- **Sign-Up Page**: - - Fields for username, email, password. - - Option to sign up via Google or Facebook. -- **Login Page**: - - Username and password fields. - - Forgot Password link. - -### Content Display - -- Minimal text to streamline onboarding. -- Prominent call-to-action buttons. - -### Navigation and Routes - -- `/welcome`, `/signup`, `/login`. - -### Restrictions - -- Accessible to all users without restrictions. - ---- - -## **8. Error and Offline Pages** - -### Page Purpose - -Provide feedback for errors and offline scenarios. - -### Core Elements - -- **404 Page**: - - Message indicating content not found. - - Button to return to Home. -- **Offline Mode**: - - Notification about internet connectivity issues. - - Limited access to downloaded or cached content. - -### Content Display - -- Clear messaging and actionable steps for users. - -### Navigation and Routes - -- `/404`, `/offline`. - -### Restrictions - -- Accessible in all scenarios. - ---- - -## **Dynamic Content and Restrictions Summary** - -- **Dynamic Content**: - - Recommendations, playlists, recently played, and search results update in real-time based on user activity. -- **Restrictions**: - - Login required for personalized content, library access, and playback history. diff --git a/backend/src/build-system/__tests__/db-requirement-document.md b/backend/src/build-system/__tests__/db-requirement-document.md deleted file mode 100644 index 3b0d088..0000000 --- a/backend/src/build-system/__tests__/db-requirement-document.md +++ /dev/null @@ -1,230 +0,0 @@ -### Database Requirements Document - -#### 1. Overview - -- **Project Scope**: Design and implement a database to support a Spotify-like music web application, facilitating personalized music streaming, content management, and user interaction. -- **Database Purpose**: Store and manage user profiles, music content, playlists, playback states, and preferences to support dynamic, personalized user experiences. -- **General Requirements**: - - Ensure high availability and scalability to handle concurrent user activity. - - Support real-time data updates for personalized recommendations and playback. - - Ensure data integrity and enforce business rules. - ---- - -#### 2. Entity Definitions - -##### User - -- **Description**: Represents registered users of the application. -- **Business Rules**: - - Each user must have a unique email. - - Users can manage their preferences and account details. -- **Key Attributes**: - - `user_id` (Primary Key) - - `username` (Unique, required) - - `email` (Unique, required) - - `password_hash` (Required) - - `subscription_type` (e.g., Free, Premium) - - `preferences` (e.g., theme, audio quality) - - `created_at`, `updated_at` -- **Relationships**: - - One-to-many with `Playlist`. - - Many-to-many with `Song` for liked songs. - -##### Song - -- **Description**: Represents individual songs available on the platform. -- **Business Rules**: - - Each song must have an associated album and artist. - - Songs may belong to multiple playlists. -- **Key Attributes**: - - `song_id` (Primary Key) - - `title` (Required) - - `artist_id` (Foreign Key) - - `album_id` (Foreign Key) - - `duration` (In seconds) - - `genre` (Category) - - `release_date` -- **Relationships**: - - Many-to-one with `Album` and `Artist`. - - Many-to-many with `Playlist`. - -##### Artist - -- **Description**: Represents artists whose songs are on the platform. -- **Key Attributes**: - - `artist_id` (Primary Key) - - `name` (Required) - - `bio` - - `profile_image` - - `created_at`, `updated_at` -- **Relationships**: - - One-to-many with `Song` and `Album`. - -##### Album - -- **Description**: Represents music albums. -- **Key Attributes**: - - `album_id` (Primary Key) - - `title` (Required) - - `artist_id` (Foreign Key) - - `release_date` - - `cover_image` -- **Relationships**: - - One-to-many with `Song`. - -##### Playlist - -- **Description**: Represents user-created or curated playlists. -- **Business Rules**: - - A playlist must belong to a user or be globally curated. -- **Key Attributes**: - - `playlist_id` (Primary Key) - - `name` (Required) - - `user_id` (Foreign Key, nullable for curated playlists) - - `description` - - `is_curated` (Boolean) - - `created_at`, `updated_at` -- **Relationships**: - - Many-to-many with `Song`. - -##### PlaybackState - -- **Description**: Tracks the playback state for a user. -- **Key Attributes**: - - `playback_id` (Primary Key) - - `user_id` (Foreign Key) - - `current_song_id` (Foreign Key) - - `queue` (Array of `song_id`s) - - `playback_position` (Seconds) - - `volume` - - `created_at`, `updated_at` - -##### Recommendation - -- **Description**: Stores dynamic recommendations for users. -- **Key Attributes**: - - `recommendation_id` (Primary Key) - - `user_id` (Foreign Key) - - `content` (JSON: list of recommended songs, albums, playlists) - - `generated_at` - ---- - -#### 3. Data Requirements - -##### User - -- Fields: - - `user_id`: UUID - - `username`: String (max 50) - - `email`: String (unique, max 100) - - `password_hash`: String - - `subscription_type`: Enum (Free, Premium) - - `preferences`: JSON - - `created_at`, `updated_at`: Timestamps -- Constraints: - - `email` and `username` must be unique. - - Enforce non-null constraints on required fields. -- Indexing: - - Index on `email` and `user_id`. - -##### Song - -- Fields: - - `song_id`: UUID - - `title`: String (max 100) - - `artist_id`, `album_id`: Foreign Keys - - `duration`: Integer - - `genre`: String - - `release_date`: Date -- Constraints: - - Non-null constraints on `title`, `artist_id`, and `album_id`. -- Indexing: - - Index on `title` and `genre`. - -##### Playlist - -- Fields: - - `playlist_id`: UUID - - `name`: String (max 50) - - `user_id`: Foreign Key - - `description`: String - - `is_curated`: Boolean - - `created_at`, `updated_at`: Timestamps -- Constraints: - - Enforce foreign key constraints for `user_id`. -- Indexing: - - Index on `user_id`. - -##### PlaybackState - -- Fields: - - `playback_id`: UUID - - `user_id`: Foreign Key - - `current_song_id`: Foreign Key - - `queue`: JSON - - `playback_position`: Integer - - `volume`: Float - - `created_at`, `updated_at`: Timestamps -- Constraints: - - Ensure valid `user_id` and `current_song_id`. - ---- - -#### 4. Relationships - -- `User` to `Playlist`: One-to-many. -- `Playlist` to `Song`: Many-to-many (junction table: `playlist_song`). -- `Song` to `Album`: Many-to-one. -- `Song` to `Artist`: Many-to-one. -- `User` to `PlaybackState`: One-to-one. -- Referential Integrity: - - Cascade delete for dependent entities (e.g., playlists when a user is deleted). - ---- - -#### 5. Data Access Patterns - -- Common Queries: - - Fetch user playlists, liked songs, and playback state. - - Search for songs, albums, or artists. - - Fetch recommended content dynamically. -- Indexing: - - Full-text search for song titles and artist names. - - Index on foreign keys for join performance. - ---- - -#### 6. Security Requirements - -- Access Control: - - Restrict user data to authenticated sessions. -- Data Privacy: - - Hash sensitive data (e.g., passwords). -- Audit: - - Log user activity and data changes. - ---- - -#### 7. Performance Requirements - -- Expected Volume: - - Millions of songs and playlists. - - Thousands of concurrent users. -- Growth: - - Plan for 10x growth in user and song data over 5 years. -- Optimizations: - - Cache frequently accessed data (e.g., recommendations). - - Use partitioning for large tables. - ---- - -#### 8. Additional Considerations - -- Backups: - - Automated daily backups. -- Archiving: - - Move inactive playlists to archival storage after 1 year. -- Integration: - - Support for third-party authentication and external APIs. diff --git a/backend/src/build-system/__tests__/file-structure-document.md b/backend/src/build-system/__tests__/file-structure-document.md deleted file mode 100644 index b12484e..0000000 --- a/backend/src/build-system/__tests__/file-structure-document.md +++ /dev/null @@ -1,77 +0,0 @@ -src/ -├── api/ # API logic for interacting with backend services -│ ├── auth.ts # Authentication API logic -│ ├── music.ts # API for music-related actions (e.g., fetching playlists, songs) -│ ├── user.ts # API for user-related actions (e.g., profile updates) -│ └── index.ts # Exports all API modules for centralized access -├── components/ # Reusable UI components -│ ├── common/ # Generic components used across the app -│ │ ├── Button/ # Button component folder -│ │ │ ├── index.tsx # Main Button component file -│ │ │ └── index.css # Button-specific styles -│ │ ├── Input/ # Input field component folder -│ │ │ ├── index.tsx # Main Input component file -│ │ │ └── index.css # Input-specific styles -│ │ ├── Modal/ # Modal component folder -│ │ │ ├── index.tsx # Main Modal component file -│ │ │ └── index.css # Modal-specific styles -│ ├── layout/ # Components for layout and structure -│ │ ├── Header/ # Header component folder -│ │ │ ├── index.tsx # Main Header component file -│ │ │ └── index.css # Header-specific styles -│ │ ├── Footer/ # Footer component folder -│ │ │ ├── index.tsx # Main Footer component file -│ │ │ └── index.css # Footer-specific styles -│ ├── specific/ # Page-specific components -│ │ ├── Home/ # Components specific to the Home page -│ │ │ ├── FeaturedPlaylists.tsx # Component for featured playlists section -│ │ │ ├── Recommendations.tsx # Component for personalized recommendations -│ │ │ └── NewReleases.tsx # Component for new releases -│ │ ├── Search/ # Components specific to the Search page -│ │ │ ├── SearchResults.tsx # Component for displaying search results -│ │ │ └── Autocomplete.tsx # Component for search suggestions -│ │ ├── Library/ # Components specific to the Library page -│ │ │ ├── MyPlaylists.tsx # Component for user playlists -│ │ │ ├── LikedSongs.tsx # Component for liked songs -│ │ │ └── RecentlyPlayed.tsx # Component for recently played songs -├── contexts/ # Context providers for global state -│ ├── AuthContext.tsx # Provides user authentication state -│ ├── ThemeContext.tsx # Provides theme (dark/light mode) state -│ ├── PlayerContext.tsx # Provides music player state and controls -│ └── index.ts # Centralized export for contexts -├── hooks/ # Custom hooks for data fetching and state management -│ ├── useAuth.ts # Hook for user authentication logic -│ ├── usePlayer.ts # Hook for controlling the music player -│ ├── useTheme.ts # Hook for managing theme preferences -│ └── useFetch.ts # Generic hook for data fetching -├── pages/ # Route-specific views -│ ├── Home/ # Home page folder -│ │ ├── index.tsx # Main Home page component -│ │ └── index.css # Home page-specific styles -│ ├── Search/ # Search page folder -│ │ ├── index.tsx # Main Search page component -│ │ └── index.css # Search page-specific styles -│ ├── Library/ # Library page folder -│ │ ├── index.tsx # Main Library page component -│ │ └── index.css # Library page-specific styles -│ ├── Playlist/ # Playlist page folder -│ │ ├── index.tsx # Main Playlist page component -│ │ └── index.css # Playlist page-specific styles -│ ├── Account/ # Account page folder -│ │ ├── index.tsx # Main Account page component -│ │ └── index.css # Account page-specific styles -│ ├── Player/ # Player page folder -│ │ ├── index.tsx # Main Player page component -│ │ └── index.css # Player page-specific styles -│ ├── Error/ # Error and Offline pages folder -│ │ ├── NotFound.tsx # 404 Page component -│ │ ├── Offline.tsx # Offline mode page component -│ │ └── index.css # Styles for error pages -├── utils/ # Utility functions -│ ├── constants.ts # Application-wide constants -│ ├── helpers.ts # Helper functions -│ ├── validators.ts # Validation logic -│ └── index.ts # Centralized export for utilities -├── router.ts # Central routing configuration -├── index.tsx # Application entry point -└── App.tsx # Main application component diff --git a/backend/src/build-system/__tests__/test-file-create.spec.ts b/backend/src/build-system/__tests__/test-file-create.spec.ts new file mode 100644 index 0000000..5c195c2 --- /dev/null +++ b/backend/src/build-system/__tests__/test-file-create.spec.ts @@ -0,0 +1,56 @@ +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { FileGeneratorHandler } from '../node/file-generate'; // Update with actual file path to the handler + +describe('FileGeneratorHandler', () => { + const projectSrcPath = 'src\\build-system\\__tests__\\test-project\\'; + // Read JSON data from file + const mdFilePath = path.resolve('src\\build-system\\__tests__\\file-arch.md'); + const structMdFilePath = path.resolve( + 'src\\build-system\\__tests__\\file-structure-document.md', + ); + + beforeEach(async () => { + // Ensure the project directory is clean + await fs.remove('src\\build-system\\__tests__\\test-project\\src\\'); + }); + + afterEach(async () => { + // Clean up the generated test files + await fs.remove('src\\build-system\\__tests__\\test-project\\src\\'); + }); + + it('should generate files based on file-arch.md', async () => { + const archMarkdownContent = fs.readFileSync( + path.resolve(mdFilePath), + 'utf8', + ); + const structMarkdownContent = fs.readFileSync( + path.resolve(structMdFilePath), + 'utf8', + ); + + const handler = new FileGeneratorHandler(structMarkdownContent); + + // Run the file generator with the JSON data + const result = await handler.generateFiles( + archMarkdownContent, + projectSrcPath, + ); + + console.log('File generation result:', result); + + // Verify that all files exist + const jsonData = JSON.parse( + /([\s\S]*?)<\/GENERATEDCODE>/.exec( + archMarkdownContent, + )![1], + ); + const files = Object.keys(jsonData.files); + + for (const file of files) { + const filePath = path.resolve(projectSrcPath, file); + expect(fs.existsSync(filePath)).toBeTruthy(); + } + }, 30000); +}); diff --git a/backend/src/build-system/__tests__/test-generate-doc.spec.ts b/backend/src/build-system/__tests__/test-generate-doc.spec.ts index 972b93b..3cdad1f 100644 --- a/backend/src/build-system/__tests__/test-generate-doc.spec.ts +++ b/backend/src/build-system/__tests__/test-generate-doc.spec.ts @@ -67,11 +67,40 @@ describe('Sequence: PRD -> UXSD -> UXDD -> UXSS', () => { }, { id: 'step-4', + name: 'UX Data Map Document', + nodes: [ + { + id: 'op:UX_DATAMAP::STATE:GENERATE', + name: 'UX Data Map Document node', + requires: ['op:UXSMD::STATE:GENERATE'], + }, + ], + }, + { + id: 'step-5', name: 'file structure generation', nodes: [ { id: 'op:FSTRUCT::STATE:GENERATE', name: 'file structure generation', + requires: [ + 'op:UXSMD::STATE:GENERATE', + 'op:UX_DATAMAP::STATE:GENERATE', + ], + }, + ], + }, + { + id: 'step-6', + name: 'File_Arch Document', + nodes: [ + { + id: 'op:FILE_ARCH::STATE:GENERATE', + name: 'File_Arch', + requires: [ + 'op:FSTRUCT::STATE:GENERATE', + 'op:UX_DATAMAP::STATE:GENERATE', + ], }, ], }, @@ -88,14 +117,15 @@ describe('Sequence: PRD -> UXSD -> UXDD -> UXSS', () => { try { await BuildSequenceExecutor.executeSequence(sequence, context); - sequence.steps.forEach((step) => { - step.nodes.forEach((node) => { - const resultData = context.getResult(node.id); + for (const step of sequence.steps) { + for (const node of step.nodes) { + const resultData = await context.getResult(node.id); + console.log(resultData); if (resultData) { writeMarkdownToFile(node.name.replace(/ /g, '_'), resultData); } - }); - }); + } + } console.log( 'Sequence completed successfully. Logs stored in:', @@ -110,5 +140,5 @@ describe('Sequence: PRD -> UXSD -> UXDD -> UXSS', () => { ); throw error; } - }, 60000); + }, 600000); }); diff --git a/backend/src/build-system/__tests__/testVirtualDir.spec.ts b/backend/src/build-system/__tests__/testVirtualDir.spec.ts new file mode 100644 index 0000000..0de61db --- /dev/null +++ b/backend/src/build-system/__tests__/testVirtualDir.spec.ts @@ -0,0 +1,58 @@ +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { VirtualDirectory } from '../virtual-dir'; +import * as normalizePath from 'normalize-path'; + +describe('VirtualDirectory', () => { + const structMdFilePath = normalizePath( + 'src\\build-system\\__tests__\\file-structure-document.md', + ); + + describe('VirtualDirectory', () => { + let virtualDir: VirtualDirectory; + let structMarkdownContent: string; + + beforeEach(() => { + structMarkdownContent = fs.readFileSync(structMdFilePath, 'utf8'); + virtualDir = new VirtualDirectory(); + virtualDir.parseJsonStructure(structMarkdownContent); + }); + + it('should print tree structure', () => { + const files = virtualDir.getAllFiles(); + console.log(files); + }); + + // change test path to your current test file + it('should validate existing files', () => { + expect(virtualDir.isValidFile('src/pages/Home/index.tsx')).toBeTruthy(); + // expect(virtualDir.isValidFile('src/utils/validators.ts')).toBeTruthy(); + // expect( + // virtualDir.isValidFile('components/common/Button/index.tsx'), + // ).toBeFalsy(); + // expect( + // virtualDir.isValidFile('src/components/layout/Footer/index.css'), + // ).toBeTruthy(); + // expect( + // virtualDir.isValidFile('components/layout/Footer/index.tsx'), + // ).toBeFalsy(); + expect(virtualDir.isValidFile('nonexistent.ts')).toBeFalsy(); + }); + + it('should validate existing directories', () => { + expect(virtualDir.isValidDirectory('src')).toBeTruthy(); + expect(virtualDir.isValidDirectory('src/components/common')).toBeTruthy(); + expect(virtualDir.isValidDirectory('api')).toBeFalsy(); + expect(virtualDir.isValidDirectory('nonexistent')).toBeFalsy(); + }); + + it('should resolve relative paths correctly', () => { + const resolved = virtualDir.resolveRelativePath( + 'src/components/common/Button/index.tsx', + '../Loader/index.tsx', + ); + expect(virtualDir.isValidFile(resolved)).toBeTruthy(); + expect(resolved).toBe('src/components/common/Loader/index.tsx'); + }); + }); +}); diff --git a/backend/src/build-system/context.ts b/backend/src/build-system/context.ts index 1e31d6f..cb98691 100644 --- a/backend/src/build-system/context.ts +++ b/backend/src/build-system/context.ts @@ -7,6 +7,7 @@ import { BuildSequence, } from './types'; import { Logger } from '@nestjs/common'; +import { VirtualDirectory } from './virtual-dir'; export type GlobalDataKeys = 'projectName' | 'description' | 'platform'; type ContextData = { @@ -26,6 +27,7 @@ export class BuilderContext { private results: Map = new Map(); private handlerManager: BuildHandlerManager; public model: ModelProvider; + public virtualDirectory: VirtualDirectory; constructor( private sequence: BuildSequence, @@ -34,13 +36,16 @@ export class BuilderContext { this.handlerManager = BuildHandlerManager.getInstance(); this.model = ModelProvider.getInstance(); this.logger = new Logger(`builder-context-${id}`); + this.virtualDirectory = new VirtualDirectory(); } canExecute(nodeId: string): boolean { const node = this.findNode(nodeId); + if (!node) return false; if (this.state.completed.has(nodeId) || this.state.pending.has(nodeId)) { + console.log(`Node ${nodeId} is already completed or pending.`); return false; } @@ -106,15 +111,14 @@ export class BuilderContext { return this.results.get(nodeId); } + buildVirtualDirectory(jsonContent: string): boolean { + return this.virtualDirectory.parseJsonStructure(jsonContent); + } + private async executeNode( node: BuildNode, args: unknown, ): Promise { - if (process.env.NODE_ENV === 'test') { - this.logger.log(`[TEST] Executing node: ${node.id}`); - return { success: true, data: { nodeId: node.id } }; - } - this.logger.log(`Executing node: ${node.id}`); const handler = this.handlerManager.getHandler(node.id); if (!handler) { diff --git a/backend/src/build-system/hanlder-manager.ts b/backend/src/build-system/hanlder-manager.ts index 3383a31..4760a77 100644 --- a/backend/src/build-system/hanlder-manager.ts +++ b/backend/src/build-system/hanlder-manager.ts @@ -5,6 +5,7 @@ import { UXSitemapStructureHandler } from './node/ux-sitemap-structure'; import { UXDatamapHandler } from './node/ux-datamap'; import { UXSMDHandler } from './node/ux-sitemap-document/uxsmd'; import { FileStructureHandler } from './node/frontend-file-structure'; +import { FileArchGenerateHandler } from './node/file-arch'; export class BuildHandlerManager { private static instance: BuildHandlerManager; @@ -22,6 +23,7 @@ export class BuildHandlerManager { new UXDatamapHandler(), new UXSMDHandler(), new FileStructureHandler(), + new FileArchGenerateHandler(), ]; for (const handler of builtInHandlers) { diff --git a/backend/src/build-system/node/file-arch/index.ts b/backend/src/build-system/node/file-arch/index.ts index dd1321d..280251b 100644 --- a/backend/src/build-system/node/file-arch/index.ts +++ b/backend/src/build-system/node/file-arch/index.ts @@ -2,13 +2,14 @@ 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/util'; export class FileArchGenerateHandler implements BuildHandler { - readonly id = 'op:File_Arch::STATE:GENERATE'; + readonly id = 'op:FILE_ARCH::STATE:GENERATE'; private readonly logger: Logger = new Logger('FileArchGenerateHandler'); // TODO: adding page by page analysis - async run(context: BuilderContext, ...args: any[]): Promise { + async run(context: BuilderContext, args: unknown): Promise { this.logger.log('Generating File Architecture Document...'); const fileStructure = args[0] as string; @@ -23,18 +24,89 @@ export class FileArchGenerateHandler implements BuildHandler { }; } - const prompt = generateFileArchPrompt(fileStructure, dataMapStruct); - - const fileArchContent = await context.model.chatSync( - { - content: prompt, - }, - 'gpt-4o-mini', + const prompt = generateFileArchPrompt( + JSON.stringify(fileStructure, null, 2), + JSON.stringify(dataMapStruct, null, 2), ); + // fileArchContent generate + let successBuild = false; + let fileArchContent = null; + let jsonData = null; + let retry = 0; + const retryChances = 2; + while (!successBuild) { + if (retry > retryChances) { + throw new Error( + 'Failed to build virtual directory after multiple attempts', + ); + } + + fileArchContent = await context.model.chatSync( + { + content: prompt, + }, + 'gpt-4o-mini', + ); + + // validation test + jsonData = FileUtil.extractJsonFromMarkdown(fileArchContent); + if (jsonData == null) { + retry += 1; + this.logger.error('Extract Json From Markdown fail'); + continue; + } + + if (!this.validateJsonData(jsonData)) { + retry += 1; + this.logger.error('FileArchGenerate validateJsonData fail'); + continue; + } + this.logger.log(jsonData); + successBuild = true; + } + return { success: true, data: fileArchContent, }; } + + /** + * Validate the structure and content of the JSON data. + * @param jsonData The JSON data to validate. + * @throws Error if validation fails. + */ + private validateJsonData(jsonData: { + files: Record; + }): boolean { + const validPathRegex = /^[a-zA-Z0-9_\-/.]+$/; + + for (const [file, details] of Object.entries(jsonData.files)) { + // Validate the file path + if (!validPathRegex.test(file)) { + this.logger.error(`Invalid file path: ${file}`); + return false; + } + + // Validate dependencies + for (const dependency of details.dependsOn) { + if (!validPathRegex.test(dependency)) { + this.logger.error( + `Invalid dependency path "${dependency}" in file "${file}".`, + ); + return false; + } + + // Ensure no double slashes or trailing slashes + if (dependency.includes('//') || dependency.endsWith('/')) { + this.logger.error( + `Malformed dependency path "${dependency}" in file "${file}".`, + ); + return false; + } + } + } + return true; + } } diff --git a/backend/src/build-system/node/file-generate/index.ts b/backend/src/build-system/node/file-generate/index.ts new file mode 100644 index 0000000..446084f --- /dev/null +++ b/backend/src/build-system/node/file-generate/index.ts @@ -0,0 +1,183 @@ +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { Logger } from '@nestjs/common'; +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/util'; + +export class FileGeneratorHandler { + 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; + + // change here + const projectSrcPath = ''; + this.generateFiles(JSON.stringify(fileArch, null, 2), projectSrcPath); + + return { + success: true, + data: 'Files and dependencies created successfully.', + }; + } + + /** + * Generate files based on the JSON extracted from a Markdown file. + * @param markdownContent The Markdown content containing the JSON. + * @param projectSrcPath The base directory where files should be generated. + */ + async generateFiles( + markdownContent: string, + projectSrcPath: string, + ): Promise<{ success: boolean; data: string }> { + const jsonData = FileUtil.extractJsonFromMarkdown(markdownContent); + // Build the dependency graph and detect cycles before any file operations + const { graph, nodes } = this.buildDependencyGraph(jsonData); + this.detectCycles(graph); + + // Add virtual directory validation + this.validateAgainstVirtualDirectory(nodes); + + // After validation and cycle detection, perform topological sort + const sortedFiles = this.getSortedFiles(graph, nodes); + + // Generate files in the correct order + for (const file of sortedFiles) { + const fullPath = path.resolve(projectSrcPath, file); + this.logger.log(`Generating file in dependency order: ${fullPath}`); + // TODO(allen) + await this.createFile(fullPath); + } + + this.logger.log('All files generated successfully.'); + return { + success: true, + data: 'Files and dependencies created successfully.', + }; + } + + /** + * Build dependency graph from JSON data. + * @param jsonData The JSON data containing file dependencies. + */ + private buildDependencyGraph(jsonData: { + files: Record; + }): { graph: [string, string][]; nodes: Set } { + const graph: [string, string][] = []; + const nodes = new Set(); + + Object.entries(jsonData.files).forEach(([fileName, details]) => { + nodes.add(fileName); + details.dependsOn.forEach((dep) => { + const resolvedDep = this.resolveDependency(fileName, dep); + graph.push([resolvedDep, fileName]); // [dependency, dependent] + nodes.add(resolvedDep); + }); + }); + + return { graph, nodes }; + } + + /** + * Detect cycles in the dependency graph before any file operations. + * @param graph The dependency graph to check. + * @throws Error if a cycle is detected. + */ + private detectCycles(graph: [string, string][]): void { + try { + toposort(graph); + } catch (error) { + if (error.message.includes('cycle')) { + throw new Error( + `Circular dependency detected in the file structure: ${error.message}`, + ); + } + throw error; + } + } + + /** + * Get topologically sorted list of files. + * @param graph The dependency graph. + * @param nodes Set of all nodes. + */ + private getSortedFiles( + graph: [string, string][], + nodes: Set, + ): string[] { + const sortedFiles = toposort(graph).reverse(); + + // Add any files that have no dependencies and weren't included in the sort + Array.from(nodes).forEach((node) => { + if (!sortedFiles.includes(node)) { + sortedFiles.unshift(node); + } + }); + + return sortedFiles; + } + + /** + * Resolve a dependency path relative to the current file. + * @param currentFile The current file's path. + * @param dependency The dependency path. + */ + private resolveDependency(currentFile: string, dependency: string): string { + const currentDir = path.dirname(currentFile); + + // Check if the dependency is a file with an extension + const hasExtension = path.extname(dependency).length > 0; + + // If the dependency doesn't have an extension and is not CSS/JS, assume it's a TypeScript file + if (!hasExtension) { + dependency = path.join(dependency, 'index.ts'); + } + + // Resolve the dependency path relative to the current directory + const resolvedPath = path.join(currentDir, dependency).replace(/\\/g, '/'); + this.logger.log(`Resolved dependency: ${resolvedPath}`); + return resolvedPath; + } + + /** + * Validate that all files and dependencies exist in the virtual directory structure + * @param nodes Set of all files and dependencies + * @throws Error if any file or dependency is not found in the virtual directory + */ + private validateAgainstVirtualDirectory(nodes: Set): void { + const invalidFiles: string[] = []; + + nodes.forEach((filePath) => { + if (!this.virtualDir.isValidFile(filePath)) { + invalidFiles.push(filePath); + } + }); + + if (invalidFiles.length > 0) { + throw new Error( + `The following files do not exist in the project structure:\n${invalidFiles.join('\n')}`, + ); + } + } + + /** + * Create a file, including creating necessary directories. + * @param filePath The full path of the file to create. + */ + private async createFile(filePath: string): Promise { + const dir = path.dirname(filePath); + + // Ensure the directory exists + await fs.mkdir(dir, { recursive: true }); + + // Create the file with a placeholder content + const content = `// Generated file: ${path.basename(filePath)}`; + await fs.writeFile(filePath, content, 'utf8'); + + this.logger.log(`File created: ${filePath}`); + } +} diff --git a/backend/src/build-system/node/frontend-file-structure/index.ts b/backend/src/build-system/node/frontend-file-structure/index.ts index 380e911..63fa13b 100644 --- a/backend/src/build-system/node/frontend-file-structure/index.ts +++ b/backend/src/build-system/node/frontend-file-structure/index.ts @@ -2,34 +2,78 @@ import { BuildHandler, BuildResult } from 'src/build-system/types'; import { BuilderContext } from 'src/build-system/context'; import { ModelProvider } from 'src/common/model-provider'; import { prompts } from './prompt'; +import { Logger } from '@nestjs/common'; export class FileStructureHandler implements BuildHandler { readonly id = 'op:FSTRUCT::STATE:GENERATE'; + private readonly logger: Logger = new Logger('FileStructureHandler'); async run(context: BuilderContext, args: unknown): Promise { - console.log('Generating File Structure Document...'); + this.logger.log('Generating File Structure Document...'); // extract relevant data from the context const projectName = context.getData('projectName') || 'Default Project Name'; + const sitemapDoc = args[0] as string; + const dataMap = args[1] as string; + + if (!dataMap || !sitemapDoc) { + return { + success: false, + error: new Error('Missing required parameters: sitemapDoc or dataMap'), + }; + } + const prompt = prompts.generateFileStructurePrompt( projectName, - args as string, - // TODO: change later - args as string, + JSON.stringify(sitemapDoc, null, 2), + JSON.stringify(dataMap, null, 2), 'FrameWork Holder', ); + // Call the chatSync function to get the file structure content. const fileStructureContent = await context.model.chatSync( { content: prompt, }, 'gpt-4o-mini', ); + + this.logger.log('For fileStructureContent debug: ' + fileStructureContent); + + const ToJsonPrompt = prompts.convertTreeToJsonPrompt(fileStructureContent); + + // Try to build the virtual directory from the JSON structure. + let successBuild = false; + let fileStructureJsonContent = null; + let retry = 0; + const retryChances = 2; + while (!successBuild) { + if (retry > retryChances) { + throw new Error( + 'Failed to build virtual directory after multiple attempts', + ); + } + fileStructureJsonContent = await context.model.chatSync( + { + content: ToJsonPrompt, + }, + 'gpt-4o-mini', + ); + + this.logger.log('fileStructureJsonContent: ' + fileStructureJsonContent); + + successBuild = context.buildVirtualDirectory(fileStructureJsonContent); + retry += 1; + } + + this.logger.log('fileStructureJsonContent success'); + this.logger.log('buildVirtualDirectory success'); + return { success: true, - data: fileStructureContent, + data: fileStructureJsonContent, }; } } diff --git a/backend/src/build-system/node/frontend-file-structure/prompt.ts b/backend/src/build-system/node/frontend-file-structure/prompt.ts index b8a445d..6929733 100644 --- a/backend/src/build-system/node/frontend-file-structure/prompt.ts +++ b/backend/src/build-system/node/frontend-file-structure/prompt.ts @@ -30,13 +30,11 @@ Include: Add example filenames for components, hooks, APIs, etc. Do Not Include: - Asset folders (e.g., images, icons, fonts). Test folders or files. Service folders unrelated to API logic. File Naming Guidelines: - Use meaningful and descriptive file names. For components, include an index.tsx file in each folder to simplify imports. Each component should have its own folder named after the component (e.g., Button/). @@ -44,11 +42,14 @@ File Naming Guidelines: Component-specific styles must be in index.css within the same folder as the component. File Comments: - Include comments describing the purpose of each file or folder to improve readability. -Self check: +Ask yourself: 1, Are you consider all the cases based on the sitemap doc? If not add new folder or file + 2, Are you consider all the components based on the sitemap doc? If not add new folder or file + 3, Are you consider all the hooks based on the sitemap doc? If not add new folder or file + 4, Are you consider all the api based on the sitemap doc? If not add new folder or file + 5, Are you consider all the pages based on the sitemap doc? If not add new folder or file This final result must be 100% complete. Will be directly use in the production @@ -56,9 +57,62 @@ Output Format: Start with: "\`\`\`FolderStructure" Tree format: - Include folder names with placeholder/example files inside. + Include folder names with placeholder files inside. Add comments to describe the purpose of each file/folder. End with: "\`\`\`" `; }, + convertTreeToJsonPrompt: (treeMarkdown: string): string => { + return `You are a highly skilled developer. Your task is to convert the given file and folder structure, currently represented in an ASCII tree format, into a JSON structure. The JSON structure must: + + - Represent directories and files in a hierarchical manner. + - Use objects with "type" and "name" keys. + - For directories: + - "type": "directory" + - "name": "" + - "children": [ ... ] (an array of files or directories) + - For files: + - "type": "file" + - "name": "" + - Maintain the same nesting as the original ASCII tree. + + **Input Tree:** + \`\`\` + ${treeMarkdown} + \`\`\` + + **Output Format:** + Return a JSON object of the form: + \`\`\`json + { + "type": "directory", + "name": "", + "children": [ + { + "type": "directory", + "name": "subDirName", + "children": [ + { + "type": "file", + "name": "fileName.ext" + } + ] + }, + { + "type": "file", + "name": "anotherFile.ext" + } + ] + } + \`\`\` + + **Additional Rules:** + - Keep directory names and file names exactly as they appear (excluding trailing slashes). + - For directories that appear like "common/", in the JSON just use "common" as the name. + - Do not include comments or extra fields besides "type", "name", and "children". + - The root node should correspond to the top-level directory in the tree. + + Return only the JSON structure (no explanations, no additional comments). This JSON will be used directly in the application. + `; + }, }; diff --git a/backend/src/build-system/node/ux-sitemap-document/uxsmd.ts b/backend/src/build-system/node/ux-sitemap-document/uxsmd.ts index 499f15e..42d6fce 100644 --- a/backend/src/build-system/node/ux-sitemap-document/uxsmd.ts +++ b/backend/src/build-system/node/ux-sitemap-document/uxsmd.ts @@ -38,7 +38,7 @@ export class UXSMDHandler implements BuildHandler { private async generateUXSMDFromLLM(prompt: string): Promise { const modelProvider = ModelProvider.getInstance(); - const model = 'gpt-3.5-turbo'; + const model = 'gpt-4o-mini'; const prdContent = modelProvider.chatSync( { diff --git a/backend/src/build-system/node/ux-sitemap-structure/index.ts b/backend/src/build-system/node/ux-sitemap-structure/index.ts index 60ffdcb..8ce2bfa 100644 --- a/backend/src/build-system/node/ux-sitemap-structure/index.ts +++ b/backend/src/build-system/node/ux-sitemap-structure/index.ts @@ -16,13 +16,22 @@ export class UXSitemapStructureHandler implements BuildHandler { const projectName = context.getData('projectName') || 'Default Project Name'; + const sitemap = args[0] as string; + + if (!sitemap) { + return { + success: false, + error: new Error('Missing required parameters: sitemap'), + }; + } + const prompt = prompts.generateUXSiteMapStructrePrompt( projectName, - args as string, + JSON.stringify(sitemap, null, 2), // TODO: change later 'web', ); - + this.logger.log(prompt); const uxStructureContent = await context.model.chatSync( { content: prompt, diff --git a/backend/src/build-system/node/ux-sitemap-structure/prompt.ts b/backend/src/build-system/node/ux-sitemap-structure/prompt.ts index 0e6f932..933bb8a 100644 --- a/backend/src/build-system/node/ux-sitemap-structure/prompt.ts +++ b/backend/src/build-system/node/ux-sitemap-structure/prompt.ts @@ -18,12 +18,13 @@ export const prompts = { 2, You need to ensure all features from the sitemap documentation are addressed. 3, You need to identify and define every page/screen required for the application. 4, Detailed Breakdown for Each Page/Screen: + Page name: ## {index}. Name page Page Purpose: Clearly state the user goal for the page. Core Elements: List all components (e.g., headers, buttons, sidebars) and explain their role on the page. Include specific interactions and behaviors for each element. Content Display: - Identify the information that needs to be visible on the page and why it’s essential for the user. + Identify the information that needs to be visible on the page and why it's essential for the user. Navigation and Routes: Specify all frontend routes required to navigate to this page. Include links or actions that lead to other pages or states. diff --git a/backend/src/build-system/util.ts b/backend/src/build-system/util.ts new file mode 100644 index 0000000..7283fed --- /dev/null +++ b/backend/src/build-system/util.ts @@ -0,0 +1,30 @@ +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; + } + } +} diff --git a/backend/src/build-system/virtual-dir.ts b/backend/src/build-system/virtual-dir.ts new file mode 100644 index 0000000..d9d568c --- /dev/null +++ b/backend/src/build-system/virtual-dir.ts @@ -0,0 +1,115 @@ +import * as path from 'path'; +import * as normalizePath from 'normalize-path'; + +interface VirtualNode { + name: string; + isFile: boolean; + children: Map; +} + +interface FileStructureNode { + type: 'file' | 'directory'; + name: string; + children?: FileStructureNode[]; +} + +export class VirtualDirectory { + private root: VirtualNode; + + constructor() { + this.root = { + name: 'src', + isFile: false, + children: new Map(), + }; + } + + private cleanJsonContent(content: string): string { + const jsonStart = content.indexOf('{'); + const jsonEnd = content.lastIndexOf('}'); + return content.slice(jsonStart, jsonEnd + 1); + } + + public parseJsonStructure(jsonContent: string): boolean { + try { + const cleanedJson = this.cleanJsonContent(jsonContent); + const structure = JSON.parse(cleanedJson); + this.buildTree(structure, this.root); + return true; + } catch (error) { + console.error('Failed to parse JSON structure:', error); + return false; + } + } + + private buildTree(node: FileStructureNode, virtualNode: VirtualNode): void { + if (node.children) { + for (const child of node.children) { + const newNode: VirtualNode = { + name: child.name, + isFile: child.type === 'file', + children: new Map(), + }; + virtualNode.children.set(child.name, newNode); + + if (child.type === 'directory' && child.children) { + this.buildTree(child, newNode); + } + } + } + } + + isValidFile(filePath: string): boolean { + const node = this.findNode(filePath); + return node?.isFile ?? false; + } + + isValidDirectory(dirPath: string): boolean { + const node = this.findNode(dirPath); + return node !== null && !node.isFile; + } + + private findNode(inputPath: string): VirtualNode | null { + const normalizedPath = this.normalize_Path(inputPath); + const parts = normalizedPath.split('/').filter(Boolean); + + if (parts[0] !== 'src') { + return null; + } + + let current = this.root; + for (let i = 1; i < parts.length; i++) { + const next = current.children.get(parts[i]); + if (!next) return null; + current = next; + } + return current; + } + + private normalize_Path(inputPath: string): string { + return normalizePath(inputPath); + } + + resolveRelativePath(fromFile: string, toPath: string): string { + const fromDir = path.dirname(fromFile); + const resolvedPath = path.join(fromDir, toPath).replace(/\\/g, '/'); + return this.normalize_Path(resolvedPath); + } + + getAllFiles(): string[] { + const files: string[] = []; + + const traverse = (node: VirtualNode, parentPath: string = '') => { + for (const [name, child] of node.children) { + const currentPath = parentPath ? `${parentPath}/${name}` : name; + if (child.isFile) { + files.push(`src/${currentPath}`); + } + traverse(child, currentPath); + } + }; + + traverse(this.root); + return files.sort(); + } +}