diff --git a/src/commands/add.rs b/src/commands/add.rs index 684948f..93640f4 100644 --- a/src/commands/add.rs +++ b/src/commands/add.rs @@ -7,6 +7,7 @@ use crate::utils::{ use colored::*; use helpers::Result; use serde_json::json; +use std::collections::HashMap; use std::fs; pub fn git(project_type: Option) -> Result<()> { @@ -70,7 +71,15 @@ pub fn jest() -> Result<()> { spinner.set_message("Installing dependencies"); - node::install_dev("jest jest-watch-typeahead"); + node::install_dev("jest jest-watch-typeahead is-ci-cli"); + + let mut scripts = HashMap::new(); + + scripts.insert("test", "is-ci-cli test:ci test:watch"); + scripts.insert("test:ci", "jest"); + scripts.insert("test:watch", "jest --watch"); + + node::add_scripts(scripts)?; template::render_file( include_str!("../templates/jest.config.js"), @@ -80,6 +89,18 @@ pub fn jest() -> Result<()> { spinner.success("Jest setup complete"); + println!( + " +New commands added +* {test} - Run tests in either CI mode or watch mode in dev +* {test_ci} - CI mode runs only if CI environment variable is set, uses is-ci-cli +* {test_watch} - Run tests in watch mode + ", + test = "test".blue(), + test_ci = "test:ci".blue(), + test_watch = "test:watch".blue() + ); + Ok(()) } diff --git a/src/commands/remove.rs b/src/commands/remove.rs index 1a6d3b7..ce78a55 100644 --- a/src/commands/remove.rs +++ b/src/commands/remove.rs @@ -30,7 +30,8 @@ pub fn prettier() -> Result<()> { pub fn jest() -> Result<()> { fs::remove_file("jest.config.js")?; - node::uninstall("jest jest-watch-typeahead"); + node::uninstall("jest jest-watch-typeahead is-ci-cli"); + node::remove_scripts(vec!["test", "test:ci", "test:watch"])?; Ok(()) } diff --git a/src/utils.rs b/src/utils.rs index 98bb852..aabf204 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,5 +2,6 @@ pub mod format; pub mod helpers; pub mod node; pub mod progressbar; +pub mod pkg_json; pub mod project; pub mod template; diff --git a/src/utils/node.rs b/src/utils/node.rs index 96686db..428b1d4 100644 --- a/src/utils/node.rs +++ b/src/utils/node.rs @@ -1,7 +1,11 @@ use super::helpers; use crate::config::{self, NodeInstaller}; +use crate::utils::pkg_json; +use helpers::Result; use lazy_static::lazy_static; use regex::Regex; +use std::collections::HashMap; +use std::fs; lazy_static! { static ref PKG: Regex = Regex::new(r"([\w@/-]+)\{([\w\-,]+)\}").unwrap(); @@ -48,6 +52,34 @@ pub fn uninstall(pkg: &str) { packages(pkg).iter().for_each(|p| uninstaller(p)); } +pub fn add_scripts(scripts: HashMap<&str, &str>) -> Result<()> { + let mut pkg = pkg_json::Package::new()?; + + scripts.iter().for_each(|(name, cmd)| { + pkg.scripts.insert(name.to_string(), cmd.to_string()); + }); + + let json = serde_json::to_string_pretty(&pkg)?; + + fs::write("package.json", json)?; + + Ok(()) +} + +pub fn remove_scripts(scripts: Vec<&str>) -> Result<()> { + let mut pkg = pkg_json::Package::new()?; + + scripts.iter().for_each(|name| { + pkg.scripts.remove(&name.to_string()); + }); + + let json = serde_json::to_string_pretty(&pkg)?; + + fs::write("package.json", json)?; + + Ok(()) +} + fn split_packages(caps: regex::Captures) -> Option> { let base = caps.get(1)?.as_str(); diff --git a/src/utils/pkg_json.rs b/src/utils/pkg_json.rs new file mode 100644 index 0000000..cf2e5d2 --- /dev/null +++ b/src/utils/pkg_json.rs @@ -0,0 +1,201 @@ +// Simplified version of npm-package-json +// https://github.com/SirWindfield/npm-package-json + +use crate::utils::helpers; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::io::ErrorKind; +use std::{collections::BTreeMap, fs}; + +/// An ordered map for `bin` entries. +pub type BinSet = BTreeMap; +/// An ordered map for `dependencies` entries. +pub type DepsSet = BTreeMap; +/// An ordered map for `engines` entries. +pub type EnginesSet = BTreeMap; +/// An ordered map for `scripts` entries. +pub type ScriptsSet = BTreeMap; + +/// The result type of this crate. +pub type Result = std::result::Result; + +/// A bug contacting form. +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct Bug { + /// The email to use for contact. + #[serde(skip_serializing_if = "Option::is_none")] + pub email: Option, + /// The url to use to submit bugs. + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, +} + +/// A person. +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct Person { + /// The name of a person. + pub name: String, + /// The email of a person. + pub email: Option, + /// The homepage of the person. + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, +} + +/// A reference to a person. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(untagged)] +pub enum PersonReference { + /// A short reference. + /// + /// Short references have a fixed format of `John Doe (https://john.doe.dev)`. + Short(String), + /// A full reference. + /// + /// This type of reference defines the parts using a struct instead of a + /// shorthand string format. + Full(Person), +} + +/// A reference to a man page. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(untagged)] +pub enum ManReference { + /// A single man page reference. Points to one single file. + Single(String), + /// Multiple man pages, can contain anything from zero to n. + Multiple(Vec), +} + +/// A repository. +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct Repository { + /// The version control system that the repository uses. + pub r#type: String, + /// The url to the repository. + pub url: String, + /// The directory that the repository is in. Often used for monorepos. + #[serde(skip_serializing_if = "Option::is_none")] + pub directory: Option, +} + +/// A repository reference. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(untagged)] +pub enum RepositoryReference { + /// A short reference to the repository. Has to have the syntax that `npm install` allows as well. For more information see [here](https://docs.npmjs.com/files/package.json#repository). + Short(String), + /// A full reference. + Full(Repository), +} + +/// The top-level `package.json` structure. +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Package { + /// The package name. + pub name: String, + /// The package version. + pub version: String, + /// The optional package description. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// The optional package main entry file. + #[serde(skip_serializing_if = "Option::is_none")] + pub main: Option, + /// The optional directories + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub directories: BTreeMap, + /// The optional list of script entries. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub scripts: ScriptsSet, + /// The optional repository reference. + #[serde(skip_serializing_if = "Option::is_none")] + pub repository: Option, + /// The optional list of keywords. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub keywords: Vec, + /// The optional author. + #[serde(skip_serializing_if = "Option::is_none")] + pub author: Option, + /// The optional package license. + #[serde(skip_serializing_if = "Option::is_none")] + pub license: Option, + /// The optional bug contact form. + #[serde(skip_serializing_if = "Option::is_none")] + pub bugs: Option, + /// The optional package homepage. + #[serde(skip_serializing_if = "Option::is_none")] + pub homepage: Option, + /// The optional list of contributors. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub contributors: Vec, + /// The optional list of files to include. Each entry defines a regex + /// pattern. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub files: Vec, + /// The optional package browser entry file. + /// + /// This is usually defined in libraries that are meant to be consumed by + /// browsers. These Thoes can refer to objects that are not available inside + /// a `nodejs` environment (like `window`). + #[serde(skip_serializing_if = "Option::is_none")] + pub browser: Option, + /// The optional set of binary definitions. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub bin: BinSet, + /// The optional list of man page references. + #[serde(skip_serializing_if = "Option::is_none")] + pub man: Option, + /// The optional list of dependencies. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub dependencies: DepsSet, + /// The optional list of development dependencies. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub dev_dependencies: DepsSet, + /// The optional list of peer dependencies. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub peer_dependencies: DepsSet, + /// The optional list of bundled dependencies. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub bundled_dependencies: DepsSet, + /// The optional list of optional dependencies. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub optional_dependencies: DepsSet, + /// The optional list of engine entries. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub engines: EnginesSet, + /// The package privacy. + #[serde(skip_serializing_if = "Option::is_none")] + pub private: Option, + /// The OS' that the package can run on. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub os: Vec, + /// The CPU architectures that the package can run on. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub cpu: Vec, + /// The optional config object. + #[serde(skip_serializing_if = "Option::is_none")] + pub config: Option, + /// Other custom fields that have been defined inside the `package.json` + /// file. + #[serde(flatten)] + pub others: BTreeMap, +} + +impl Package { + pub fn new() -> Result { + match fs::metadata("package.json") { + Ok(_) => (), + Err(err) => { + // Initialize an empty package.json if none exists + if let ErrorKind::NotFound = err.kind() { + helpers::run_command("npm", &["init", "-y"]); + } + } + }; + + let content = fs::read_to_string("package.json")?; + Ok(serde_json::from_str(&content)?) + } +}