1use crate::error::{CliError, Result};
4use crate::readers::{FileSystemReader, Reader};
5use serde::{Deserialize, Deserializer, Serialize};
6use serde_json::Value;
7use std::cell::RefCell;
8use std::collections::BTreeMap;
9use std::path::Path;
10
11pub const DEFAULT_LANGUAGE: &str = "rs";
12pub const DEFAULT_SOURCE_ROOT: &str = "src";
13pub const DEFAULT_COLLECTION: &str = "@nestrs/schematics";
14pub const DEFAULT_ENTRY_FILE: &str = "main";
15pub const DEFAULT_EXEC: &str = "cargo";
16pub const DEFAULT_TSCONFIG_FILENAME: &str = "tsconfig.json";
17pub const DEFAULT_WEBPACK_CONFIG_FILENAME: &str = "webpack.config.js";
18pub const DEFAULT_OUT_DIR: &str = "dist";
19
20pub mod configuration;
21pub mod configuration_loader;
22pub mod defaults;
23pub mod nest_configuration_loader;
24
25#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
26#[serde(untagged)]
27pub enum Asset {
28 Glob(String),
29 Entry(AssetEntry),
30}
31
32#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
33#[serde(rename_all = "camelCase")]
34pub struct ActionOnFile {
35 pub action: String,
36 pub item: AssetEntry,
37 pub path: String,
38 pub source_root: String,
39 pub watch_assets_mode: bool,
40}
41
42#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
43#[serde(rename_all = "camelCase")]
44pub struct AssetEntry {
45 pub glob: String,
46 pub include: Option<String>,
47 pub flat: Option<bool>,
48 pub exclude: Option<String>,
49 pub out_dir: Option<String>,
50 pub watch_assets: Option<bool>,
51}
52
53#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
54#[serde(default)]
55#[serde(rename_all = "camelCase")]
56pub struct SwcBuilderOptions {
57 pub swcrc_path: Option<String>,
58 pub out_dir: Option<String>,
59 pub filenames: Vec<String>,
60 pub sync: Option<bool>,
61 pub extensions: Vec<String>,
62 pub copy_files: Option<bool>,
63 pub include_dotfiles: Option<bool>,
64 pub quiet: Option<bool>,
65}
66
67#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
68#[serde(rename_all = "camelCase")]
69pub struct WebpackBuilderOptions {
70 pub config_path: Option<String>,
71}
72
73#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
74#[serde(rename_all = "camelCase")]
75pub struct TscBuilderOptions {
76 pub config_path: Option<String>,
77}
78
79#[derive(Clone, Debug, PartialEq, Eq)]
80pub enum Builder {
81 Cargo,
82 Tsc(TscBuilderOptions),
83 Swc(SwcBuilderOptions),
84 Webpack(WebpackBuilderOptions),
85}
86
87impl Default for Builder {
88 fn default() -> Self {
89 Self::Cargo
90 }
91}
92
93impl<'de> Deserialize<'de> for Builder {
94 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
95 where
96 D: Deserializer<'de>,
97 {
98 #[derive(Deserialize)]
99 #[serde(rename_all = "lowercase")]
100 enum BuilderVariant {
101 Cargo,
102 Tsc,
103 Swc,
104 Webpack,
105 }
106
107 #[derive(Deserialize)]
108 struct BuilderObject {
109 #[serde(rename = "type")]
110 builder_type: BuilderVariant,
111 #[serde(default = "empty_builder_options")]
112 options: Value,
113 }
114
115 fn empty_builder_options() -> Value {
116 Value::Object(Default::default())
117 }
118
119 #[derive(Deserialize)]
120 #[serde(untagged)]
121 enum BuilderInput {
122 Variant(BuilderVariant),
123 Object(BuilderObject),
124 }
125
126 let input = BuilderInput::deserialize(deserializer)?;
127 match input {
128 BuilderInput::Variant(BuilderVariant::Cargo) => Ok(Self::Cargo),
129 BuilderInput::Variant(BuilderVariant::Tsc) => {
130 Ok(Self::Tsc(TscBuilderOptions::default()))
131 }
132 BuilderInput::Variant(BuilderVariant::Swc) => {
133 Ok(Self::Swc(SwcBuilderOptions::default()))
134 }
135 BuilderInput::Variant(BuilderVariant::Webpack) => {
136 Ok(Self::Webpack(WebpackBuilderOptions::default()))
137 }
138 BuilderInput::Object(object) => match object.builder_type {
139 BuilderVariant::Cargo => Ok(Self::Cargo),
140 BuilderVariant::Tsc => serde_json::from_value(object.options)
141 .map(Self::Tsc)
142 .map_err(serde::de::Error::custom),
143 BuilderVariant::Swc => serde_json::from_value(object.options)
144 .map(Self::Swc)
145 .map_err(serde::de::Error::custom),
146 BuilderVariant::Webpack => serde_json::from_value(object.options)
147 .map(Self::Webpack)
148 .map_err(serde::de::Error::custom),
149 },
150 }
151 }
152}
153
154impl Serialize for Builder {
155 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
156 where
157 S: serde::Serializer,
158 {
159 #[derive(Serialize)]
160 struct BuilderObject<'a, T> {
161 #[serde(rename = "type")]
162 builder_type: &'a str,
163 options: &'a T,
164 }
165
166 match self {
167 Self::Cargo => serializer.serialize_str("cargo"),
168 Self::Tsc(options) => BuilderObject {
169 builder_type: "tsc",
170 options,
171 }
172 .serialize(serializer),
173 Self::Swc(options) => BuilderObject {
174 builder_type: "swc",
175 options,
176 }
177 .serialize(serializer),
178 Self::Webpack(options) => BuilderObject {
179 builder_type: "webpack",
180 options,
181 }
182 .serialize(serializer),
183 }
184 }
185}
186
187#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
188#[serde(default)]
189#[serde(rename_all = "camelCase")]
190pub struct CompilerOptions {
191 pub ts_config_path: Option<String>,
192 pub webpack: bool,
193 pub webpack_config_path: Option<String>,
194 pub plugins: Vec<Plugin>,
195 pub assets: Vec<Asset>,
196 pub delete_out_dir: Option<bool>,
197 pub manual_restart: bool,
198 pub builder: Builder,
199}
200
201impl Default for CompilerOptions {
202 fn default() -> Self {
203 Self {
204 ts_config_path: None,
205 webpack: false,
206 webpack_config_path: None,
207 plugins: Vec::new(),
208 assets: Vec::new(),
209 delete_out_dir: None,
210 manual_restart: false,
211 builder: Builder::default(),
212 }
213 }
214}
215
216#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
217#[serde(untagged)]
218pub enum Plugin {
219 Name(String),
220 Options(PluginOptions),
221}
222
223#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
224pub struct PluginOptions {
225 pub name: String,
226 #[serde(default)]
227 #[serde(deserialize_with = "deserialize_plugin_options")]
228 pub options: Vec<BTreeMap<String, Value>>,
229}
230
231#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
232#[serde(untagged)]
233pub enum GenerateSpec {
234 Bool(bool),
235 BySchematic(BTreeMap<String, bool>),
236}
237
238#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
239#[serde(rename_all = "camelCase")]
240pub struct GenerateOptions {
241 pub spec: Option<GenerateSpec>,
242 pub flat: Option<bool>,
243 pub spec_file_suffix: Option<String>,
244 pub base_dir: Option<String>,
245}
246
247#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
248#[serde(rename_all = "camelCase")]
249pub struct ProjectConfiguration {
250 #[serde(rename = "type")]
251 pub project_type: Option<String>,
252 pub root: Option<String>,
253 pub entry_file: Option<String>,
254 pub exec: Option<String>,
255 pub source_root: Option<String>,
256 pub compiler_options: Option<CompilerOptions>,
257 pub generate_options: Option<GenerateOptions>,
258 #[serde(flatten)]
259 pub extra: BTreeMap<String, Value>,
260}
261
262#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
263#[serde(rename_all = "camelCase")]
264pub struct Configuration {
265 pub language: String,
266 pub collection: String,
267 pub source_root: String,
268 pub entry_file: String,
269 pub exec: String,
270 pub monorepo: bool,
271 pub compiler_options: CompilerOptions,
272 pub generate_options: GenerateOptions,
273 pub projects: BTreeMap<String, ProjectConfiguration>,
274 #[serde(flatten)]
275 pub extra: BTreeMap<String, Value>,
276}
277
278impl Default for Configuration {
279 fn default() -> Self {
280 Self {
281 language: DEFAULT_LANGUAGE.to_string(),
282 collection: DEFAULT_COLLECTION.to_string(),
283 source_root: DEFAULT_SOURCE_ROOT.to_string(),
284 entry_file: DEFAULT_ENTRY_FILE.to_string(),
285 exec: DEFAULT_EXEC.to_string(),
286 monorepo: false,
287 compiler_options: CompilerOptions::default(),
288 generate_options: GenerateOptions::default(),
289 projects: BTreeMap::new(),
290 extra: BTreeMap::new(),
291 }
292 }
293}
294
295impl Configuration {
296 pub fn default_in(directory: impl AsRef<Path>) -> Self {
297 let _ = directory;
298 Self::default()
299 }
300
301 pub fn merge(self, override_config: ConfigurationOverride) -> Self {
302 Self {
303 language: override_config.language.unwrap_or(self.language),
304 collection: override_config.collection.unwrap_or(self.collection),
305 source_root: override_config.source_root.unwrap_or(self.source_root),
306 entry_file: override_config.entry_file.unwrap_or(self.entry_file),
307 exec: override_config.exec.unwrap_or(self.exec),
308 monorepo: override_config.monorepo.unwrap_or(self.monorepo),
309 compiler_options: match override_config.compiler_options {
310 Some(compiler_options) => self.compiler_options.merge(compiler_options),
311 None => self.compiler_options,
312 },
313 generate_options: override_config
314 .generate_options
315 .unwrap_or(self.generate_options),
316 projects: if override_config.projects.is_empty() {
317 self.projects
318 } else {
319 override_config.projects
320 },
321 extra: merge_extra(self.extra, override_config.extra),
322 }
323 }
324}
325
326#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)]
327#[serde(default)]
328#[serde(rename_all = "camelCase")]
329pub struct ConfigurationOverride {
330 pub language: Option<String>,
331 pub collection: Option<String>,
332 pub source_root: Option<String>,
333 pub entry_file: Option<String>,
334 pub exec: Option<String>,
335 pub monorepo: Option<bool>,
336 pub compiler_options: Option<CompilerOptionsOverride>,
337 pub generate_options: Option<GenerateOptions>,
338 pub projects: BTreeMap<String, ProjectConfiguration>,
339 #[serde(flatten)]
340 pub extra: BTreeMap<String, Value>,
341}
342
343#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)]
344#[serde(rename_all = "camelCase")]
345pub struct CompilerOptionsOverride {
346 pub ts_config_path: Option<String>,
347 pub webpack: Option<bool>,
348 pub webpack_config_path: Option<String>,
349 pub plugins: Option<Vec<Plugin>>,
350 pub assets: Option<Vec<Asset>>,
351 pub delete_out_dir: Option<bool>,
352 pub manual_restart: Option<bool>,
353 pub builder: Option<Builder>,
354}
355
356impl CompilerOptions {
357 pub fn merge(self, override_options: CompilerOptionsOverride) -> Self {
358 Self {
359 ts_config_path: override_options.ts_config_path.or(self.ts_config_path),
360 webpack: override_options.webpack.unwrap_or(self.webpack),
361 webpack_config_path: override_options
362 .webpack_config_path
363 .or(self.webpack_config_path),
364 plugins: override_options.plugins.unwrap_or(self.plugins),
365 assets: override_options.assets.unwrap_or(self.assets),
366 delete_out_dir: override_options.delete_out_dir.or(self.delete_out_dir),
367 manual_restart: override_options
368 .manual_restart
369 .unwrap_or(self.manual_restart),
370 builder: override_options.builder.unwrap_or(self.builder),
371 }
372 }
373}
374
375pub trait ConfigurationLoader {
376 fn load(&self, name: Option<&str>) -> Result<Configuration>;
377}
378
379#[derive(Clone, Debug)]
380pub struct NestConfigurationLoader {
381 reader: FileSystemReader,
382 cache: RefCell<BTreeMap<Option<String>, Configuration>>,
383}
384
385impl NestConfigurationLoader {
386 pub fn new(reader: FileSystemReader) -> Self {
387 Self {
388 reader,
389 cache: RefCell::new(BTreeMap::new()),
390 }
391 }
392}
393
394impl ConfigurationLoader for NestConfigurationLoader {
395 fn load(&self, name: Option<&str>) -> Result<Configuration> {
396 let cache_key = name.map(ToString::to_string);
397 if let Some(config) = self.cache.borrow().get(&cache_key) {
398 return Ok(config.clone());
399 }
400 let content = match name {
401 Some(name) => Some(self.reader.read(name)?),
402 None => self
403 .reader
404 .read_any_of(&["nestrs-cli.json", ".nestrs-cli.json"])?,
405 };
406
407 let config = match content {
408 Some(content) => parse_configuration_in_dir(&content, self.reader.directory()),
409 None => Ok(Configuration::default_in(self.reader.directory())),
410 }?;
411 self.cache.borrow_mut().insert(cache_key, config.clone());
412 Ok(config)
413 }
414}
415
416pub fn load_configuration(directory: impl AsRef<Path>) -> Result<Configuration> {
417 NestConfigurationLoader::new(FileSystemReader::new(directory.as_ref())).load(None)
418}
419
420pub fn load_configuration_file(path: impl AsRef<Path>) -> Result<Configuration> {
421 let path = path.as_ref();
422 let directory = path.parent().unwrap_or_else(|| Path::new("."));
423 let name = path
424 .file_name()
425 .and_then(|name| name.to_str())
426 .ok_or_else(|| {
427 CliError::InvalidConfiguration(format!(
428 "configuration path `{}` has no valid file name",
429 path.display()
430 ))
431 })?;
432
433 NestConfigurationLoader::new(FileSystemReader::new(directory)).load(Some(name))
434}
435
436pub fn parse_configuration(content: &str) -> Result<Configuration> {
437 parse_configuration_in_dir(content, Path::new("."))
438}
439
440pub fn parse_configuration_in_dir(
441 content: &str,
442 directory: impl AsRef<Path>,
443) -> Result<Configuration> {
444 serde_json::from_str::<ConfigurationOverride>(content)
445 .map(|config| Configuration::default_in(directory).merge(config))
446 .map_err(|error| CliError::InvalidConfiguration(error.to_string()))
447}
448
449fn deserialize_plugin_options<'de, D>(
450 deserializer: D,
451) -> std::result::Result<Vec<BTreeMap<String, Value>>, D::Error>
452where
453 D: Deserializer<'de>,
454{
455 #[derive(Deserialize)]
456 #[serde(untagged)]
457 enum Input {
458 Array(Vec<BTreeMap<String, Value>>),
459 Object(BTreeMap<String, Value>),
460 }
461
462 match Option::<Input>::deserialize(deserializer)? {
463 Some(Input::Array(values)) => Ok(values),
464 Some(Input::Object(value)) => Ok(vec![value]),
465 None => Ok(Vec::new()),
466 }
467}
468
469fn merge_extra(
470 mut base: BTreeMap<String, Value>,
471 override_extra: BTreeMap<String, Value>,
472) -> BTreeMap<String, Value> {
473 base.extend(override_extra);
474 base
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480
481 #[test]
482 fn default_configuration_matches_nestrs_defaults() {
483 let configuration = Configuration::default();
484
485 assert_eq!(configuration.language, "rs");
486 assert_eq!(configuration.collection, "@nestrs/schematics");
487 assert_eq!(configuration.source_root, "src");
488 assert_eq!(configuration.entry_file, "main");
489 assert_eq!(configuration.exec, "cargo");
490 assert!(!configuration.monorepo);
491 assert!(!configuration.compiler_options.webpack);
492 assert!(!configuration.compiler_options.manual_restart);
493 assert_eq!(configuration.compiler_options.builder, Builder::Cargo);
494 }
495
496 #[test]
497 fn merge_preserves_nested_compiler_defaults() {
498 let configuration = Configuration::default().merge(ConfigurationOverride {
499 entry_file: Some("secondary".to_string()),
500 compiler_options: Some(CompilerOptionsOverride {
501 webpack: Some(true),
502 ..CompilerOptionsOverride::default()
503 }),
504 ..ConfigurationOverride::default()
505 });
506
507 assert_eq!(configuration.entry_file, "secondary");
508 assert!(configuration.compiler_options.webpack);
509 assert_eq!(configuration.compiler_options.assets, Vec::new());
510 assert_eq!(configuration.compiler_options.plugins, Vec::new());
511 }
512}