1use 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#[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}