Skip to main content

cli/lib/configuration/
mod.rs

1//! Nest CLI configuration loading and defaults.
2
3use 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}