use std::path::{Path, PathBuf};
use crate::Result;
use crate::actions::abstract_action::AbstractAction;
use crate::actions::{ActionInvocation, ActionKind, ActionSpec, action_spec};
use crate::build_action::{
BuildActionPlan, BuildActionPlanRequest, create_build_action_plan, project_configuration,
string_list_option, string_option,
};
use crate::commands::{Input, InputValue};
use crate::configuration::{Configuration, DEFAULT_ENTRY_FILE, DEFAULT_SOURCE_ROOT};
use crate::runners::RunnerCommand;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct StartAction;
impl StartAction {
pub const fn new() -> Self {
Self
}
pub fn spec(&self) -> &'static ActionSpec {
action_spec(ActionKind::Start).expect("start action spec")
}
pub fn handle_invocation(
&self,
inputs: Vec<Input>,
options: Vec<Input>,
extra_flags: Vec<String>,
) -> ActionInvocation {
<Self as AbstractAction>::handle(self, inputs, options, extra_flags)
}
pub fn create_plan(&self, request: StartActionPlanRequest) -> Result<StartActionPlan> {
create_start_action_plan(request)
}
}
impl AbstractAction for StartAction {
fn kind(&self) -> ActionKind {
ActionKind::Start
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StartActionPlanRequest {
pub cwd: PathBuf,
pub configuration: Configuration,
pub command_inputs: Vec<Input>,
pub command_options: Vec<Input>,
pub extra_flags: Vec<String>,
pub ts_build_info_file: Option<PathBuf>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StartActionPlan {
pub app_name: Option<String>,
pub build_plan: BuildActionPlan,
pub process_plan: StartProcessPlan,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StartProcessPlan {
pub entry_file: String,
pub source_root: String,
pub debug_flag: Option<DebugFlag>,
pub out_dir_name: PathBuf,
pub binary_to_run: String,
pub requested_exec: Option<String>,
pub shell: bool,
pub env_file: Vec<String>,
pub enable_source_maps: bool,
pub child_process_args: Vec<String>,
pub manifest_path: Option<PathBuf>,
pub source_root_output: PathBuf,
pub fallback_output: PathBuf,
pub source_root_command: RunnerCommand,
pub fallback_command: RunnerCommand,
pub restart: StartRestartPlan,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DebugFlag {
Inspect,
InspectAddress(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StartRestartPlan {
pub kill_previous_process_on_success: bool,
pub forward_sigint: bool,
pub forward_sigterm: bool,
pub kill_process_on_parent_exit: bool,
}
pub fn create_start_action_plan(request: StartActionPlanRequest) -> Result<StartActionPlan> {
let cwd = request.cwd.clone();
let app_name = request
.command_inputs
.iter()
.find(|input| input.name == "app")
.and_then(|input| match input.value.as_ref() {
Some(InputValue::String(value)) => Some(value.clone()),
_ => None,
});
let project = project_configuration(&request.configuration, app_name.as_deref());
let build_plan = create_build_action_plan(BuildActionPlanRequest {
cwd: request.cwd,
configuration: request.configuration,
command_inputs: request.command_inputs,
command_options: request.command_options.clone(),
ts_build_info_file: request.ts_build_info_file,
})?;
let project_build_plan = build_plan
.project_plans
.iter()
.find(|plan| plan.app_name == app_name)
.or_else(|| build_plan.project_plans.first());
let out_dir_name = project_build_plan
.map(|plan| plan.build_plan.inputs.output_dir.clone())
.unwrap_or_else(|| PathBuf::from("dist"));
let entry_file = string_option(&request.command_options, "entryFile")
.or(project.entry_file)
.unwrap_or_else(|| DEFAULT_ENTRY_FILE.to_string());
let source_root = string_option(&request.command_options, "sourceRoot")
.or(project.source_root)
.unwrap_or_else(|| DEFAULT_SOURCE_ROOT.to_string());
let requested_exec = string_option(&request.command_options, "exec");
let binary_to_run = "cargo".to_string();
let debug_flag = debug_flag(&request.command_options);
let shell = bool_option_default(&request.command_options, "shell", true);
let env_file = string_list_option(&request.command_options, "envFile");
let child_process_args = request.extra_flags;
let manifest_path = project_manifest_path(
project_build_plan.and_then(|plan| plan.project_root.as_deref()),
&source_root,
);
let process_plan = create_start_process_plan(
entry_file,
source_root,
debug_flag,
out_dir_name,
binary_to_run,
requested_exec,
shell,
env_file,
child_process_args,
manifest_path,
cwd,
);
Ok(StartActionPlan {
app_name,
build_plan,
process_plan,
})
}
pub fn create_start_process_plan(
entry_file: String,
source_root: String,
debug_flag: Option<DebugFlag>,
out_dir_name: PathBuf,
binary_to_run: String,
requested_exec: Option<String>,
shell: bool,
env_file: Vec<String>,
child_process_args: Vec<String>,
manifest_path: Option<PathBuf>,
cwd: PathBuf,
) -> StartProcessPlan {
let source_root_output = out_dir_name
.join(path_from_slash_separated(&source_root))
.join(&entry_file);
let fallback_output = out_dir_name.join(&entry_file);
let command = cargo_run_command(manifest_path.as_deref(), &child_process_args);
let runner_command = RunnerCommand {
binary: "cargo".to_string(),
prefix_args: Vec::new(),
command,
collect: false,
cwd: Some(cwd),
shell: true,
env: Vec::new(),
};
StartProcessPlan {
entry_file,
source_root,
debug_flag,
out_dir_name,
binary_to_run,
requested_exec,
shell,
env_file,
enable_source_maps: false,
child_process_args,
manifest_path,
source_root_output,
fallback_output,
source_root_command: runner_command.clone(),
fallback_command: runner_command,
restart: StartRestartPlan {
kill_previous_process_on_success: true,
forward_sigint: true,
forward_sigterm: true,
kill_process_on_parent_exit: true,
},
}
}
fn debug_flag(options: &[Input]) -> Option<DebugFlag> {
options
.iter()
.find(|option| option.name == "debug")
.and_then(|option| match option.value.as_ref() {
Some(InputValue::Bool(true)) => Some(DebugFlag::Inspect),
Some(InputValue::String(value)) if value.is_empty() => Some(DebugFlag::Inspect),
Some(InputValue::String(value)) => Some(DebugFlag::InspectAddress(value.clone())),
_ => None,
})
}
fn bool_option_default(options: &[Input], name: &str, default: bool) -> bool {
options
.iter()
.find(|option| option.name == name)
.and_then(|option| match option.value.as_ref() {
Some(InputValue::Bool(value)) => Some(*value),
_ => None,
})
.unwrap_or(default)
}
fn path_from_slash_separated(path: &str) -> PathBuf {
path.split(['/', '\\'])
.filter(|part| !part.is_empty())
.collect()
}
fn project_manifest_path(project_root: Option<&Path>, source_root: &str) -> Option<PathBuf> {
if let Some(project_root) = project_root {
return Some(project_root.join("Cargo.toml"));
}
let source_root = path_from_slash_separated(source_root);
source_root
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.map(|parent| parent.join("Cargo.toml"))
}
fn cargo_run_command(manifest_path: Option<&Path>, child_process_args: &[String]) -> String {
let mut parts = vec!["run".to_string()];
if let Some(manifest_path) = manifest_path {
parts.push("--manifest-path".to_string());
parts.push(quote_command_arg(&manifest_path.display().to_string()));
}
if !child_process_args.is_empty() {
parts.push("--".to_string());
parts.extend(child_process_args.iter().map(|arg| quote_command_arg(arg)));
}
parts.join(" ")
}
fn quote_command_arg(value: &str) -> String {
if value.is_empty() || value.chars().any(char::is_whitespace) || value.contains('"') {
format!("\"{}\"", value.replace('"', "\\\""))
} else {
value.to_string()
}
}