Skip to main content

cli/actions/
add_action.rs

1//! Port plan for upstream `../nest-cli/actions/add.action.ts`.
2
3use crate::actions::abstract_action::AbstractAction;
4use crate::actions::{ActionInvocation, ActionKind, ActionSpec, action_spec};
5use crate::commands::Input;
6use crate::configuration::Configuration;
7use crate::package_managers::{PackageManager, PackageManagerClient};
8pub use crate::runners::schematic_runner::SCHEMATICS_CLI_RELATIVE_PATH;
9pub use crate::runners::schematic_runner::find_closest_schematics_binary;
10use crate::runners::{Runner, RunnerCommand, RunnerFactory};
11use crate::schematics::{NESTJS_COLLECTION_NAME, SchematicOption};
12use schematics::nest_add::{has_native_nest_add, unsupported_native_nest_add_reason};
13use std::path::{Path, PathBuf};
14
15pub const SCHEMATIC_NAME: &str = "nest-add";
16/// Typed wrapper for upstream `AddAction`.
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
18pub struct AddAction;
19
20impl AddAction {
21    pub const fn new() -> Self {
22        Self
23    }
24
25    pub fn spec(&self) -> &'static ActionSpec {
26        action_spec(ActionKind::Add).expect("add 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: AddActionRequest) -> AddActionPlan {
39        create_add_action_plan(request)
40    }
41}
42
43impl AbstractAction for AddAction {
44    fn kind(&self) -> ActionKind {
45        ActionKind::Add
46    }
47}
48
49#[derive(Clone, Debug, PartialEq, Eq)]
50pub struct AddActionRequest {
51    pub library: String,
52    pub skip_install: bool,
53    pub dry_run: bool,
54    pub project: Option<String>,
55    pub source_root: Option<String>,
56    pub extra_flags: Vec<String>,
57    pub package_manager: PackageManager,
58    pub configuration: Configuration,
59}
60
61#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct AddActionPlan {
63    pub library_name: String,
64    pub package_name: String,
65    pub collection_name: String,
66    pub tag_name: String,
67    pub source_root: String,
68    pub install_command: Option<RunnerCommand>,
69    pub schematic_command: String,
70    pub dry_run: bool,
71}
72
73#[derive(Clone, Debug, PartialEq, Eq)]
74pub struct AddSchematicExecutionPlan {
75    pub schematics_binary: PathBuf,
76    pub command: RunnerCommand,
77}
78
79#[derive(Clone, Debug, PartialEq, Eq)]
80pub enum AddExecutionPlan {
81    Native(AddNativeExecutionPlan),
82    Node(AddSchematicExecutionPlan),
83}
84
85#[derive(Clone, Debug, PartialEq, Eq)]
86pub struct AddNativeExecutionPlan {
87    pub collection_name: String,
88    pub source_root: String,
89    pub extra_flags: Vec<String>,
90    pub dry_run: bool,
91}
92
93pub fn create_add_action_plan(request: AddActionRequest) -> AddActionPlan {
94    let package_name = get_package_name(&request.library);
95    let collection_name = get_collection_name(&request.library, &package_name);
96    let tag_name = get_tag_name(&request.library)
97        .filter(|tag| !tag.is_empty())
98        .unwrap_or_else(|| "latest".to_string());
99    let source_root = resolve_source_root(
100        request.source_root,
101        request.project.as_deref(),
102        &request.configuration,
103    );
104    let install_command = (!request.skip_install
105        && !request.dry_run
106        && !has_native_nest_add(&collection_name))
107    .then(|| {
108        PackageManagerClient::new(request.package_manager)
109            .add_production_command(&[collection_name.as_str()], &tag_name)
110    });
111    let mut options = vec![SchematicOption::new("sourceRoot", source_root.as_str())];
112    if request.dry_run {
113        options.push(SchematicOption::new("dry-run", true));
114    }
115    let mut schematic_command = format!(
116        "{collection_name}:{SCHEMATIC_NAME}{}",
117        options.iter().fold(String::new(), |mut output, option| {
118            output.push(' ');
119            output.push_str(&option.to_command_string());
120            output
121        })
122    );
123    if !request.extra_flags.is_empty() {
124        schematic_command.push(' ');
125        schematic_command.push_str(&request.extra_flags.join(" "));
126    }
127
128    AddActionPlan {
129        library_name: request.library,
130        package_name,
131        collection_name,
132        tag_name,
133        source_root,
134        install_command,
135        schematic_command,
136        dry_run: request.dry_run,
137    }
138}
139
140pub fn create_add_schematic_execution_plan(
141    plan: &AddActionPlan,
142    cwd: impl AsRef<Path>,
143) -> Result<AddSchematicExecutionPlan, String> {
144    let cwd = cwd.as_ref();
145    let schematics_binary = find_closest_schematics_binary(cwd)?;
146    let command = RunnerFactory::create_schematic(&schematics_binary).describe(
147        &plan.schematic_command,
148        false,
149        Some(cwd.to_path_buf()),
150    );
151
152    Ok(AddSchematicExecutionPlan {
153        schematics_binary,
154        command,
155    })
156}
157
158pub fn create_add_execution_plan(
159    plan: &AddActionPlan,
160    extra_flags: &[String],
161    cwd: impl AsRef<Path>,
162) -> Result<AddExecutionPlan, String> {
163    if has_native_nest_add(&plan.collection_name) {
164        return Ok(AddExecutionPlan::Native(AddNativeExecutionPlan {
165            collection_name: plan.collection_name.clone(),
166            source_root: plan.source_root.clone(),
167            extra_flags: extra_flags.to_vec(),
168            dry_run: plan.dry_run,
169        }));
170    }
171
172    if let Some(reason) = unsupported_native_nest_add_reason(&plan.collection_name) {
173        return Err(reason.to_string());
174    }
175
176    create_add_schematic_execution_plan(plan, cwd).map(AddExecutionPlan::Node)
177}
178
179pub fn has_native_add_handler(collection_name: &str) -> bool {
180    has_native_nest_add(collection_name)
181}
182
183pub fn unsupported_native_add_reason(collection_name: &str) -> Option<&'static str> {
184    unsupported_native_nest_add_reason(collection_name)
185}
186
187pub fn get_package_name(library: &str) -> String {
188    let end = package_end_index(library);
189    library[..end].to_string()
190}
191
192pub fn get_collection_name(library: &str, package_name: &str) -> String {
193    if let Some(tag_start) = package_tag_index(library) {
194        let tag_end = library[tag_start + 1..]
195            .find('/')
196            .map(|index| tag_start + 1 + index)
197            .unwrap_or(library.len());
198        format!("{}{}", &library[..tag_start], &library[tag_end..])
199    } else if let Some(suffix) = library.strip_prefix(package_name) {
200        format!("{package_name}{suffix}")
201    } else {
202        library.to_string()
203    }
204}
205
206pub fn get_tag_name(library: &str) -> Option<String> {
207    package_tag_index(library).and_then(|tag_start| {
208        let tag = library[tag_start + 1..]
209            .split('/')
210            .next()
211            .unwrap_or_default();
212        (!tag.is_empty()).then(|| tag.to_string())
213    })
214}
215
216fn package_end_index(library: &str) -> usize {
217    if library.starts_with('@') {
218        let slash = match library.find('/') {
219            Some(index) => index,
220            None => return library.len(),
221        };
222        let after_name = slash + 1;
223        let scoped_name_len = library[after_name..]
224            .find(['@', '/'])
225            .map(|index| after_name + index)
226            .unwrap_or(library.len());
227        scoped_name_len
228    } else {
229        library.find(['@', '/']).unwrap_or(library.len())
230    }
231}
232
233fn package_tag_index(library: &str) -> Option<usize> {
234    if library.starts_with('@') {
235        let slash = library.find('/')?;
236        library[slash + 1..]
237            .find('@')
238            .map(|index| slash + 1 + index)
239    } else {
240        library.find('@')
241    }
242}
243
244fn resolve_source_root(
245    source_root: Option<String>,
246    project: Option<&str>,
247    configuration: &Configuration,
248) -> String {
249    if let Some(source_root) = source_root {
250        return source_root;
251    }
252
253    if let Some(project) = project {
254        if let Some(project_configuration) = configuration.projects.get(project) {
255            if let Some(source_root) = &project_configuration.source_root {
256                return source_root.clone();
257            }
258        }
259    }
260
261    configuration.source_root.clone()
262}
263
264pub fn default_collection_name() -> &'static str {
265    NESTJS_COLLECTION_NAME
266}