use std::{
collections::BTreeMap,
env,
fs::{self, File},
io::Write,
path::{Path, PathBuf},
process::{Command, Stdio},
};
use anyhow::Context;
use serde::{Deserialize, Serialize};
use crate::{
Error,
build::*,
config::{ContractKind, InitPackageConfig},
util::pipe_std_streams,
};
pub fn create_new_package(config: InitPackageConfig) -> anyhow::Result<()> {
let InitPackageConfig { kind, path } = config;
let path = path
.or_else(|| env::current_dir().ok())
.context("Valid path for creating a new package")?;
if path.join("freenet.toml").exists() {
anyhow::bail!(
"A Freenet project already exists in directory: {}",
path.display()
);
}
match kind {
ContractKind::WebApp => create_view_package(&path),
ContractKind::Contract => create_regular_contract(&path),
}
}
fn create_view_package(dir: impl AsRef<Path>) -> anyhow::Result<()> {
let dir = dir.as_ref();
create_rust_crate(dir, ContractKind::WebApp)?;
create_web_init_files(dir)?;
let freenet_file_config = ContractBuildConfig {
contract: Contract {
c_type: Some(ContractType::WebApp),
lang: Some(SupportedContractLangs::Rust),
output_dir: None,
},
webapp: Some(WebAppContract {
lang: Some(SupportedWebLangs::Typescript),
typescript: Some(TypescriptConfig { webpack: true }),
state_sources: Sources {
source_dirs: Some(vec![PathBuf::from("dist")]),
files: None,
},
metadata: None,
dependencies: None,
}),
state: None,
};
let serialized = toml::to_string(&freenet_file_config)?.into_bytes();
let path = dir.join("freenet").with_extension("toml");
let mut file = File::create(path)?;
file.write_all(&serialized)?;
Ok(())
}
fn create_regular_contract(dir: impl AsRef<Path>) -> anyhow::Result<()> {
let dir = dir.as_ref();
create_rust_crate(dir, ContractKind::Contract)?;
let freenet_file_config = ContractBuildConfig {
contract: Contract {
c_type: Some(ContractType::Standard),
lang: Some(SupportedContractLangs::Rust),
output_dir: None,
},
webapp: None,
state: None,
};
let serialized = toml::to_string(&freenet_file_config)?.into_bytes();
let path = dir.join("freenet").with_extension("toml");
let mut file = File::create(path)?;
file.write_all(&serialized)?;
Ok(())
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
struct Manifest {
package: toml::Value,
dependencies: toml::Value,
#[serde(default)]
lib: Option<ManifestLib>,
#[serde(default)]
features: Option<BTreeMap<String, Vec<String>>>,
#[serde(default, flatten)]
any: Option<BTreeMap<String, toml::Value>>,
}
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
struct ManifestLib {
name: Option<String>,
path: Option<PathBuf>,
crate_type: Option<Vec<String>>,
required_features: Option<Vec<String>>,
}
fn create_rust_crate(dir: impl AsRef<Path>, kind: ContractKind) -> anyhow::Result<()> {
let dir = match kind {
ContractKind::WebApp => dir.as_ref().join("container"),
ContractKind::Contract => dir.as_ref().to_owned(),
};
let command = if dir.exists() { "init" } else { "new" };
let colored = [command, "--color", "always", "--lib"];
let auto_colored = [command, "--lib"];
use std::io::IsTerminal;
let cmd_args = if std::io::stdout().is_terminal() && std::io::stderr().is_terminal() {
colored
.as_slice()
.iter()
.copied()
.map(std::ffi::OsStr::new)
.chain([dir.as_os_str()])
} else {
auto_colored
.as_slice()
.iter()
.copied()
.map(std::ffi::OsStr::new)
.chain([dir.as_os_str()])
};
let child = Command::new("cargo")
.args(cmd_args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| {
eprintln!("Error while executing cargo command: {e}");
Error::CommandFailed("cargo")
})?;
pipe_std_streams(child)?;
let child = Command::new("cargo")
.args(["add", "freenet-stdlib"])
.current_dir(&dir)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| {
eprintln!("Error while executing cargo command: {e}");
Error::CommandFailed("cargo")
})?;
pipe_std_streams(child)?;
let manifest_path = dir.join("Cargo.toml");
let toml_str = fs::read_to_string(&manifest_path)?;
let mut manifest: Manifest = toml::from_str(&toml_str)?;
manifest.lib = Some(ManifestLib {
crate_type: Some(vec!["cdylib".into()]),
..Default::default()
});
manifest.features = Some(
[
("default".into(), vec!["freenet-main-contract".into()]),
("contract".into(), vec!["freenet-stdlib/contract".into()]),
("freenet-main-contract".into(), vec![]),
("trace".into(), vec!["freenet-stdlib/trace".into()]),
]
.into_iter()
.collect(),
);
fs::write(&manifest_path, toml::to_string(&manifest)?)?;
Ok(())
}
#[cfg(windows)]
const NPM: &str = "npm.cmd";
#[cfg(unix)]
const NPM: &str = "npm";
#[cfg(windows)]
const TSC: &str = "tsc.cmd";
#[cfg(unix)]
const TSC: &str = "tsc";
fn create_web_init_files(dir: impl AsRef<Path>) -> anyhow::Result<()> {
let dir = dir.as_ref();
let child = Command::new(NPM)
.args(["init", "--force"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.current_dir(dir)
.spawn()
.map_err(|e| {
eprintln!("Error while executing npm command: {e}");
Error::CommandFailed("npm")
})?;
pipe_std_streams(child)?;
let child = Command::new(TSC)
.args(["--init", "--pretty"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.current_dir(dir)
.spawn()
.map_err(|e| {
eprintln!("Error while executing npm command: {e}");
Error::CommandFailed("tsc")
})?;
pipe_std_streams(child)?;
const WEBPACK_CONFIG: &str = r#"
const path = require("path");
module.exports = {
entry: "./src/index.ts",
devtool: "inline-source-map",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
},
devServer: {
static: path.resolve(__dirname, "dist"),
port: 8080,
hot: true,
},
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
}
],
},
};"#;
let mut f = File::create(dir.join("webpack.config.js"))?;
f.write_all(WEBPACK_CONFIG.as_bytes())?;
fs::create_dir_all(dir.join("src"))?;
let idx = dir.join("src").join("index").with_extension("ts");
File::create(idx)?;
fs::create_dir_all(dir.join("dist"))?;
let idx = dir.join("dist").join("index").with_extension("html");
File::create(idx)?;
Ok(())
}