瀏覽代碼

cli: Add program template with multiple files (#2602)

acheron 2 年之前
父節點
當前提交
6eacad4b11
共有 4 個文件被更改,包括 263 次插入105 次删除
  1. 2 1
      CHANGELOG.md
  2. 69 33
      cli/src/lib.rs
  3. 181 70
      cli/src/rust_template.rs
  4. 11 1
      cli/src/solidity_template.rs

+ 2 - 1
CHANGELOG.md

@@ -20,7 +20,8 @@ The minor version will be incremented upon a breaking change and the patch versi
 - ts: Add ability to access workspace programs independent of the casing used, e.g. `anchor.workspace.myProgram`, `anchor.workspace.MyProgram`... ([#2579](https://github.com/coral-xyz/anchor/pull/2579)).
 - spl: Export `mpl-token-metadata` crate ([#2583](https://github.com/coral-xyz/anchor/pull/2583)).
 - spl: Add `TokenRecordAccount` for pNFTs ([#2597](https://github.com/coral-xyz/anchor/pull/2597)).
-- ts: Add support for unnamed(tuple) enum in accounts([#2601](https://github.com/coral-xyz/anchor/pull/2601)).
+- ts: Add support for unnamed(tuple) enum in accounts ([#2601](https://github.com/coral-xyz/anchor/pull/2601)).
+- cli: Add program template with multiple files for instructions, state... ([#2602](https://github.com/coral-xyz/anchor/pull/2602)).
 
 ### Fixes
 

+ 69 - 33
cli/src/lib.rs

@@ -20,6 +20,7 @@ use heck::{ToKebabCase, ToSnakeCase};
 use regex::{Regex, RegexBuilder};
 use reqwest::blocking::multipart::{Form, Part};
 use reqwest::blocking::Client;
+use rust_template::ProgramTemplate;
 use semver::{Version, VersionReq};
 use serde::{Deserialize, Serialize};
 use serde_json::{json, Map, Value as JsonValue};
@@ -69,15 +70,23 @@ pub struct Opts {
 pub enum Command {
     /// Initializes a workspace.
     Init {
+        /// Workspace name
         name: String,
+        /// Use JavaScript instead of TypeScript
         #[clap(short, long)]
         javascript: bool,
+        /// Use Solidity instead of Rust
         #[clap(short, long)]
         solidity: bool,
+        /// Don't initialize git
         #[clap(long)]
         no_git: bool,
+        /// Use `jest` instead of `mocha` for tests
         #[clap(long)]
         jest: bool,
+        /// Rust program template to use
+        #[clap(value_enum, short, long, default_value = "single")]
+        template: ProgramTemplate,
     },
     /// Builds the workspace.
     #[clap(name = "build", alias = "b")]
@@ -207,9 +216,14 @@ pub enum Command {
     },
     /// Creates a new program.
     New {
+        /// Program name
+        name: String,
+        /// Use Solidity instead of Rust
         #[clap(short, long)]
         solidity: bool,
-        name: String,
+        /// Rust program template to use
+        #[clap(value_enum, short, long, default_value = "single")]
+        template: ProgramTemplate,
     },
     /// Commands for interacting with interface definitions.
     Idl {
@@ -456,8 +470,21 @@ pub fn entry(opts: Opts) -> Result<()> {
             solidity,
             no_git,
             jest,
-        } => init(&opts.cfg_override, name, javascript, solidity, no_git, jest),
-        Command::New { solidity, name } => new(&opts.cfg_override, solidity, name),
+            template,
+        } => init(
+            &opts.cfg_override,
+            name,
+            javascript,
+            solidity,
+            no_git,
+            jest,
+            template,
+        ),
+        Command::New {
+            solidity,
+            name,
+            template,
+        } => new(&opts.cfg_override, solidity, name, template),
         Command::Build {
             idl,
             idl_ts,
@@ -604,6 +631,7 @@ fn init(
     solidity: bool,
     no_git: bool,
     jest: bool,
+    template: ProgramTemplate,
 ) -> Result<()> {
     if Config::discover(cfg_override)?.is_some() {
         return Err(anyhow!("Workspace already initialized"));
@@ -682,17 +710,11 @@ fn init(
 
     // Build the program.
     if solidity {
-        fs::create_dir("solidity")?;
-
-        new_solidity_program(&project_name)?;
+        solidity_template::create_program(&project_name)?;
     } else {
-        // Build virtual manifest for rust programs
-        fs::write("Cargo.toml", rust_template::virtual_manifest())?;
-
-        fs::create_dir("programs")?;
-
-        new_rust_program(&project_name)?;
+        rust_template::create_program(&project_name, template)?;
     }
+
     // Build the test suite.
     fs::create_dir("tests")?;
     // Build the migrations directory.
@@ -783,7 +805,12 @@ fn install_node_modules(cmd: &str) -> Result<std::process::Output> {
 }
 
 // Creates a new program crate in the `programs/<name>` directory.
-fn new(cfg_override: &ConfigOverride, solidity: bool, name: String) -> Result<()> {
+fn new(
+    cfg_override: &ConfigOverride,
+    solidity: bool,
+    name: String,
+    template: ProgramTemplate,
+) -> Result<()> {
     with_workspace(cfg_override, |cfg| {
         match cfg.path().parent() {
             None => {
@@ -802,10 +829,10 @@ fn new(cfg_override: &ConfigOverride, solidity: bool, name: String) -> Result<()
                     name.clone(),
                     ProgramDeployment {
                         address: if solidity {
-                            new_solidity_program(&name)?;
+                            solidity_template::create_program(&name)?;
                             solidity_template::default_program_id()
                         } else {
-                            new_rust_program(&name)?;
+                            rust_template::create_program(&name, template)?;
                             rust_template::get_or_create_program_id(&name)
                         },
                         path: None,
@@ -823,26 +850,32 @@ fn new(cfg_override: &ConfigOverride, solidity: bool, name: String) -> Result<()
     })
 }
 
-// Creates a new rust program crate in the current directory with `name`.
-fn new_rust_program(name: &str) -> Result<()> {
-    if !PathBuf::from("Cargo.toml").exists() {
-        fs::write("Cargo.toml", rust_template::virtual_manifest())?;
+/// Array of (path, content) tuple.
+pub type Files = Vec<(PathBuf, String)>;
+
+/// Create files from the given (path, content) tuple array.
+///
+/// # Example
+///
+/// ```ignore
+/// crate_files(vec![("programs/my_program/src/lib.rs".into(), "// Content".into())])?;
+/// ```
+pub fn create_files(files: &Files) -> Result<()> {
+    for (path, content) in files {
+        let path = Path::new(path);
+        if path.exists() {
+            continue;
+        }
+
+        match path.extension() {
+            Some(_) => {
+                fs::create_dir_all(path.parent().unwrap())?;
+                fs::write(path, content)?;
+            }
+            None => fs::create_dir_all(path)?,
+        }
     }
-    fs::create_dir_all(format!("programs/{name}/src/"))?;
-    let mut cargo_toml = File::create(format!("programs/{name}/Cargo.toml"))?;
-    cargo_toml.write_all(rust_template::cargo_toml(name).as_bytes())?;
-    let mut xargo_toml = File::create(format!("programs/{name}/Xargo.toml"))?;
-    xargo_toml.write_all(rust_template::xargo_toml().as_bytes())?;
-    let mut lib_rs = File::create(format!("programs/{name}/src/lib.rs"))?;
-    lib_rs.write_all(rust_template::lib_rs(name).as_bytes())?;
-    Ok(())
-}
 
-// Creates a new solidity program in the current directory with `name`.
-fn new_solidity_program(name: &str) -> Result<()> {
-    fs::create_dir_all("solidity")?;
-    let mut lib_rs = File::create(format!("solidity/{name}.sol"))?;
-    lib_rs.write_all(solidity_template::solidity(name).as_bytes())?;
     Ok(())
 }
 
@@ -4224,6 +4257,7 @@ mod tests {
             false,
             false,
             false,
+            ProgramTemplate::default(),
         )
         .unwrap();
     }
@@ -4241,6 +4275,7 @@ mod tests {
             false,
             false,
             false,
+            ProgramTemplate::default(),
         )
         .unwrap();
     }
@@ -4258,6 +4293,7 @@ mod tests {
             false,
             false,
             false,
+            ProgramTemplate::default(),
         )
         .unwrap();
     }

+ 181 - 70
cli/src/rust_template.rs

@@ -1,7 +1,8 @@
-use crate::config::ProgramWorkspace;
 use crate::VERSION;
+use crate::{config::ProgramWorkspace, create_files, Files};
 use anchor_syn::idl::types::Idl;
 use anyhow::Result;
+use clap::{Parser, ValueEnum};
 use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase};
 use solana_sdk::{
     pubkey::Pubkey,
@@ -10,22 +11,140 @@ use solana_sdk::{
 };
 use std::{fmt::Write, path::Path};
 
-/// Read the program keypair file or create a new one if it doesn't exist.
-pub fn get_or_create_program_id(name: &str) -> Pubkey {
-    let keypair_path = Path::new("target")
-        .join("deploy")
-        .join(format!("{}-keypair.json", name.to_snake_case()));
+/// Program initialization template
+#[derive(Clone, Debug, Default, Eq, PartialEq, Parser, ValueEnum)]
+pub enum ProgramTemplate {
+    /// Program with a single `lib.rs` file
+    #[default]
+    Single,
+    /// Program with multiple files for instructions, state...
+    Multiple,
+}
 
-    read_keypair_file(&keypair_path)
-        .unwrap_or_else(|_| {
-            let keypair = Keypair::new();
-            write_keypair_file(&keypair, keypair_path).expect("Unable to create program keypair");
-            keypair
-        })
-        .pubkey()
+/// Create a program from the given name and template.
+pub fn create_program(name: &str, template: ProgramTemplate) -> Result<()> {
+    let program_path = Path::new("programs").join(name);
+    let common_files = vec![
+        ("Cargo.toml".into(), workspace_manifest().into()),
+        (program_path.join("Cargo.toml"), cargo_toml(name)),
+        (program_path.join("Xargo.toml"), xargo_toml().into()),
+    ];
+
+    let template_files = match template {
+        ProgramTemplate::Single => create_program_template_single(name, &program_path),
+        ProgramTemplate::Multiple => create_program_template_multiple(name, &program_path),
+    };
+
+    create_files(&[common_files, template_files].concat())
+}
+
+/// Create a program with a single `lib.rs` file.
+fn create_program_template_single(name: &str, program_path: &Path) -> Files {
+    vec![(
+        program_path.join("src").join("lib.rs"),
+        format!(
+            r#"use anchor_lang::prelude::*;
+
+declare_id!("{}");
+
+#[program]
+pub mod {} {{
+    use super::*;
+
+    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {{
+        Ok(())
+    }}
+}}
+
+#[derive(Accounts)]
+pub struct Initialize {{}}
+"#,
+            get_or_create_program_id(name),
+            name.to_snake_case(),
+        ),
+    )]
+}
+
+/// Create a program with multiple files for instructions, state...
+fn create_program_template_multiple(name: &str, program_path: &Path) -> Files {
+    let src_path = program_path.join("src");
+    vec![
+        (
+            src_path.join("lib.rs"),
+            format!(
+                r#"pub mod constants;
+pub mod error;
+pub mod instructions;
+pub mod state;
+
+use anchor_lang::prelude::*;
+
+pub use constants::*;
+pub use instructions::*;
+pub use state::*;
+
+declare_id!("{}");
+
+#[program]
+pub mod {} {{
+    use super::*;
+
+    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {{
+        initialize::handler(ctx)
+    }}
+}}
+"#,
+                get_or_create_program_id(name),
+                name.to_snake_case(),
+            ),
+        ),
+        (
+            src_path.join("constants.rs"),
+            r#"use anchor_lang::prelude::*;
+
+#[constant]
+pub const SEED: &str = "anchor";
+"#
+            .into(),
+        ),
+        (
+            src_path.join("error.rs"),
+            r#"use anchor_lang::prelude::*;
+
+#[error_code]
+pub enum ErrorCode {
+    #[msg("Custom error message")]
+    CustomError,
+}
+"#
+            .into(),
+        ),
+        (
+            src_path.join("instructions").join("mod.rs"),
+            r#"pub mod initialize;
+
+pub use initialize::*;
+"#
+            .into(),
+        ),
+        (
+            src_path.join("instructions").join("initialize.rs"),
+            r#"use anchor_lang::prelude::*;
+
+#[derive(Accounts)]
+pub struct Initialize {}
+
+pub fn handler(ctx: Context<Initialize>) -> Result<()> {
+    Ok(())
+}
+"#
+            .into(),
+        ),
+        (src_path.join("state").join("mod.rs"), r#""#.into()),
+    ]
 }
 
-pub fn virtual_manifest() -> &'static str {
+const fn workspace_manifest() -> &'static str {
     r#"[workspace]
 members = [
     "programs/*"
@@ -42,33 +161,7 @@ codegen-units = 1
 "#
 }
 
-pub fn credentials(token: &str) -> String {
-    format!(
-        r#"[registry]
-token = "{token}"
-"#
-    )
-}
-
-pub fn idl_ts(idl: &Idl) -> Result<String> {
-    let mut idl = idl.clone();
-    for acc in idl.accounts.iter_mut() {
-        acc.name = acc.name.to_lower_camel_case();
-    }
-    let idl_json = serde_json::to_string_pretty(&idl)?;
-    Ok(format!(
-        r#"export type {} = {};
-
-export const IDL: {} = {};
-"#,
-        idl.name.to_upper_camel_case(),
-        idl_json,
-        idl.name.to_upper_camel_case(),
-        idl_json
-    ))
-}
-
-pub fn cargo_toml(name: &str) -> String {
+fn cargo_toml(name: &str) -> String {
     format!(
         r#"[package]
 name = "{0}"
@@ -96,6 +189,53 @@ anchor-lang = "{2}"
     )
 }
 
+fn xargo_toml() -> &'static str {
+    r#"[target.bpfel-unknown-unknown.dependencies.std]
+features = []
+"#
+}
+
+/// Read the program keypair file or create a new one if it doesn't exist.
+pub fn get_or_create_program_id(name: &str) -> Pubkey {
+    let keypair_path = Path::new("target")
+        .join("deploy")
+        .join(format!("{}-keypair.json", name.to_snake_case()));
+
+    read_keypair_file(&keypair_path)
+        .unwrap_or_else(|_| {
+            let keypair = Keypair::new();
+            write_keypair_file(&keypair, keypair_path).expect("Unable to create program keypair");
+            keypair
+        })
+        .pubkey()
+}
+
+pub fn credentials(token: &str) -> String {
+    format!(
+        r#"[registry]
+token = "{token}"
+"#
+    )
+}
+
+pub fn idl_ts(idl: &Idl) -> Result<String> {
+    let mut idl = idl.clone();
+    for acc in idl.accounts.iter_mut() {
+        acc.name = acc.name.to_lower_camel_case();
+    }
+    let idl_json = serde_json::to_string_pretty(&idl)?;
+    Ok(format!(
+        r#"export type {} = {};
+
+export const IDL: {} = {};
+"#,
+        idl.name.to_upper_camel_case(),
+        idl_json,
+        idl.name.to_upper_camel_case(),
+        idl_json
+    ))
+}
+
 pub fn deploy_js_script_host(cluster_url: &str, script_path: &str) -> String {
     format!(
         r#"
@@ -181,35 +321,6 @@ module.exports = async function (provider) {
 "#
 }
 
-pub fn xargo_toml() -> &'static str {
-    r#"[target.bpfel-unknown-unknown.dependencies.std]
-features = []
-"#
-}
-
-pub fn lib_rs(name: &str) -> String {
-    format!(
-        r#"use anchor_lang::prelude::*;
-
-declare_id!("{}");
-
-#[program]
-pub mod {} {{
-    use super::*;
-
-    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {{
-        Ok(())
-    }}
-}}
-
-#[derive(Accounts)]
-pub struct Initialize {{}}
-"#,
-        get_or_create_program_id(name),
-        name.to_snake_case(),
-    )
-}
-
 pub fn mocha(name: &str) -> String {
     format!(
         r#"const anchor = require("@coral-xyz/anchor");

+ 11 - 1
cli/src/solidity_template.rs

@@ -1,10 +1,20 @@
-use crate::config::ProgramWorkspace;
 use crate::VERSION;
+use crate::{config::ProgramWorkspace, create_files};
 use anchor_syn::idl::types::Idl;
 use anyhow::Result;
 use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase};
 use solana_sdk::pubkey::Pubkey;
 use std::fmt::Write;
+use std::path::Path;
+
+/// Create a solidity program.
+pub fn create_program(name: &str) -> Result<()> {
+    let files = vec![(
+        Path::new("solidity").join(name).with_extension("sol"),
+        solidity(name),
+    )];
+    create_files(&files)
+}
 
 pub fn default_program_id() -> Pubkey {
     "F1ipperKF9EfD821ZbbYjS319LXYiBmjhzkkf5a26rC"