6
e2e-tests/fixtures/rename-edit.md
Normal file
6
e2e-tests/fixtures/rename-edit.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<dyad-rename from="src/App.tsx" to="src/Renamed.tsx">
|
||||||
|
</dyad-rename>
|
||||||
|
|
||||||
|
<dyad-write path="src/Renamed.tsx">
|
||||||
|
// newly added content to renamed file should exist
|
||||||
|
</dyad-write>
|
||||||
9
e2e-tests/rename_edit.spec.ts
Normal file
9
e2e-tests/rename_edit.spec.ts
Normal file
@@ -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" });
|
||||||
|
});
|
||||||
185
e2e-tests/snapshots/rename_edit.spec.ts_rename-edit.txt
Normal file
185
e2e-tests/snapshots/rename_edit.spec.ts_rename-edit.txt
Normal file
@@ -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 ===
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>dyad-generated-app</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
=== 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": "<scrubbed>"
|
||||||
|
}
|
||||||
|
|
||||||
|
=== src/main.tsx ===
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import App from "./App.tsx";
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(<App />);
|
||||||
|
|
||||||
|
|
||||||
|
=== src/Renamed.tsx ===
|
||||||
|
// newly added content to renamed file should exist
|
||||||
|
|
||||||
|
=== src/vite-env.d.ts ===
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
|
||||||
|
=== 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"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -3,8 +3,7 @@ import { PlaywrightTestConfig } from "@playwright/test";
|
|||||||
const config: PlaywrightTestConfig = {
|
const config: PlaywrightTestConfig = {
|
||||||
testDir: "./e2e-tests",
|
testDir: "./e2e-tests",
|
||||||
workers: 1,
|
workers: 1,
|
||||||
retries: process.env.CI ? 2 : 1,
|
retries: process.env.CI ? 2 : 0,
|
||||||
// maxFailures: 1,
|
|
||||||
timeout: process.env.CI ? 180_000 : 30_000,
|
timeout: process.env.CI ? 180_000 : 30_000,
|
||||||
// Use a custom snapshot path template because Playwright's default
|
// Use a custom snapshot path template because Playwright's default
|
||||||
// is platform-specific which isn't necessary for Dyad e2e tests
|
// is platform-specific which isn't necessary for Dyad e2e tests
|
||||||
|
|||||||
@@ -305,30 +305,55 @@ export async function processFullResponseActions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process all file writes
|
//////////////////////
|
||||||
for (const tag of dyadWriteTags) {
|
// File operations //
|
||||||
const filePath = tag.path;
|
// Do it in this order:
|
||||||
const content = tag.content;
|
// 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);
|
const fullFilePath = path.join(appPath, filePath);
|
||||||
|
|
||||||
// Ensure directory exists
|
// Delete the file if it exists
|
||||||
const dirPath = path.dirname(fullFilePath);
|
if (fs.existsSync(fullFilePath)) {
|
||||||
fs.mkdirSync(dirPath, { recursive: true });
|
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
|
// Remove the file from git
|
||||||
fs.writeFileSync(fullFilePath, content);
|
try {
|
||||||
logger.log(`Successfully wrote file: ${fullFilePath}`);
|
await git.remove({
|
||||||
writtenFiles.push(filePath);
|
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)) {
|
if (isServerFunction(filePath)) {
|
||||||
try {
|
try {
|
||||||
await deploySupabaseFunctions({
|
await deleteSupabaseFunction({
|
||||||
supabaseProjectId: chatWithApp.app.supabaseProjectId!,
|
supabaseProjectId: chatWithApp.app.supabaseProjectId!,
|
||||||
functionName: path.basename(path.dirname(filePath)),
|
functionName: getFunctionNameFromPath(filePath),
|
||||||
content: content,
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errors.push({
|
errors.push({
|
||||||
message: `Failed to deploy Supabase function: ${filePath}`,
|
message: `Failed to delete Supabase function: ${filePath}`,
|
||||||
error: error,
|
error: error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -398,43 +423,30 @@ export async function processFullResponseActions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process all file deletions
|
// Process all file writes
|
||||||
for (const filePath of dyadDeletePaths) {
|
for (const tag of dyadWriteTags) {
|
||||||
|
const filePath = tag.path;
|
||||||
|
const content = tag.content;
|
||||||
const fullFilePath = path.join(appPath, filePath);
|
const fullFilePath = path.join(appPath, filePath);
|
||||||
|
|
||||||
// Delete the file if it exists
|
// Ensure directory exists
|
||||||
if (fs.existsSync(fullFilePath)) {
|
const dirPath = path.dirname(fullFilePath);
|
||||||
if (fs.lstatSync(fullFilePath).isDirectory()) {
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
fs.rmdirSync(fullFilePath, { recursive: true });
|
|
||||||
} else {
|
|
||||||
fs.unlinkSync(fullFilePath);
|
|
||||||
}
|
|
||||||
logger.log(`Successfully deleted file: ${fullFilePath}`);
|
|
||||||
deletedFiles.push(filePath);
|
|
||||||
|
|
||||||
// Remove the file from git
|
// Write file content
|
||||||
try {
|
fs.writeFileSync(fullFilePath, content);
|
||||||
await git.remove({
|
logger.log(`Successfully wrote file: ${fullFilePath}`);
|
||||||
fs,
|
writtenFiles.push(filePath);
|
||||||
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)) {
|
if (isServerFunction(filePath)) {
|
||||||
try {
|
try {
|
||||||
await deleteSupabaseFunction({
|
await deploySupabaseFunctions({
|
||||||
supabaseProjectId: chatWithApp.app.supabaseProjectId!,
|
supabaseProjectId: chatWithApp.app.supabaseProjectId!,
|
||||||
functionName: getFunctionNameFromPath(filePath),
|
functionName: path.basename(path.dirname(filePath)),
|
||||||
|
content: content,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errors.push({
|
errors.push({
|
||||||
message: `Failed to delete Supabase function: ${filePath}`,
|
message: `Failed to deploy Supabase function: ${filePath}`,
|
||||||
error: error,
|
error: error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user