Skip to main content

cli/actions/
build_action.rs

1//! Pure plan builder for upstream `nest-cli/actions/build.action.ts`.
2
3use 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/// Typed wrapper for upstream `BuildAction`.
16#[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}