From 52aebae9035929c916ee0ec57d2be4adc6b098af Mon Sep 17 00:00:00 2001 From: Will Chen Date: Wed, 25 Jun 2025 15:34:11 -0700 Subject: [PATCH] Fix order of dyad tag processing (#495) Fixes #493 --- e2e-tests/fixtures/rename-edit.md | 6 + e2e-tests/rename_edit.spec.ts | 9 + .../rename_edit.spec.ts_rename-edit.txt | 185 ++++++++++++++++++ playwright.config.ts | 3 +- src/ipc/processors/response_processor.ts | 98 ++++++---- 5 files changed, 256 insertions(+), 45 deletions(-) create mode 100644 e2e-tests/fixtures/rename-edit.md create mode 100644 e2e-tests/rename_edit.spec.ts create mode 100644 e2e-tests/snapshots/rename_edit.spec.ts_rename-edit.txt diff --git a/e2e-tests/fixtures/rename-edit.md b/e2e-tests/fixtures/rename-edit.md new file mode 100644 index 0000000..20b079e --- /dev/null +++ b/e2e-tests/fixtures/rename-edit.md @@ -0,0 +1,6 @@ + + + + +// newly added content to renamed file should exist + diff --git a/e2e-tests/rename_edit.spec.ts b/e2e-tests/rename_edit.spec.ts new file mode 100644 index 0000000..7244e4f --- /dev/null +++ b/e2e-tests/rename_edit.spec.ts @@ -0,0 +1,9 @@ +import { test } from "./helpers/test_helper"; + +test("rename then edit works", async ({ po }) => { + await po.setUp({ autoApprove: true }); + await po.importApp("minimal"); + + await po.sendPrompt("tc=rename-edit"); + await po.snapshotAppFiles({ name: "rename-edit" }); +}); diff --git a/e2e-tests/snapshots/rename_edit.spec.ts_rename-edit.txt b/e2e-tests/snapshots/rename_edit.spec.ts_rename-edit.txt new file mode 100644 index 0000000..a8eae25 --- /dev/null +++ b/e2e-tests/snapshots/rename_edit.spec.ts_rename-edit.txt @@ -0,0 +1,185 @@ +=== .gitignore === +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + + +=== file1.txt === +A file (2) + +=== index.html === + + + + + + dyad-generated-app + + + +
+ + + + + +=== package.json === +{ + "name": "vite_react_shadcn_ts", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "build:dev": "vite build --mode development", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/node": "^22.5.5", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react-swc": "^3.9.0", + "typescript": "^5.5.3", + "vite": "^6.3.4" + }, + "packageManager": "" +} + +=== src/main.tsx === +import { createRoot } from "react-dom/client"; +import App from "./App.tsx"; + +createRoot(document.getElementById("root")!).render(); + + +=== src/Renamed.tsx === +// newly added content to renamed file should exist + +=== src/vite-env.d.ts === +/// + + +=== tsconfig.app.json === +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": false, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitAny": false, + "noFallthroughCasesInSwitch": false, + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"] +} + + +=== tsconfig.json === +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "noImplicitAny": false, + "noUnusedParameters": false, + "skipLibCheck": true, + "allowJs": true, + "noUnusedLocals": false, + "strictNullChecks": false + } +} + + +=== tsconfig.node.json === +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} + + +=== vite.config.ts === +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react-swc"; +import path from "path"; + +export default defineConfig(() => ({ + server: { + host: "::", + port: 8080, + }, + plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +})); diff --git a/playwright.config.ts b/playwright.config.ts index 3b01438..f6db8fa 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -3,8 +3,7 @@ import { PlaywrightTestConfig } from "@playwright/test"; const config: PlaywrightTestConfig = { testDir: "./e2e-tests", workers: 1, - retries: process.env.CI ? 2 : 1, - // maxFailures: 1, + retries: process.env.CI ? 2 : 0, timeout: process.env.CI ? 180_000 : 30_000, // Use a custom snapshot path template because Playwright's default // is platform-specific which isn't necessary for Dyad e2e tests diff --git a/src/ipc/processors/response_processor.ts b/src/ipc/processors/response_processor.ts index 4d9d7e0..4e89b71 100644 --- a/src/ipc/processors/response_processor.ts +++ b/src/ipc/processors/response_processor.ts @@ -305,30 +305,55 @@ export async function processFullResponseActions( } } - // Process all file writes - for (const tag of dyadWriteTags) { - const filePath = tag.path; - const content = tag.content; + ////////////////////// + // File operations // + // Do it in this order: + // 1. Deletes + // 2. Renames + // 3. Writes + // + // Why? + // - Deleting first avoids path conflicts before the other operations. + // - LLMs like to rename and then edit the same file. + ////////////////////// + + // Process all file deletions + for (const filePath of dyadDeletePaths) { const fullFilePath = path.join(appPath, filePath); - // Ensure directory exists - const dirPath = path.dirname(fullFilePath); - fs.mkdirSync(dirPath, { recursive: true }); + // Delete the file if it exists + if (fs.existsSync(fullFilePath)) { + if (fs.lstatSync(fullFilePath).isDirectory()) { + fs.rmdirSync(fullFilePath, { recursive: true }); + } else { + fs.unlinkSync(fullFilePath); + } + logger.log(`Successfully deleted file: ${fullFilePath}`); + deletedFiles.push(filePath); - // Write file content - fs.writeFileSync(fullFilePath, content); - logger.log(`Successfully wrote file: ${fullFilePath}`); - writtenFiles.push(filePath); + // Remove the file from git + try { + await git.remove({ + fs, + dir: appPath, + filepath: filePath, + }); + } catch (error) { + logger.warn(`Failed to git remove deleted file ${filePath}:`, error); + // Continue even if remove fails as the file was still deleted + } + } else { + logger.warn(`File to delete does not exist: ${fullFilePath}`); + } if (isServerFunction(filePath)) { try { - await deploySupabaseFunctions({ + await deleteSupabaseFunction({ supabaseProjectId: chatWithApp.app.supabaseProjectId!, - functionName: path.basename(path.dirname(filePath)), - content: content, + functionName: getFunctionNameFromPath(filePath), }); } catch (error) { errors.push({ - message: `Failed to deploy Supabase function: ${filePath}`, + message: `Failed to delete Supabase function: ${filePath}`, error: error, }); } @@ -398,43 +423,30 @@ export async function processFullResponseActions( } } - // Process all file deletions - for (const filePath of dyadDeletePaths) { + // Process all file writes + for (const tag of dyadWriteTags) { + const filePath = tag.path; + const content = tag.content; const fullFilePath = path.join(appPath, filePath); - // Delete the file if it exists - if (fs.existsSync(fullFilePath)) { - if (fs.lstatSync(fullFilePath).isDirectory()) { - fs.rmdirSync(fullFilePath, { recursive: true }); - } else { - fs.unlinkSync(fullFilePath); - } - logger.log(`Successfully deleted file: ${fullFilePath}`); - deletedFiles.push(filePath); + // Ensure directory exists + const dirPath = path.dirname(fullFilePath); + fs.mkdirSync(dirPath, { recursive: true }); - // Remove the file from git - try { - await git.remove({ - fs, - dir: appPath, - filepath: filePath, - }); - } catch (error) { - logger.warn(`Failed to git remove deleted file ${filePath}:`, error); - // Continue even if remove fails as the file was still deleted - } - } else { - logger.warn(`File to delete does not exist: ${fullFilePath}`); - } + // Write file content + fs.writeFileSync(fullFilePath, content); + logger.log(`Successfully wrote file: ${fullFilePath}`); + writtenFiles.push(filePath); if (isServerFunction(filePath)) { try { - await deleteSupabaseFunction({ + await deploySupabaseFunctions({ supabaseProjectId: chatWithApp.app.supabaseProjectId!, - functionName: getFunctionNameFromPath(filePath), + functionName: path.basename(path.dirname(filePath)), + content: content, }); } catch (error) { errors.push({ - message: `Failed to delete Supabase function: ${filePath}`, + message: `Failed to deploy Supabase function: ${filePath}`, error: error, }); }