1use 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
10const 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
32pub 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 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 cleanup_vue_starter()?;
81
82 if !args.no_tailwind && args.style != Style::Unstyled {
84 setup_tailwind()?;
85 }
86
87 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 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 if std::path::Path::new("src/components/icons").exists() {
118 let _ = std::fs::remove_dir("src/components/icons");
119 }
120
121 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 if std::path::Path::new("src/views/AboutView.vue").exists() {
148 let _ = std::fs::remove_file("src/views/AboutView.vue");
149 }
150
151 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 if std::path::Path::new("src/assets/main.css").exists() {
169 let content = std::fs::read_to_string("src/assets/main.css")?;
170 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 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 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 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 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 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 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 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 setup_tsconfig_alias()?;
295
296 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 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 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 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 if content.contains("\"@/*\"") || content.contains("'@/*'") {
373 print_info(&format!("{} already has import alias", tsconfig_path));
374 continue;
375 }
376
377 let updated = if content.contains("\"compilerOptions\": {") {
379 content.replace(
381 "\"compilerOptions\": {",
382 "\"compilerOptions\": {\n \"baseUrl\": \".\",\n \"paths\": {\n \"@/*\": [\"./src/*\"]\n },"
383 )
384 } else if content.contains("\"files\":") || content.contains("\"references\":") {
385 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 setup_vite_path_resolution()?;
400
401 setup_tailwind_config_stub()?;
403
404 Ok(())
405}
406
407fn setup_tailwind_config_stub() -> Result<()> {
408 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 print_info("Installing @types/node for path resolution...");
429 let _ = Command::new("npm")
430 .args(["install", "-D", "@types/node"])
431 .status();
432
433 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 if content.contains("fileURLToPath") && content.contains("resolve:") {
446 print_info("Vite path resolution already configured");
447 return Ok(());
448 }
449
450 let mut updated = content;
452
453 if !updated.contains("fileURLToPath") {
455 updated = format!(
456 "import {{ fileURLToPath, URL }} from 'node:url'\n{}",
457 updated
458 );
459 }
460
461 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}