use std::path::PathBuf;
use crate::actions::abstract_action::AbstractAction;
use crate::actions::{ActionInvocation, ActionKind, ActionSpec, action_spec};
use crate::commands::{Input, InputValue};
use crate::compiler::{
BuildCommand, BuildPlan, BuildPlanRequest, BuilderVariant, CompilerCommandOptions,
create_build_plan,
};
use crate::configuration::{CompilerOptions, Configuration, ProjectConfiguration};
use crate::{CliError, Result};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct BuildAction;
impl BuildAction {
pub const fn new() -> Self {
Self
}
pub fn spec(&self) -> &'static ActionSpec {
action_spec(ActionKind::Build).expect("build action spec")
}
pub fn handle_invocation(&self, inputs: Vec<Input>, options: Vec<Input>) -> ActionInvocation {
<Self as AbstractAction>::handle(self, inputs, options, Vec::new())
}
pub fn create_plan(&self, request: BuildActionPlanRequest) -> Result<BuildActionPlan> {
create_build_action_plan(request)
}
}
impl AbstractAction for BuildAction {
fn kind(&self) -> ActionKind {
ActionKind::Build
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BuildActionPlanRequest {
pub cwd: PathBuf,
pub configuration: Configuration,
pub command_inputs: Vec<Input>,
pub command_options: Vec<Input>,
pub ts_build_info_file: Option<PathBuf>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BuildActionPlan {
pub config_file_name: Option<String>,
pub watch_mode: bool,
pub watch_assets_mode: bool,
pub app_names: Vec<Option<String>>,
pub project_plans: Vec<ProjectBuildActionPlan>,
pub type_check_warnings: Vec<TypeCheckWarning>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProjectBuildActionPlan {
pub app_name: Option<String>,
pub project_root: Option<PathBuf>,
pub build_plan: BuildPlan,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TypeCheckWarning {
pub app_name: Option<String>,
pub builder: BuilderVariant,
pub message: String,
}
pub fn create_build_action_plan(request: BuildActionPlanRequest) -> Result<BuildActionPlan> {
let config_file_name = string_option(&request.command_options, "config");
let watch_mode = bool_option(&request.command_options, "watch");
let watch_assets_mode = bool_option(&request.command_options, "watchAssets");
let build_all = bool_option(&request.command_options, "all");
let compiler_command_options = compiler_command_options(&request.command_options)?;
let app_names = resolve_app_names(&request.configuration, &request.command_inputs, build_all);
let mut project_plans = Vec::with_capacity(app_names.len());
let mut type_check_warnings = Vec::new();
for app_name in &app_names {
let project = project_configuration(&request.configuration, app_name.as_deref());
let project_root = project.root.as_ref().map(PathBuf::from);
let compiler_options = compiler_options(&request.configuration, app_name.as_deref());
let command = BuildCommand {
apps: app_name.iter().cloned().collect(),
options: compiler_command_options.clone(),
};
let build_plan = create_build_plan(BuildPlanRequest {
cwd: request.cwd.clone(),
command,
project,
compiler_options,
ts_build_info_file: request.ts_build_info_file.clone(),
});
if build_plan.inputs.type_check && build_plan.inputs.builder != BuilderVariant::Swc {
type_check_warnings.push(TypeCheckWarning {
app_name: app_name.clone(),
builder: build_plan.inputs.builder,
message: "\"typeCheck\" will not have any effect when \"builder\" is not \"swc\"."
.to_string(),
});
}
project_plans.push(ProjectBuildActionPlan {
app_name: app_name.clone(),
project_root,
build_plan,
});
}
Ok(BuildActionPlan {
config_file_name,
watch_mode,
watch_assets_mode,
app_names,
project_plans,
type_check_warnings,
})
}
pub(crate) fn compiler_command_options(options: &[Input]) -> Result<CompilerCommandOptions> {
Ok(CompilerCommandOptions {
path: string_option(options, "path"),
webpack: bool_option_value(options, "webpack"),
webpack_path: string_option(options, "webpackPath"),
builder: builder_option(options)?,
watch: bool_option_value(options, "watch"),
watch_assets: bool_option_value(options, "watchAssets"),
type_check: bool_option_value(options, "typeCheck"),
preserve_watch_output: bool_option_value(options, "preserveWatchOutput"),
})
}
pub(crate) fn resolve_app_names(
configuration: &Configuration,
command_inputs: &[Input],
build_all: bool,
) -> Vec<Option<String>> {
let mut app_names = if build_all {
configuration
.projects
.keys()
.cloned()
.map(Some)
.collect::<Vec<_>>()
} else {
command_inputs
.iter()
.filter(|input| input.name == "app")
.map(|input| string_input_value(input.value.as_ref()))
.collect::<Vec<_>>()
};
if app_names.is_empty() {
app_names.push(None);
}
app_names
}
pub(crate) fn project_configuration(
configuration: &Configuration,
app_name: Option<&str>,
) -> ProjectConfiguration {
app_name
.and_then(|name| configuration.projects.get(name))
.cloned()
.unwrap_or_else(|| ProjectConfiguration {
entry_file: Some(configuration.entry_file.clone()),
exec: Some(configuration.exec.clone()),
source_root: Some(configuration.source_root.clone()),
compiler_options: Some(configuration.compiler_options.clone()),
..ProjectConfiguration::default()
})
}
pub(crate) fn compiler_options(
configuration: &Configuration,
app_name: Option<&str>,
) -> CompilerOptions {
app_name
.and_then(|name| configuration.projects.get(name))
.and_then(|project| project.compiler_options.clone())
.unwrap_or_else(|| configuration.compiler_options.clone())
}
pub(crate) fn bool_option(options: &[Input], name: &str) -> bool {
matches!(
options
.iter()
.find(|option| option.name == name)
.and_then(|option| option.value.as_ref()),
Some(InputValue::Bool(true))
)
}
pub(crate) fn bool_option_value(options: &[Input], name: &str) -> Option<bool> {
options
.iter()
.find(|option| option.name == name)
.and_then(|option| match option.value.as_ref() {
Some(InputValue::Bool(value)) => Some(*value),
_ => None,
})
}
pub(crate) fn string_option(options: &[Input], name: &str) -> Option<String> {
options
.iter()
.find(|option| option.name == name)
.and_then(|option| string_input_value(option.value.as_ref()))
}
pub(crate) fn string_list_option(options: &[Input], name: &str) -> Vec<String> {
options
.iter()
.find(|option| option.name == name)
.and_then(|option| match option.value.as_ref() {
Some(InputValue::StringList(values)) => Some(values.clone()),
_ => None,
})
.unwrap_or_default()
}
fn builder_option(options: &[Input]) -> Result<Option<BuilderVariant>> {
string_option(options, "builder")
.map(|builder| match builder.as_str() {
"cargo" => Ok(BuilderVariant::Cargo),
"tsc" => Ok(BuilderVariant::Tsc),
"swc" => Ok(BuilderVariant::Swc),
"webpack" => Ok(BuilderVariant::Webpack),
_ => Err(CliError::UnsupportedCommand(format!(
"Invalid builder option: {builder}. Available builder: cargo"
))),
})
.transpose()
}
fn string_input_value(value: Option<&InputValue>) -> Option<String> {
match value {
Some(InputValue::String(value)) => Some(value.clone()),
_ => None,
}
}