use crate::error::{CliError, Result};
use crate::readers::{FileSystemReader, Reader};
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::path::Path;
pub const DEFAULT_LANGUAGE: &str = "rs";
pub const DEFAULT_SOURCE_ROOT: &str = "src";
pub const DEFAULT_COLLECTION: &str = "@nestrs/schematics";
pub const DEFAULT_ENTRY_FILE: &str = "main";
pub const DEFAULT_EXEC: &str = "cargo";
pub const DEFAULT_TSCONFIG_FILENAME: &str = "tsconfig.json";
pub const DEFAULT_WEBPACK_CONFIG_FILENAME: &str = "webpack.config.js";
pub const DEFAULT_OUT_DIR: &str = "dist";
pub mod configuration;
pub mod configuration_loader;
pub mod defaults;
pub mod nest_configuration_loader;
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum Asset {
Glob(String),
Entry(AssetEntry),
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ActionOnFile {
pub action: String,
pub item: AssetEntry,
pub path: String,
pub source_root: String,
pub watch_assets_mode: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AssetEntry {
pub glob: String,
pub include: Option<String>,
pub flat: Option<bool>,
pub exclude: Option<String>,
pub out_dir: Option<String>,
pub watch_assets: Option<bool>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(default)]
#[serde(rename_all = "camelCase")]
pub struct SwcBuilderOptions {
pub swcrc_path: Option<String>,
pub out_dir: Option<String>,
pub filenames: Vec<String>,
pub sync: Option<bool>,
pub extensions: Vec<String>,
pub copy_files: Option<bool>,
pub include_dotfiles: Option<bool>,
pub quiet: Option<bool>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WebpackBuilderOptions {
pub config_path: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TscBuilderOptions {
pub config_path: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Builder {
Cargo,
Tsc(TscBuilderOptions),
Swc(SwcBuilderOptions),
Webpack(WebpackBuilderOptions),
}
impl Default for Builder {
fn default() -> Self {
Self::Cargo
}
}
impl<'de> Deserialize<'de> for Builder {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(rename_all = "lowercase")]
enum BuilderVariant {
Cargo,
Tsc,
Swc,
Webpack,
}
#[derive(Deserialize)]
struct BuilderObject {
#[serde(rename = "type")]
builder_type: BuilderVariant,
#[serde(default = "empty_builder_options")]
options: Value,
}
fn empty_builder_options() -> Value {
Value::Object(Default::default())
}
#[derive(Deserialize)]
#[serde(untagged)]
enum BuilderInput {
Variant(BuilderVariant),
Object(BuilderObject),
}
let input = BuilderInput::deserialize(deserializer)?;
match input {
BuilderInput::Variant(BuilderVariant::Cargo) => Ok(Self::Cargo),
BuilderInput::Variant(BuilderVariant::Tsc) => {
Ok(Self::Tsc(TscBuilderOptions::default()))
}
BuilderInput::Variant(BuilderVariant::Swc) => {
Ok(Self::Swc(SwcBuilderOptions::default()))
}
BuilderInput::Variant(BuilderVariant::Webpack) => {
Ok(Self::Webpack(WebpackBuilderOptions::default()))
}
BuilderInput::Object(object) => match object.builder_type {
BuilderVariant::Cargo => Ok(Self::Cargo),
BuilderVariant::Tsc => serde_json::from_value(object.options)
.map(Self::Tsc)
.map_err(serde::de::Error::custom),
BuilderVariant::Swc => serde_json::from_value(object.options)
.map(Self::Swc)
.map_err(serde::de::Error::custom),
BuilderVariant::Webpack => serde_json::from_value(object.options)
.map(Self::Webpack)
.map_err(serde::de::Error::custom),
},
}
}
}
impl Serialize for Builder {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
#[derive(Serialize)]
struct BuilderObject<'a, T> {
#[serde(rename = "type")]
builder_type: &'a str,
options: &'a T,
}
match self {
Self::Cargo => serializer.serialize_str("cargo"),
Self::Tsc(options) => BuilderObject {
builder_type: "tsc",
options,
}
.serialize(serializer),
Self::Swc(options) => BuilderObject {
builder_type: "swc",
options,
}
.serialize(serializer),
Self::Webpack(options) => BuilderObject {
builder_type: "webpack",
options,
}
.serialize(serializer),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(default)]
#[serde(rename_all = "camelCase")]
pub struct CompilerOptions {
pub ts_config_path: Option<String>,
pub webpack: bool,
pub webpack_config_path: Option<String>,
pub plugins: Vec<Plugin>,
pub assets: Vec<Asset>,
pub delete_out_dir: Option<bool>,
pub manual_restart: bool,
pub builder: Builder,
}
impl Default for CompilerOptions {
fn default() -> Self {
Self {
ts_config_path: None,
webpack: false,
webpack_config_path: None,
plugins: Vec::new(),
assets: Vec::new(),
delete_out_dir: None,
manual_restart: false,
builder: Builder::default(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum Plugin {
Name(String),
Options(PluginOptions),
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
pub struct PluginOptions {
pub name: String,
#[serde(default)]
#[serde(deserialize_with = "deserialize_plugin_options")]
pub options: Vec<BTreeMap<String, Value>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum GenerateSpec {
Bool(bool),
BySchematic(BTreeMap<String, bool>),
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct GenerateOptions {
pub spec: Option<GenerateSpec>,
pub flat: Option<bool>,
pub spec_file_suffix: Option<String>,
pub base_dir: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ProjectConfiguration {
#[serde(rename = "type")]
pub project_type: Option<String>,
pub root: Option<String>,
pub entry_file: Option<String>,
pub exec: Option<String>,
pub source_root: Option<String>,
pub compiler_options: Option<CompilerOptions>,
pub generate_options: Option<GenerateOptions>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Configuration {
pub language: String,
pub collection: String,
pub source_root: String,
pub entry_file: String,
pub exec: String,
pub monorepo: bool,
pub compiler_options: CompilerOptions,
pub generate_options: GenerateOptions,
pub projects: BTreeMap<String, ProjectConfiguration>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl Default for Configuration {
fn default() -> Self {
Self {
language: DEFAULT_LANGUAGE.to_string(),
collection: DEFAULT_COLLECTION.to_string(),
source_root: DEFAULT_SOURCE_ROOT.to_string(),
entry_file: DEFAULT_ENTRY_FILE.to_string(),
exec: DEFAULT_EXEC.to_string(),
monorepo: false,
compiler_options: CompilerOptions::default(),
generate_options: GenerateOptions::default(),
projects: BTreeMap::new(),
extra: BTreeMap::new(),
}
}
}
impl Configuration {
pub fn default_in(directory: impl AsRef<Path>) -> Self {
let _ = directory;
Self::default()
}
pub fn merge(self, override_config: ConfigurationOverride) -> Self {
Self {
language: override_config.language.unwrap_or(self.language),
collection: override_config.collection.unwrap_or(self.collection),
source_root: override_config.source_root.unwrap_or(self.source_root),
entry_file: override_config.entry_file.unwrap_or(self.entry_file),
exec: override_config.exec.unwrap_or(self.exec),
monorepo: override_config.monorepo.unwrap_or(self.monorepo),
compiler_options: match override_config.compiler_options {
Some(compiler_options) => self.compiler_options.merge(compiler_options),
None => self.compiler_options,
},
generate_options: override_config
.generate_options
.unwrap_or(self.generate_options),
projects: if override_config.projects.is_empty() {
self.projects
} else {
override_config.projects
},
extra: merge_extra(self.extra, override_config.extra),
}
}
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)]
#[serde(default)]
#[serde(rename_all = "camelCase")]
pub struct ConfigurationOverride {
pub language: Option<String>,
pub collection: Option<String>,
pub source_root: Option<String>,
pub entry_file: Option<String>,
pub exec: Option<String>,
pub monorepo: Option<bool>,
pub compiler_options: Option<CompilerOptionsOverride>,
pub generate_options: Option<GenerateOptions>,
pub projects: BTreeMap<String, ProjectConfiguration>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CompilerOptionsOverride {
pub ts_config_path: Option<String>,
pub webpack: Option<bool>,
pub webpack_config_path: Option<String>,
pub plugins: Option<Vec<Plugin>>,
pub assets: Option<Vec<Asset>>,
pub delete_out_dir: Option<bool>,
pub manual_restart: Option<bool>,
pub builder: Option<Builder>,
}
impl CompilerOptions {
pub fn merge(self, override_options: CompilerOptionsOverride) -> Self {
Self {
ts_config_path: override_options.ts_config_path.or(self.ts_config_path),
webpack: override_options.webpack.unwrap_or(self.webpack),
webpack_config_path: override_options
.webpack_config_path
.or(self.webpack_config_path),
plugins: override_options.plugins.unwrap_or(self.plugins),
assets: override_options.assets.unwrap_or(self.assets),
delete_out_dir: override_options.delete_out_dir.or(self.delete_out_dir),
manual_restart: override_options
.manual_restart
.unwrap_or(self.manual_restart),
builder: override_options.builder.unwrap_or(self.builder),
}
}
}
pub trait ConfigurationLoader {
fn load(&self, name: Option<&str>) -> Result<Configuration>;
}
#[derive(Clone, Debug)]
pub struct NestConfigurationLoader {
reader: FileSystemReader,
cache: RefCell<BTreeMap<Option<String>, Configuration>>,
}
impl NestConfigurationLoader {
pub fn new(reader: FileSystemReader) -> Self {
Self {
reader,
cache: RefCell::new(BTreeMap::new()),
}
}
}
impl ConfigurationLoader for NestConfigurationLoader {
fn load(&self, name: Option<&str>) -> Result<Configuration> {
let cache_key = name.map(ToString::to_string);
if let Some(config) = self.cache.borrow().get(&cache_key) {
return Ok(config.clone());
}
let content = match name {
Some(name) => Some(self.reader.read(name)?),
None => self
.reader
.read_any_of(&["nestrs-cli.json", ".nestrs-cli.json"])?,
};
let config = match content {
Some(content) => parse_configuration_in_dir(&content, self.reader.directory()),
None => Ok(Configuration::default_in(self.reader.directory())),
}?;
self.cache.borrow_mut().insert(cache_key, config.clone());
Ok(config)
}
}
pub fn load_configuration(directory: impl AsRef<Path>) -> Result<Configuration> {
NestConfigurationLoader::new(FileSystemReader::new(directory.as_ref())).load(None)
}
pub fn load_configuration_file(path: impl AsRef<Path>) -> Result<Configuration> {
let path = path.as_ref();
let directory = path.parent().unwrap_or_else(|| Path::new("."));
let name = path
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| {
CliError::InvalidConfiguration(format!(
"configuration path `{}` has no valid file name",
path.display()
))
})?;
NestConfigurationLoader::new(FileSystemReader::new(directory)).load(Some(name))
}
pub fn parse_configuration(content: &str) -> Result<Configuration> {
parse_configuration_in_dir(content, Path::new("."))
}
pub fn parse_configuration_in_dir(
content: &str,
directory: impl AsRef<Path>,
) -> Result<Configuration> {
serde_json::from_str::<ConfigurationOverride>(content)
.map(|config| Configuration::default_in(directory).merge(config))
.map_err(|error| CliError::InvalidConfiguration(error.to_string()))
}
fn deserialize_plugin_options<'de, D>(
deserializer: D,
) -> std::result::Result<Vec<BTreeMap<String, Value>>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Input {
Array(Vec<BTreeMap<String, Value>>),
Object(BTreeMap<String, Value>),
}
match Option::<Input>::deserialize(deserializer)? {
Some(Input::Array(values)) => Ok(values),
Some(Input::Object(value)) => Ok(vec![value]),
None => Ok(Vec::new()),
}
}
fn merge_extra(
mut base: BTreeMap<String, Value>,
override_extra: BTreeMap<String, Value>,
) -> BTreeMap<String, Value> {
base.extend(override_extra);
base
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_configuration_matches_nestrs_defaults() {
let configuration = Configuration::default();
assert_eq!(configuration.language, "rs");
assert_eq!(configuration.collection, "@nestrs/schematics");
assert_eq!(configuration.source_root, "src");
assert_eq!(configuration.entry_file, "main");
assert_eq!(configuration.exec, "cargo");
assert!(!configuration.monorepo);
assert!(!configuration.compiler_options.webpack);
assert!(!configuration.compiler_options.manual_restart);
assert_eq!(configuration.compiler_options.builder, Builder::Cargo);
}
#[test]
fn merge_preserves_nested_compiler_defaults() {
let configuration = Configuration::default().merge(ConfigurationOverride {
entry_file: Some("secondary".to_string()),
compiler_options: Some(CompilerOptionsOverride {
webpack: Some(true),
..CompilerOptionsOverride::default()
}),
..ConfigurationOverride::default()
});
assert_eq!(configuration.entry_file, "secondary");
assert!(configuration.compiler_options.webpack);
assert_eq!(configuration.compiler_options.assets, Vec::new());
assert_eq!(configuration.compiler_options.plugins, Vec::new());
}
}