Skip to main content

tideway_cli/commands/
setup.rs

1//! Setup command - installs frontend dependencies (Tailwind, shadcn, etc.)
2
3use anyhow::{Context, Result};
4use colored::Colorize;
5use std::process::Command;
6
7use crate::cli::{Framework, SetupArgs, Style};
8use crate::{is_json_output, is_plan_mode, print_error, print_info, print_success, print_warning};
9
10/// Components required for tideway frontend
11const SHADCN_VUE_COMPONENTS: &[&str] = &[
12    "alert",
13    "avatar",
14    "badge",
15    "button",
16    "card",
17    "checkbox",
18    "dialog",
19    "dropdown-menu",
20    "form",
21    "input",
22    "label",
23    "select",
24    "separator",
25    "skeleton",
26    "sonner",
27    "switch",
28    "table",
29    "tabs",
30];
31
32/// Run the setup command
33pub fn run(args: SetupArgs) -> Result<()> {
34    if is_plan_mode() {
35        print_info("Plan: would set up frontend dependencies");
36        return Ok(());
37    }
38    if !is_json_output() {
39        println!(
40            "\n{} Setting up frontend dependencies...\n",
41            "tideway".cyan().bold()
42        );
43    }
44
45    // Check for package.json
46    if !std::path::Path::new("package.json").exists() {
47        print_error("No package.json found. Please run this from a frontend project directory.");
48        if !is_json_output() {
49            println!("\nTo create a new Vue project:");
50            println!("  npm create vue@latest my-app");
51            println!("  cd my-app");
52            println!("  tideway setup");
53        }
54        return Ok(());
55    }
56
57    match args.framework {
58        Framework::Vue => setup_vue(&args)?,
59    }
60
61    if !is_json_output() {
62        println!(
63            "\n{} Frontend setup complete!\n",
64            "✓".green().bold()
65        );
66
67        println!("{}", "Next steps:".yellow().bold());
68        println!("  1. Generate components:");
69        println!("     tideway generate all --with-views");
70        println!();
71        println!("  2. Set up your router to use the generated views");
72        println!();
73    }
74
75    Ok(())
76}
77
78fn setup_vue(args: &SetupArgs) -> Result<()> {
79    // Step 1: Clean up default Vue starter files
80    cleanup_vue_starter()?;
81
82    // Step 2: Install Tailwind if needed
83    if !args.no_tailwind && args.style != Style::Unstyled {
84        setup_tailwind()?;
85    }
86
87    // Step 3: Install shadcn-vue if using shadcn style
88    if args.style == Style::Shadcn && !args.no_components {
89        setup_shadcn_vue()?;
90    }
91
92    Ok(())
93}
94
95fn cleanup_vue_starter() -> Result<()> {
96    print_info("Cleaning up default Vue starter files...");
97
98    // Remove default components
99    let default_files = [
100        "src/components/HelloWorld.vue",
101        "src/components/TheWelcome.vue",
102        "src/components/WelcomeItem.vue",
103        "src/components/icons/IconCommunity.vue",
104        "src/components/icons/IconDocumentation.vue",
105        "src/components/icons/IconEcosystem.vue",
106        "src/components/icons/IconSupport.vue",
107        "src/components/icons/IconTooling.vue",
108    ];
109
110    for file in default_files {
111        if std::path::Path::new(file).exists() {
112            let _ = std::fs::remove_file(file);
113        }
114    }
115
116    // Remove icons directory if empty
117    if std::path::Path::new("src/components/icons").exists() {
118        let _ = std::fs::remove_dir("src/components/icons");
119    }
120
121    // Replace HomeView with a simple redirect
122    if std::path::Path::new("src/views/HomeView.vue").exists() {
123        std::fs::write(
124            "src/views/HomeView.vue",
125            r#"<script setup lang="ts">
126import { onMounted } from 'vue'
127import { useRouter } from 'vue-router'
128
129const router = useRouter()
130
131onMounted(() => {
132  router.push('/login')
133})
134</script>
135
136<template>
137  <div class="min-h-screen flex items-center justify-center">
138    <p class="text-muted-foreground">Redirecting...</p>
139  </div>
140</template>
141"#,
142        )?;
143        print_success("Replaced HomeView.vue with login redirect");
144    }
145
146    // Remove AboutView if it exists
147    if std::path::Path::new("src/views/AboutView.vue").exists() {
148        let _ = std::fs::remove_file("src/views/AboutView.vue");
149    }
150
151    // Clean up App.vue
152    if std::path::Path::new("src/App.vue").exists() {
153        std::fs::write(
154            "src/App.vue",
155            r#"<script setup lang="ts">
156import { RouterView } from 'vue-router'
157</script>
158
159<template>
160  <RouterView />
161</template>
162"#,
163        )?;
164        print_success("Cleaned up App.vue");
165    }
166
167    // Clean up main.css - remove default styles and base.css import
168    if std::path::Path::new("src/assets/main.css").exists() {
169        let content = std::fs::read_to_string("src/assets/main.css")?;
170        // Keep only @import lines that are NOT base.css
171        let imports: Vec<&str> = content
172            .lines()
173            .filter(|line| {
174                let trimmed = line.trim();
175                trimmed.starts_with("@import") && !trimmed.contains("base.css")
176            })
177            .collect();
178
179        if !imports.is_empty() {
180            std::fs::write("src/assets/main.css", imports.join("\n") + "\n")?;
181            print_success("Cleaned up main.css");
182        }
183    }
184
185    // Remove base.css if it exists (default Vue styles)
186    if std::path::Path::new("src/assets/base.css").exists() {
187        let _ = std::fs::remove_file("src/assets/base.css");
188        print_success("Removed base.css");
189    }
190
191    // Clean up router - remove default Home and About routes
192    cleanup_router()?;
193
194    Ok(())
195}
196
197fn cleanup_router() -> Result<()> {
198    let router_path = std::path::Path::new("src/router/index.ts");
199    if !router_path.exists() {
200        return Ok(());
201    }
202
203    // Replace with a clean router template - tideway generate will add routes
204    std::fs::write(
205        router_path,
206        r#"import { createRouter, createWebHistory } from 'vue-router'
207
208const router = createRouter({
209  history: createWebHistory(import.meta.env.BASE_URL),
210  routes: [],
211})
212
213export default router
214"#,
215    )?;
216    print_success("Cleaned up router (removed default routes)");
217
218    Ok(())
219}
220
221fn setup_tailwind() -> Result<()> {
222    print_info("Setting up Tailwind CSS v4...");
223
224    // Check if vite.config exists
225    let vite_config_path = if std::path::Path::new("vite.config.ts").exists() {
226        "vite.config.ts"
227    } else if std::path::Path::new("vite.config.js").exists() {
228        "vite.config.js"
229    } else {
230        print_warning("No vite.config found. Tailwind v4 setup requires Vite.");
231        return Ok(());
232    };
233
234    // Install Tailwind v4 with Vite plugin
235    print_info("Installing @tailwindcss/vite...");
236    let status = Command::new("npm")
237        .args(["install", "-D", "tailwindcss", "@tailwindcss/vite"])
238        .status()
239        .context("Failed to run npm install")?;
240
241    if !status.success() {
242        print_warning("Failed to install Tailwind CSS");
243        return Ok(());
244    }
245
246    print_success("Tailwind CSS v4 installed");
247
248    // Update vite.config to add tailwindcss plugin
249    print_info("Configuring vite.config...");
250    let vite_config = std::fs::read_to_string(vite_config_path)?;
251
252    if !vite_config.contains("@tailwindcss/vite") {
253        let updated = vite_config
254            .replace(
255                "import vue from '@vitejs/plugin-vue'",
256                "import vue from '@vitejs/plugin-vue'\nimport tailwindcss from '@tailwindcss/vite'"
257            )
258            .replace(
259                "plugins: [vue()]",
260                "plugins: [vue(), tailwindcss()]"
261            )
262            .replace(
263                "plugins: [\n    vue()",
264                "plugins: [\n    vue(),\n    tailwindcss()"
265            );
266
267        std::fs::write(vite_config_path, updated)?;
268        print_success("Updated vite.config with tailwindcss plugin");
269    } else {
270        print_info("Tailwind already in vite.config");
271    }
272
273    // Update main CSS file
274    let css_paths = ["src/assets/main.css", "src/style.css", "src/index.css"];
275    for css_path in css_paths {
276        if std::path::Path::new(css_path).exists() {
277            let css_content = std::fs::read_to_string(css_path)?;
278            if !css_content.contains("@import \"tailwindcss\"") && !css_content.contains("@import 'tailwindcss'") {
279                let updated = format!("@import \"tailwindcss\";\n\n{}", css_content);
280                std::fs::write(css_path, updated)?;
281                print_success(&format!("Added Tailwind import to {}", css_path));
282            }
283            break;
284        }
285    }
286
287    Ok(())
288}
289
290fn setup_shadcn_vue() -> Result<()> {
291    print_info("Setting up shadcn-vue...");
292
293    // First, ensure tsconfig has the @ alias
294    setup_tsconfig_alias()?;
295
296    // Check if shadcn is already initialized (components.json exists)
297    let has_shadcn = std::path::Path::new("components.json").exists();
298
299    if !has_shadcn {
300        print_info("Initializing shadcn-vue...");
301
302        let status = Command::new("npx")
303            .args(["shadcn-vue@latest", "init", "-y", "-d"])
304            .status()
305            .context("Failed to run shadcn-vue init")?;
306
307        if !status.success() {
308            print_error("Failed to initialize shadcn-vue");
309            if !is_json_output() {
310                println!("You can try running manually: npx shadcn-vue@latest init");
311            }
312            return Ok(());
313        }
314
315        print_success("shadcn-vue initialized");
316    } else {
317        print_info("shadcn-vue already initialized");
318    }
319
320    // Install required components
321    print_info(&format!(
322        "Installing {} shadcn components...",
323        SHADCN_VUE_COMPONENTS.len()
324    ));
325
326    let components = SHADCN_VUE_COMPONENTS.join(" ");
327
328    let status = Command::new("npx")
329        .args(["shadcn-vue@latest", "add", "-y"])
330        .args(SHADCN_VUE_COMPONENTS)
331        .status()
332        .context("Failed to install shadcn components")?;
333
334    if !status.success() {
335        print_warning("Some components may have failed to install");
336        if !is_json_output() {
337            println!("You can try running manually: npx shadcn-vue@latest add {}", components);
338        }
339        return Ok(());
340    }
341
342    print_success("shadcn components installed");
343
344    // Install tw-animate-css (required by shadcn-vue animations)
345    print_info("Installing tw-animate-css...");
346    let status = Command::new("npm")
347        .args(["install", "tw-animate-css"])
348        .status()
349        .context("Failed to install tw-animate-css")?;
350
351    if status.success() {
352        print_success("tw-animate-css installed");
353    }
354
355    Ok(())
356}
357
358fn setup_tsconfig_alias() -> Result<()> {
359    // Need to update BOTH tsconfig.json and tsconfig.app.json for shadcn-vue
360    let configs = ["tsconfig.json", "tsconfig.app.json"];
361
362    for tsconfig_path in configs {
363        if !std::path::Path::new(tsconfig_path).exists() {
364            continue;
365        }
366
367        print_info(&format!("Checking {} for import alias...", tsconfig_path));
368
369        let content = std::fs::read_to_string(tsconfig_path)?;
370
371        // Check if paths already configured
372        if content.contains("\"@/*\"") || content.contains("'@/*'") {
373            print_info(&format!("{} already has import alias", tsconfig_path));
374            continue;
375        }
376
377        // Add paths to compilerOptions - handle both regular and references-style tsconfig
378        let updated = if content.contains("\"compilerOptions\": {") {
379            // Regular tsconfig with existing compilerOptions
380            content.replace(
381                "\"compilerOptions\": {",
382                "\"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    },"
383            )
384        } else if content.contains("\"files\":") || content.contains("\"references\":") {
385            // References-style tsconfig without compilerOptions - add it
386            content.replace(
387                "{",
388                "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  },"
389            )
390        } else {
391            content
392        };
393
394        std::fs::write(tsconfig_path, updated)?;
395        print_success(&format!("Added @ import alias to {}", tsconfig_path));
396    }
397
398    // Also update vite.config for path resolution
399    setup_vite_path_resolution()?;
400
401    // Create tailwind.config.ts stub for shadcn-vue compatibility
402    setup_tailwind_config_stub()?;
403
404    Ok(())
405}
406
407fn setup_tailwind_config_stub() -> Result<()> {
408    // shadcn-vue requires a tailwind.config file even though Tailwind v4 doesn't need one
409    let config_exists = std::path::Path::new("tailwind.config.ts").exists()
410        || std::path::Path::new("tailwind.config.js").exists();
411
412    if config_exists {
413        return Ok(());
414    }
415
416    print_info("Creating tailwind.config.ts for shadcn-vue compatibility...");
417    std::fs::write(
418        "tailwind.config.ts",
419        "// Tailwind v4 uses CSS-based configuration, but shadcn-vue needs this file\nexport default {}\n"
420    )?;
421    print_success("Created tailwind.config.ts");
422
423    Ok(())
424}
425
426fn setup_vite_path_resolution() -> Result<()> {
427    // Install @types/node if needed
428    print_info("Installing @types/node for path resolution...");
429    let _ = Command::new("npm")
430        .args(["install", "-D", "@types/node"])
431        .status();
432
433    // Update vite.config to add resolve.alias
434    let vite_config_path = if std::path::Path::new("vite.config.ts").exists() {
435        "vite.config.ts"
436    } else if std::path::Path::new("vite.config.js").exists() {
437        "vite.config.js"
438    } else {
439        return Ok(());
440    };
441
442    let content = std::fs::read_to_string(vite_config_path)?;
443
444    // Check if already has path import and resolve.alias
445    if content.contains("fileURLToPath") && content.contains("resolve:") {
446        print_info("Vite path resolution already configured");
447        return Ok(());
448    }
449
450    // Add the import and resolve config
451    let mut updated = content;
452
453    // Add import if not present
454    if !updated.contains("fileURLToPath") {
455        updated = format!(
456            "import {{ fileURLToPath, URL }} from 'node:url'\n{}",
457            updated
458        );
459    }
460
461    // Add resolve.alias if not present
462    if !updated.contains("resolve:") {
463        updated = updated.replace(
464            "plugins: [",
465            "resolve: {\n    alias: {\n      '@': fileURLToPath(new URL('./src', import.meta.url))\n    }\n  },\n  plugins: ["
466        );
467    }
468
469    std::fs::write(vite_config_path, updated)?;
470    print_success("Added path resolution to vite.config");
471
472    Ok(())
473}