1use anyhow::{Context, Result};
4use colored::Colorize;
5use std::path::Path;
6use std::process::Command;
7
8use crate::cli::{Framework, SetupArgs, Style};
9use crate::{
10 is_json_output, is_plan_mode, print_error, print_info, print_success, print_warning,
11 remove_dir, remove_file, write_file,
12};
13
14const SHADCN_VUE_COMPONENTS: &[&str] = &[
16 "alert",
17 "avatar",
18 "badge",
19 "button",
20 "card",
21 "checkbox",
22 "dialog",
23 "dropdown-menu",
24 "form",
25 "input",
26 "label",
27 "select",
28 "separator",
29 "skeleton",
30 "sonner",
31 "switch",
32 "table",
33 "tabs",
34];
35
36pub fn run(args: SetupArgs) -> Result<()> {
38 if !is_json_output() {
39 println!(
40 "\n{} Setting up frontend dependencies...\n",
41 "tideway".cyan().bold()
42 );
43 }
44
45 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!("\n{} Frontend setup complete!\n", "✓".green().bold());
63
64 println!("{}", "Next steps:".yellow().bold());
65 println!(" 1. Generate components:");
66 println!(" tideway generate all --with-views");
67 println!();
68 println!(" 2. Set up your router to use the generated views");
69 println!();
70 }
71
72 Ok(())
73}
74
75fn setup_vue(args: &SetupArgs) -> Result<()> {
76 cleanup_vue_starter()?;
78
79 if !args.no_tailwind && args.style != Style::Unstyled {
81 setup_tailwind()?;
82 }
83
84 if args.style == Style::Shadcn && !args.no_components {
86 setup_shadcn_vue()?;
87 }
88
89 Ok(())
90}
91
92fn cleanup_vue_starter() -> Result<()> {
93 print_info("Cleaning up default Vue starter files...");
94
95 let default_files = [
97 "src/components/HelloWorld.vue",
98 "src/components/TheWelcome.vue",
99 "src/components/WelcomeItem.vue",
100 "src/components/icons/IconCommunity.vue",
101 "src/components/icons/IconDocumentation.vue",
102 "src/components/icons/IconEcosystem.vue",
103 "src/components/icons/IconSupport.vue",
104 "src/components/icons/IconTooling.vue",
105 ];
106
107 for file in default_files {
108 if Path::new(file).exists() {
109 let _ = remove_file(Path::new(file));
110 }
111 }
112
113 if Path::new("src/components/icons").exists() {
115 let _ = remove_dir(Path::new("src/components/icons"));
116 }
117
118 if Path::new("src/views/HomeView.vue").exists() {
120 write_file(
121 Path::new("src/views/HomeView.vue"),
122 r#"<script setup lang="ts">
123import { onMounted } from 'vue'
124import { useRouter } from 'vue-router'
125
126const router = useRouter()
127
128onMounted(() => {
129 router.push('/login')
130})
131</script>
132
133<template>
134 <div class="min-h-screen flex items-center justify-center">
135 <p class="text-muted-foreground">Redirecting...</p>
136 </div>
137</template>
138"#,
139 )?;
140 print_success("Replaced HomeView.vue with login redirect");
141 }
142
143 if Path::new("src/views/AboutView.vue").exists() {
145 let _ = remove_file(Path::new("src/views/AboutView.vue"));
146 }
147
148 if Path::new("src/App.vue").exists() {
150 write_file(
151 Path::new("src/App.vue"),
152 r#"<script setup lang="ts">
153import { RouterView } from 'vue-router'
154</script>
155
156<template>
157 <RouterView />
158</template>
159"#,
160 )?;
161 print_success("Cleaned up App.vue");
162 }
163
164 if Path::new("src/assets/main.css").exists() {
166 let content = std::fs::read_to_string("src/assets/main.css")?;
167 let imports: Vec<&str> = content
169 .lines()
170 .filter(|line| {
171 let trimmed = line.trim();
172 trimmed.starts_with("@import") && !trimmed.contains("base.css")
173 })
174 .collect();
175
176 if !imports.is_empty() {
177 write_file(
178 Path::new("src/assets/main.css"),
179 &(imports.join("\n") + "\n"),
180 )?;
181 print_success("Cleaned up main.css");
182 }
183 }
184
185 if Path::new("src/assets/base.css").exists() {
187 let _ = remove_file(Path::new("src/assets/base.css"));
188 print_success("Removed base.css");
189 }
190
191 cleanup_router()?;
193
194 Ok(())
195}
196
197fn cleanup_router() -> Result<()> {
198 let router_path = Path::new("src/router/index.ts");
199 if !router_path.exists() {
200 return Ok(());
201 }
202
203 write_file(
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 let vite_config_path = if Path::new("vite.config.ts").exists() {
226 "vite.config.ts"
227 } else if 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 print_info("Installing @tailwindcss/vite...");
236 if !run_external_command(
237 "npm",
238 &["install", "-D", "tailwindcss", "@tailwindcss/vite"],
239 "install tailwind dependencies",
240 )? {
241 print_warning("Failed to install Tailwind CSS");
242 return Ok(());
243 }
244
245 print_success("Tailwind CSS v4 installed");
246
247 print_info("Configuring vite.config...");
249 let vite_config = std::fs::read_to_string(vite_config_path)?;
250
251 if !vite_config.contains("@tailwindcss/vite") {
252 let updated = vite_config
253 .replace(
254 "import vue from '@vitejs/plugin-vue'",
255 "import vue from '@vitejs/plugin-vue'\nimport tailwindcss from '@tailwindcss/vite'",
256 )
257 .replace("plugins: [vue()]", "plugins: [vue(), tailwindcss()]")
258 .replace(
259 "plugins: [\n vue()",
260 "plugins: [\n vue(),\n tailwindcss()",
261 );
262
263 write_file(Path::new(vite_config_path), &updated)?;
264 print_success("Updated vite.config with tailwindcss plugin");
265 } else {
266 print_info("Tailwind already in vite.config");
267 }
268
269 let css_paths = ["src/assets/main.css", "src/style.css", "src/index.css"];
271 for css_path in css_paths {
272 if Path::new(css_path).exists() {
273 let css_content = std::fs::read_to_string(css_path)?;
274 if !css_content.contains("@import \"tailwindcss\"")
275 && !css_content.contains("@import 'tailwindcss'")
276 {
277 let updated = format!("@import \"tailwindcss\";\n\n{}", css_content);
278 write_file(Path::new(css_path), &updated)?;
279 print_success(&format!("Added Tailwind import to {}", css_path));
280 }
281 break;
282 }
283 }
284
285 Ok(())
286}
287
288fn setup_shadcn_vue() -> Result<()> {
289 print_info("Setting up shadcn-vue...");
290
291 setup_tsconfig_alias()?;
293
294 let has_shadcn = Path::new("components.json").exists();
296
297 if !has_shadcn {
298 print_info("Initializing shadcn-vue...");
299
300 if !run_external_command(
301 "npx",
302 &["shadcn-vue@latest", "init", "-y", "-d"],
303 "initialize shadcn-vue",
304 )? {
305 print_error("Failed to initialize shadcn-vue");
306 if !is_json_output() {
307 println!("You can try running manually: npx shadcn-vue@latest init");
308 }
309 return Ok(());
310 }
311
312 print_success("shadcn-vue initialized");
313 } else {
314 print_info("shadcn-vue already initialized");
315 }
316
317 print_info(&format!(
319 "Installing {} shadcn components...",
320 SHADCN_VUE_COMPONENTS.len()
321 ));
322
323 let components = SHADCN_VUE_COMPONENTS.join(" ");
324
325 let mut add_args = vec!["shadcn-vue@latest", "add", "-y"];
326 add_args.extend(SHADCN_VUE_COMPONENTS);
327
328 if !run_external_command("npx", &add_args, "install shadcn components")? {
329 print_warning("Some components may have failed to install");
330 if !is_json_output() {
331 println!(
332 "You can try running manually: npx shadcn-vue@latest add {}",
333 components
334 );
335 }
336 return Ok(());
337 }
338
339 print_success("shadcn components installed");
340
341 print_info("Installing tw-animate-css...");
343 if run_external_command(
344 "npm",
345 &["install", "tw-animate-css"],
346 "install tw-animate-css",
347 )? {
348 print_success("tw-animate-css installed");
349 }
350
351 Ok(())
352}
353
354fn setup_tsconfig_alias() -> Result<()> {
355 let configs = ["tsconfig.json", "tsconfig.app.json"];
357
358 for tsconfig_path in configs {
359 if !Path::new(tsconfig_path).exists() {
360 continue;
361 }
362
363 print_info(&format!("Checking {} for import alias...", tsconfig_path));
364
365 let content = std::fs::read_to_string(tsconfig_path)?;
366
367 if content.contains("\"@/*\"") || content.contains("'@/*'") {
369 print_info(&format!("{} already has import alias", tsconfig_path));
370 continue;
371 }
372
373 let updated = if content.contains("\"compilerOptions\": {") {
375 content.replace(
377 "\"compilerOptions\": {",
378 "\"compilerOptions\": {\n \"baseUrl\": \".\",\n \"paths\": {\n \"@/*\": [\"./src/*\"]\n },"
379 )
380 } else if content.contains("\"files\":") || content.contains("\"references\":") {
381 content.replace(
383 "{",
384 "{\n \"compilerOptions\": {\n \"baseUrl\": \".\",\n \"paths\": {\n \"@/*\": [\"./src/*\"]\n }\n },"
385 )
386 } else {
387 content
388 };
389
390 write_file(Path::new(tsconfig_path), &updated)?;
391 print_success(&format!("Added @ import alias to {}", tsconfig_path));
392 }
393
394 setup_vite_path_resolution()?;
396
397 setup_tailwind_config_stub()?;
399
400 Ok(())
401}
402
403fn setup_tailwind_config_stub() -> Result<()> {
404 let config_exists =
406 Path::new("tailwind.config.ts").exists() || Path::new("tailwind.config.js").exists();
407
408 if config_exists {
409 return Ok(());
410 }
411
412 print_info("Creating tailwind.config.ts for shadcn-vue compatibility...");
413 write_file(
414 Path::new("tailwind.config.ts"),
415 "// Tailwind v4 uses CSS-based configuration, but shadcn-vue needs this file\nexport default {}\n",
416 )?;
417 print_success("Created tailwind.config.ts");
418
419 Ok(())
420}
421
422fn setup_vite_path_resolution() -> Result<()> {
423 print_info("Installing @types/node for path resolution...");
425 let _ = run_external_command(
426 "npm",
427 &["install", "-D", "@types/node"],
428 "install @types/node",
429 );
430
431 let vite_config_path = if Path::new("vite.config.ts").exists() {
433 "vite.config.ts"
434 } else if Path::new("vite.config.js").exists() {
435 "vite.config.js"
436 } else {
437 return Ok(());
438 };
439
440 let content = std::fs::read_to_string(vite_config_path)?;
441
442 if content.contains("fileURLToPath") && content.contains("resolve:") {
444 print_info("Vite path resolution already configured");
445 return Ok(());
446 }
447
448 let mut updated = content;
450
451 if !updated.contains("fileURLToPath") {
453 updated = format!(
454 "import {{ fileURLToPath, URL }} from 'node:url'\n{}",
455 updated
456 );
457 }
458
459 if !updated.contains("resolve:") {
461 updated = updated.replace(
462 "plugins: [",
463 "resolve: {\n alias: {\n '@': fileURLToPath(new URL('./src', import.meta.url))\n }\n },\n plugins: ["
464 );
465 }
466
467 write_file(Path::new(vite_config_path), &updated)?;
468 print_success("Added path resolution to vite.config");
469
470 Ok(())
471}
472
473fn run_external_command(program: &str, args: &[&str], context: &str) -> Result<bool> {
474 if is_plan_mode() {
475 print_info(&format!(
476 "Plan: run command `{}`",
477 format_command(program, args)
478 ));
479 return Ok(true);
480 }
481
482 let status = Command::new(program)
483 .args(args)
484 .status()
485 .with_context(|| format!("Failed to {}", context))?;
486
487 Ok(status.success())
488}
489
490fn format_command(program: &str, args: &[&str]) -> String {
491 if args.is_empty() {
492 program.to_string()
493 } else {
494 format!("{} {}", program, args.join(" "))
495 }
496}