Skip to main content

cli/actions/
start_action.rs

1//! Pure plan builder for Rust `nest start` execution.
2
3use 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/// Typed wrapper for upstream `StartAction`.
17#[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}