1use std::path::{Path, PathBuf};
4
5use crate::Result;
6use crate::actions::abstract_action::AbstractAction;
7use crate::actions::{ActionInvocation, ActionKind, ActionSpec, action_spec};
8use crate::build_action::{
9 BuildActionPlan, BuildActionPlanRequest, create_build_action_plan, project_configuration,
10 string_list_option, string_option,
11};
12use crate::commands::{Input, InputValue};
13use crate::configuration::{Configuration, DEFAULT_ENTRY_FILE, DEFAULT_SOURCE_ROOT};
14use crate::runners::RunnerCommand;
15
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
18pub struct StartAction;
19
20impl StartAction {
21 pub const fn new() -> Self {
22 Self
23 }
24
25 pub fn spec(&self) -> &'static ActionSpec {
26 action_spec(ActionKind::Start).expect("start action spec")
27 }
28
29 pub fn handle_invocation(
30 &self,
31 inputs: Vec<Input>,
32 options: Vec<Input>,
33 extra_flags: Vec<String>,
34 ) -> ActionInvocation {
35 <Self as AbstractAction>::handle(self, inputs, options, extra_flags)
36 }
37
38 pub fn create_plan(&self, request: StartActionPlanRequest) -> Result<StartActionPlan> {
39 create_start_action_plan(request)
40 }
41}
42
43impl AbstractAction for StartAction {
44 fn kind(&self) -> ActionKind {
45 ActionKind::Start
46 }
47}
48
49#[derive(Clone, Debug, PartialEq, Eq)]
50pub struct StartActionPlanRequest {
51 pub cwd: PathBuf,
52 pub configuration: Configuration,
53 pub command_inputs: Vec<Input>,
54 pub command_options: Vec<Input>,
55 pub extra_flags: Vec<String>,
56 pub ts_build_info_file: Option<PathBuf>,
57}
58
59#[derive(Clone, Debug, PartialEq, Eq)]
60pub struct StartActionPlan {
61 pub app_name: Option<String>,
62 pub build_plan: BuildActionPlan,
63 pub process_plan: StartProcessPlan,
64}
65
66#[derive(Clone, Debug, PartialEq, Eq)]
67pub struct StartProcessPlan {
68 pub entry_file: String,
69 pub source_root: String,
70 pub debug_flag: Option<DebugFlag>,
71 pub out_dir_name: PathBuf,
72 pub binary_to_run: String,
73 pub requested_exec: Option<String>,
74 pub shell: bool,
75 pub env_file: Vec<String>,
76 pub enable_source_maps: bool,
77 pub child_process_args: Vec<String>,
78 pub manifest_path: Option<PathBuf>,
79 pub source_root_output: PathBuf,
80 pub fallback_output: PathBuf,
81 pub source_root_command: RunnerCommand,
82 pub fallback_command: RunnerCommand,
83 pub restart: StartRestartPlan,
84}
85
86#[derive(Clone, Debug, PartialEq, Eq)]
87pub enum DebugFlag {
88 Inspect,
89 InspectAddress(String),
90}
91
92#[derive(Clone, Debug, PartialEq, Eq)]
93pub struct StartRestartPlan {
94 pub kill_previous_process_on_success: bool,
95 pub forward_sigint: bool,
96 pub forward_sigterm: bool,
97 pub kill_process_on_parent_exit: bool,
98}
99
100pub fn create_start_action_plan(request: StartActionPlanRequest) -> Result<StartActionPlan> {
101 let cwd = request.cwd.clone();
102 let app_name = request
103 .command_inputs
104 .iter()
105 .find(|input| input.name == "app")
106 .and_then(|input| match input.value.as_ref() {
107 Some(InputValue::String(value)) => Some(value.clone()),
108 _ => None,
109 });
110
111 let project = project_configuration(&request.configuration, app_name.as_deref());
112
113 let build_plan = create_build_action_plan(BuildActionPlanRequest {
114 cwd: request.cwd,
115 configuration: request.configuration,
116 command_inputs: request.command_inputs,
117 command_options: request.command_options.clone(),
118 ts_build_info_file: request.ts_build_info_file,
119 })?;
120
121 let project_build_plan = build_plan
122 .project_plans
123 .iter()
124 .find(|plan| plan.app_name == app_name)
125 .or_else(|| build_plan.project_plans.first());
126 let out_dir_name = project_build_plan
127 .map(|plan| plan.build_plan.inputs.output_dir.clone())
128 .unwrap_or_else(|| PathBuf::from("dist"));
129
130 let entry_file = string_option(&request.command_options, "entryFile")
131 .or(project.entry_file)
132 .unwrap_or_else(|| DEFAULT_ENTRY_FILE.to_string());
133 let source_root = string_option(&request.command_options, "sourceRoot")
134 .or(project.source_root)
135 .unwrap_or_else(|| DEFAULT_SOURCE_ROOT.to_string());
136 let requested_exec = string_option(&request.command_options, "exec");
137 let binary_to_run = "cargo".to_string();
138 let debug_flag = debug_flag(&request.command_options);
139 let shell = bool_option_default(&request.command_options, "shell", true);
140 let env_file = string_list_option(&request.command_options, "envFile");
141 let child_process_args = request.extra_flags;
142 let manifest_path = project_manifest_path(
143 project_build_plan.and_then(|plan| plan.project_root.as_deref()),
144 &source_root,
145 );
146
147 let process_plan = create_start_process_plan(
148 entry_file,
149 source_root,
150 debug_flag,
151 out_dir_name,
152 binary_to_run,
153 requested_exec,
154 shell,
155 env_file,
156 child_process_args,
157 manifest_path,
158 cwd,
159 );
160
161 Ok(StartActionPlan {
162 app_name,
163 build_plan,
164 process_plan,
165 })
166}
167
168pub fn create_start_process_plan(
169 entry_file: String,
170 source_root: String,
171 debug_flag: Option<DebugFlag>,
172 out_dir_name: PathBuf,
173 binary_to_run: String,
174 requested_exec: Option<String>,
175 shell: bool,
176 env_file: Vec<String>,
177 child_process_args: Vec<String>,
178 manifest_path: Option<PathBuf>,
179 cwd: PathBuf,
180) -> StartProcessPlan {
181 let source_root_output = out_dir_name
182 .join(path_from_slash_separated(&source_root))
183 .join(&entry_file);
184 let fallback_output = out_dir_name.join(&entry_file);
185 let command = cargo_run_command(manifest_path.as_deref(), &child_process_args);
186 let runner_command = RunnerCommand {
187 binary: "cargo".to_string(),
188 prefix_args: Vec::new(),
189 command,
190 collect: false,
191 cwd: Some(cwd),
192 shell: true,
193 env: Vec::new(),
194 };
195
196 StartProcessPlan {
197 entry_file,
198 source_root,
199 debug_flag,
200 out_dir_name,
201 binary_to_run,
202 requested_exec,
203 shell,
204 env_file,
205 enable_source_maps: false,
206 child_process_args,
207 manifest_path,
208 source_root_output,
209 fallback_output,
210 source_root_command: runner_command.clone(),
211 fallback_command: runner_command,
212 restart: StartRestartPlan {
213 kill_previous_process_on_success: true,
214 forward_sigint: true,
215 forward_sigterm: true,
216 kill_process_on_parent_exit: true,
217 },
218 }
219}
220
221fn debug_flag(options: &[Input]) -> Option<DebugFlag> {
222 options
223 .iter()
224 .find(|option| option.name == "debug")
225 .and_then(|option| match option.value.as_ref() {
226 Some(InputValue::Bool(true)) => Some(DebugFlag::Inspect),
227 Some(InputValue::String(value)) if value.is_empty() => Some(DebugFlag::Inspect),
228 Some(InputValue::String(value)) => Some(DebugFlag::InspectAddress(value.clone())),
229 _ => None,
230 })
231}
232
233fn bool_option_default(options: &[Input], name: &str, default: bool) -> bool {
234 options
235 .iter()
236 .find(|option| option.name == name)
237 .and_then(|option| match option.value.as_ref() {
238 Some(InputValue::Bool(value)) => Some(*value),
239 _ => None,
240 })
241 .unwrap_or(default)
242}
243
244fn path_from_slash_separated(path: &str) -> PathBuf {
245 path.split(['/', '\\'])
246 .filter(|part| !part.is_empty())
247 .collect()
248}
249
250fn project_manifest_path(project_root: Option<&Path>, source_root: &str) -> Option<PathBuf> {
251 if let Some(project_root) = project_root {
252 return Some(project_root.join("Cargo.toml"));
253 }
254
255 let source_root = path_from_slash_separated(source_root);
256 source_root
257 .parent()
258 .filter(|parent| !parent.as_os_str().is_empty())
259 .map(|parent| parent.join("Cargo.toml"))
260}
261
262fn cargo_run_command(manifest_path: Option<&Path>, child_process_args: &[String]) -> String {
263 let mut parts = vec!["run".to_string()];
264
265 if let Some(manifest_path) = manifest_path {
266 parts.push("--manifest-path".to_string());
267 parts.push(quote_command_arg(&manifest_path.display().to_string()));
268 }
269
270 if !child_process_args.is_empty() {
271 parts.push("--".to_string());
272 parts.extend(child_process_args.iter().map(|arg| quote_command_arg(arg)));
273 }
274
275 parts.join(" ")
276}
277
278fn quote_command_arg(value: &str) -> String {
279 if value.is_empty() || value.chars().any(char::is_whitespace) || value.contains('"') {
280 format!("\"{}\"", value.replace('"', "\\\""))
281 } else {
282 value.to_string()
283 }
284}