1use std::path::PathBuf;
4
5use crate::actions::abstract_action::AbstractAction;
6use crate::actions::{ActionInvocation, ActionKind, ActionSpec, action_spec};
7use crate::commands::{Input, InputValue};
8use crate::compiler::{
9 BuildCommand, BuildPlan, BuildPlanRequest, BuilderVariant, CompilerCommandOptions,
10 create_build_plan,
11};
12use crate::configuration::{CompilerOptions, Configuration, ProjectConfiguration};
13use crate::{CliError, Result};
14
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
17pub struct BuildAction;
18
19impl BuildAction {
20 pub const fn new() -> Self {
21 Self
22 }
23
24 pub fn spec(&self) -> &'static ActionSpec {
25 action_spec(ActionKind::Build).expect("build action spec")
26 }
27
28 pub fn handle_invocation(&self, inputs: Vec<Input>, options: Vec<Input>) -> ActionInvocation {
29 <Self as AbstractAction>::handle(self, inputs, options, Vec::new())
30 }
31
32 pub fn create_plan(&self, request: BuildActionPlanRequest) -> Result<BuildActionPlan> {
33 create_build_action_plan(request)
34 }
35}
36
37impl AbstractAction for BuildAction {
38 fn kind(&self) -> ActionKind {
39 ActionKind::Build
40 }
41}
42
43#[derive(Clone, Debug, PartialEq, Eq)]
44pub struct BuildActionPlanRequest {
45 pub cwd: PathBuf,
46 pub configuration: Configuration,
47 pub command_inputs: Vec<Input>,
48 pub command_options: Vec<Input>,
49 pub ts_build_info_file: Option<PathBuf>,
50}
51
52#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct BuildActionPlan {
54 pub config_file_name: Option<String>,
55 pub watch_mode: bool,
56 pub watch_assets_mode: bool,
57 pub app_names: Vec<Option<String>>,
58 pub project_plans: Vec<ProjectBuildActionPlan>,
59 pub type_check_warnings: Vec<TypeCheckWarning>,
60}
61
62#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct ProjectBuildActionPlan {
64 pub app_name: Option<String>,
65 pub project_root: Option<PathBuf>,
66 pub build_plan: BuildPlan,
67}
68
69#[derive(Clone, Debug, PartialEq, Eq)]
70pub struct TypeCheckWarning {
71 pub app_name: Option<String>,
72 pub builder: BuilderVariant,
73 pub message: String,
74}
75
76pub fn create_build_action_plan(request: BuildActionPlanRequest) -> Result<BuildActionPlan> {
77 let config_file_name = string_option(&request.command_options, "config");
78 let watch_mode = bool_option(&request.command_options, "watch");
79 let watch_assets_mode = bool_option(&request.command_options, "watchAssets");
80 let build_all = bool_option(&request.command_options, "all");
81 let compiler_command_options = compiler_command_options(&request.command_options)?;
82 let app_names = resolve_app_names(&request.configuration, &request.command_inputs, build_all);
83
84 let mut project_plans = Vec::with_capacity(app_names.len());
85 let mut type_check_warnings = Vec::new();
86
87 for app_name in &app_names {
88 let project = project_configuration(&request.configuration, app_name.as_deref());
89 let project_root = project.root.as_ref().map(PathBuf::from);
90 let compiler_options = compiler_options(&request.configuration, app_name.as_deref());
91 let command = BuildCommand {
92 apps: app_name.iter().cloned().collect(),
93 options: compiler_command_options.clone(),
94 };
95
96 let build_plan = create_build_plan(BuildPlanRequest {
97 cwd: request.cwd.clone(),
98 command,
99 project,
100 compiler_options,
101 ts_build_info_file: request.ts_build_info_file.clone(),
102 });
103
104 if build_plan.inputs.type_check && build_plan.inputs.builder != BuilderVariant::Swc {
105 type_check_warnings.push(TypeCheckWarning {
106 app_name: app_name.clone(),
107 builder: build_plan.inputs.builder,
108 message: "\"typeCheck\" will not have any effect when \"builder\" is not \"swc\"."
109 .to_string(),
110 });
111 }
112
113 project_plans.push(ProjectBuildActionPlan {
114 app_name: app_name.clone(),
115 project_root,
116 build_plan,
117 });
118 }
119
120 Ok(BuildActionPlan {
121 config_file_name,
122 watch_mode,
123 watch_assets_mode,
124 app_names,
125 project_plans,
126 type_check_warnings,
127 })
128}
129
130pub(crate) fn compiler_command_options(options: &[Input]) -> Result<CompilerCommandOptions> {
131 Ok(CompilerCommandOptions {
132 path: string_option(options, "path"),
133 webpack: bool_option_value(options, "webpack"),
134 webpack_path: string_option(options, "webpackPath"),
135 builder: builder_option(options)?,
136 watch: bool_option_value(options, "watch"),
137 watch_assets: bool_option_value(options, "watchAssets"),
138 type_check: bool_option_value(options, "typeCheck"),
139 preserve_watch_output: bool_option_value(options, "preserveWatchOutput"),
140 })
141}
142
143pub(crate) fn resolve_app_names(
144 configuration: &Configuration,
145 command_inputs: &[Input],
146 build_all: bool,
147) -> Vec<Option<String>> {
148 let mut app_names = if build_all {
149 configuration
150 .projects
151 .keys()
152 .cloned()
153 .map(Some)
154 .collect::<Vec<_>>()
155 } else {
156 command_inputs
157 .iter()
158 .filter(|input| input.name == "app")
159 .map(|input| string_input_value(input.value.as_ref()))
160 .collect::<Vec<_>>()
161 };
162
163 if app_names.is_empty() {
164 app_names.push(None);
165 }
166
167 app_names
168}
169
170pub(crate) fn project_configuration(
171 configuration: &Configuration,
172 app_name: Option<&str>,
173) -> ProjectConfiguration {
174 app_name
175 .and_then(|name| configuration.projects.get(name))
176 .cloned()
177 .unwrap_or_else(|| ProjectConfiguration {
178 entry_file: Some(configuration.entry_file.clone()),
179 exec: Some(configuration.exec.clone()),
180 source_root: Some(configuration.source_root.clone()),
181 compiler_options: Some(configuration.compiler_options.clone()),
182 ..ProjectConfiguration::default()
183 })
184}
185
186pub(crate) fn compiler_options(
187 configuration: &Configuration,
188 app_name: Option<&str>,
189) -> CompilerOptions {
190 app_name
191 .and_then(|name| configuration.projects.get(name))
192 .and_then(|project| project.compiler_options.clone())
193 .unwrap_or_else(|| configuration.compiler_options.clone())
194}
195
196pub(crate) fn bool_option(options: &[Input], name: &str) -> bool {
197 matches!(
198 options
199 .iter()
200 .find(|option| option.name == name)
201 .and_then(|option| option.value.as_ref()),
202 Some(InputValue::Bool(true))
203 )
204}
205
206pub(crate) fn bool_option_value(options: &[Input], name: &str) -> Option<bool> {
207 options
208 .iter()
209 .find(|option| option.name == name)
210 .and_then(|option| match option.value.as_ref() {
211 Some(InputValue::Bool(value)) => Some(*value),
212 _ => None,
213 })
214}
215
216pub(crate) fn string_option(options: &[Input], name: &str) -> Option<String> {
217 options
218 .iter()
219 .find(|option| option.name == name)
220 .and_then(|option| string_input_value(option.value.as_ref()))
221}
222
223pub(crate) fn string_list_option(options: &[Input], name: &str) -> Vec<String> {
224 options
225 .iter()
226 .find(|option| option.name == name)
227 .and_then(|option| match option.value.as_ref() {
228 Some(InputValue::StringList(values)) => Some(values.clone()),
229 _ => None,
230 })
231 .unwrap_or_default()
232}
233
234fn builder_option(options: &[Input]) -> Result<Option<BuilderVariant>> {
235 string_option(options, "builder")
236 .map(|builder| match builder.as_str() {
237 "cargo" => Ok(BuilderVariant::Cargo),
238 "tsc" => Ok(BuilderVariant::Tsc),
239 "swc" => Ok(BuilderVariant::Swc),
240 "webpack" => Ok(BuilderVariant::Webpack),
241 _ => Err(CliError::UnsupportedCommand(format!(
242 "Invalid builder option: {builder}. Available builder: cargo"
243 ))),
244 })
245 .transpose()
246}
247
248fn string_input_value(value: Option<&InputValue>) -> Option<String> {
249 match value {
250 Some(InputValue::String(value)) => Some(value.clone()),
251 _ => None,
252 }
253}