Bläddra i källkod

:sparkles: Add a working ecs example on project init (#12)

* :sparkles: Add a working ecs example on project init

* :recycle: Code refactoring

* :recycle: Refactor & Bug Fixes

* :bug: Fix --force flag handling

* :construction_worker: Add tests for CLI

* :sparkles: Add InitWorld example for js (jest and mocha)

* :construction_worker: Update CI

* :construction_worker: Add CI

* :construction_worker: Update CI

* :construction_worker: Change rust toolchain

* :construction_worker: Update CI

* :construction_worker: Fix CI

* :construction_worker: Update CI

* :construction_worker: Update CI

* :construction_worker: Update CI

* :construction_worker: Update CI

* :construction_worker: Add build step
Gabriele Picco 1 år sedan
förälder
incheckning
4be07288d3
6 ändrade filer med 709 tillägg och 76 borttagningar
  1. 28 12
      .github/workflows/run-tests.yml
  2. 42 38
      Cargo.lock
  3. 3 1
      cli/Cargo.toml
  4. 311 13
      cli/src/lib.rs
  5. 321 11
      cli/src/rust_template.rs
  6. 4 1
      crates/bolt-lang/Cargo.toml

+ 28 - 12
.github/workflows/run-tests.yml

@@ -4,16 +4,16 @@ on:
   pull_request:
 
 env:
-  solana_version: v1.17.0
+  solana_version: v1.18.0
   anchor_version: 0.29.0
 
 jobs:
   install:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
-      - uses: actions/cache@v3
+      - uses: actions/cache@v4
         name: cache solana cli
         id: cache-solana
         with:
@@ -22,7 +22,7 @@ jobs:
             ~/.local/share/solana/
           key: solana-${{ runner.os }}-v0000-${{ env.solana_version }}
 
-      - uses: actions/setup-node@v3
+      - uses: actions/setup-node@v4
         with:
           node-version: 20
 
@@ -42,7 +42,8 @@ jobs:
           export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH"
           yarn --frozen-lockfile --network-concurrency 2
 
-      - uses: dtolnay/rust-toolchain@stable
+      - name: install rust
+        uses: dtolnay/rust-toolchain@stable
         with:
           toolchain: stable
 
@@ -61,7 +62,7 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
       - name: Cache rust
         uses: Swatinem/rust-cache@v2
       - name: Run fmt
@@ -73,14 +74,14 @@ jobs:
     needs: install
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
       - name: Use Node ${{ matrix.node }}
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
         with:
           node-version: 20
 
       - name: Cache node dependencies
-        uses: actions/cache@v3
+        uses: actions/cache@v4
         with:
           path: '**/node_modules'
           key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
@@ -93,10 +94,18 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@v3
+      - name: install rust
+        uses: dtolnay/rust-toolchain@stable
+        with:
+          toolchain: stable
+
+      - name: Cache rust
+        uses: Swatinem/rust-cache@v2
+
+      - uses: actions/checkout@v4
 
       - name: Use Node ${{ matrix.node }}
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
         with:
           node-version: 20
 
@@ -110,7 +119,7 @@ jobs:
           export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH"
           yarn --frozen-lockfile
 
-      - uses: actions/cache@v3
+      - uses: actions/cache@v4
         name: cache solana cli
         id: cache-solana
         with:
@@ -144,6 +153,13 @@ jobs:
           npm i -g @coral-xyz/anchor-cli@${{ env.anchor_version }} ts-mocha typescript
           anchor test
 
+      - name: Install the Bolt CLI and create & build a new project
+        run: |
+          export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH"
+          cargo install --path cli --force --locked
+          bolt init test-project --force
+          cd test-project && bolt build
+
       - uses: actions/upload-artifact@v3
         if: always()
         with:

+ 42 - 38
Cargo.lock

@@ -126,9 +126,9 @@ dependencies = [
 [[package]]
 name = "anchor-attribute-access-control"
 version = "0.29.0"
-source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae"
+source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532"
 dependencies = [
- "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)",
+ "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git)",
  "proc-macro2",
  "quote",
  "syn 1.0.109",
@@ -150,9 +150,9 @@ dependencies = [
 [[package]]
 name = "anchor-attribute-account"
 version = "0.29.0"
-source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae"
+source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532"
 dependencies = [
- "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)",
+ "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git)",
  "bs58 0.5.0",
  "proc-macro2",
  "quote",
@@ -173,9 +173,9 @@ dependencies = [
 [[package]]
 name = "anchor-attribute-constant"
 version = "0.29.0"
-source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae"
+source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532"
 dependencies = [
- "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)",
+ "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git)",
  "quote",
  "syn 1.0.109",
 ]
@@ -194,9 +194,9 @@ dependencies = [
 [[package]]
 name = "anchor-attribute-error"
 version = "0.29.0"
-source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae"
+source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532"
 dependencies = [
- "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)",
+ "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git)",
  "quote",
  "syn 1.0.109",
 ]
@@ -216,9 +216,9 @@ dependencies = [
 [[package]]
 name = "anchor-attribute-event"
 version = "0.29.0"
-source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae"
+source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532"
 dependencies = [
- "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)",
+ "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git)",
  "proc-macro2",
  "quote",
  "syn 1.0.109",
@@ -238,9 +238,9 @@ dependencies = [
 [[package]]
 name = "anchor-attribute-program"
 version = "0.29.0"
-source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae"
+source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532"
 dependencies = [
- "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)",
+ "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git)",
  "quote",
  "syn 1.0.109",
 ]
@@ -248,13 +248,13 @@ dependencies = [
 [[package]]
 name = "anchor-cli"
 version = "0.29.0"
-source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae"
+source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532"
 dependencies = [
  "anchor-client",
- "anchor-lang 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)",
- "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)",
+ "anchor-lang 0.29.0 (git+https://github.com/coral-xyz/anchor.git)",
+ "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git)",
  "anyhow",
- "base64 0.13.1",
+ "base64 0.21.5",
  "bincode",
  "cargo_toml",
  "chrono",
@@ -285,9 +285,9 @@ dependencies = [
 [[package]]
 name = "anchor-client"
 version = "0.29.0"
-source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae"
+source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532"
 dependencies = [
- "anchor-lang 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)",
+ "anchor-lang 0.29.0 (git+https://github.com/coral-xyz/anchor.git)",
  "anyhow",
  "futures",
  "regex",
@@ -314,9 +314,9 @@ dependencies = [
 [[package]]
 name = "anchor-derive-accounts"
 version = "0.29.0"
-source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae"
+source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532"
 dependencies = [
- "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)",
+ "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git)",
  "quote",
  "syn 1.0.109",
 ]
@@ -337,9 +337,9 @@ dependencies = [
 [[package]]
 name = "anchor-derive-serde"
 version = "0.29.0"
-source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae"
+source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532"
 dependencies = [
- "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)",
+ "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git)",
  "borsh-derive-internal 0.10.3",
  "proc-macro2",
  "quote",
@@ -360,7 +360,7 @@ dependencies = [
 [[package]]
 name = "anchor-derive-space"
 version = "0.29.0"
-source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae"
+source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -395,19 +395,20 @@ dependencies = [
 [[package]]
 name = "anchor-lang"
 version = "0.29.0"
-source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae"
-dependencies = [
- "anchor-attribute-access-control 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)",
- "anchor-attribute-account 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)",
- "anchor-attribute-constant 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)",
- "anchor-attribute-error 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)",
- "anchor-attribute-event 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)",
- "anchor-attribute-program 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)",
- "anchor-derive-accounts 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)",
- "anchor-derive-serde 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)",
- "anchor-derive-space 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)",
+source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532"
+dependencies = [
+ "ahash 0.8.6",
+ "anchor-attribute-access-control 0.29.0 (git+https://github.com/coral-xyz/anchor.git)",
+ "anchor-attribute-account 0.29.0 (git+https://github.com/coral-xyz/anchor.git)",
+ "anchor-attribute-constant 0.29.0 (git+https://github.com/coral-xyz/anchor.git)",
+ "anchor-attribute-error 0.29.0 (git+https://github.com/coral-xyz/anchor.git)",
+ "anchor-attribute-event 0.29.0 (git+https://github.com/coral-xyz/anchor.git)",
+ "anchor-attribute-program 0.29.0 (git+https://github.com/coral-xyz/anchor.git)",
+ "anchor-derive-accounts 0.29.0 (git+https://github.com/coral-xyz/anchor.git)",
+ "anchor-derive-serde 0.29.0 (git+https://github.com/coral-xyz/anchor.git)",
+ "anchor-derive-space 0.29.0 (git+https://github.com/coral-xyz/anchor.git)",
  "arrayref",
- "base64 0.13.1",
+ "base64 0.21.5",
  "bincode",
  "borsh 0.10.3",
  "bytemuck",
@@ -437,7 +438,7 @@ dependencies = [
 [[package]]
 name = "anchor-syn"
 version = "0.29.0"
-source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae"
+source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532"
 dependencies = [
  "anyhow",
  "bs58 0.5.0",
@@ -947,9 +948,11 @@ name = "bolt-cli"
 version = "0.0.1"
 dependencies = [
  "anchor-cli",
+ "anchor-client",
  "anyhow",
  "clap 4.4.11",
  "heck 0.4.1",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -968,6 +971,7 @@ version = "0.1.0"
 name = "bolt-lang"
 version = "0.0.1"
 dependencies = [
+ "ahash 0.8.6",
  "anchor-lang 0.29.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "bolt-attribute-bolt-account",
  "bolt-attribute-bolt-component",
@@ -4684,9 +4688,9 @@ dependencies = [
 
 [[package]]
 name = "solang-parser"
-version = "0.3.2"
+version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cb9fa2fa2fa6837be8a2495486ff92e3ffe68a99b6eeba288e139efdd842457"
+checksum = "c425ce1c59f4b154717592f0bdf4715c3a1d55058883622d3157e1f0908a5b26"
 dependencies = [
  "itertools 0.11.0",
  "lalrpop",

+ 3 - 1
cli/Cargo.toml

@@ -16,7 +16,9 @@ path = "src/bin/main.rs"
 dev = []
 
 [dependencies]
-anchor-cli = { git = "https://github.com/coral-xyz/anchor.git", rev = "v0.29.0" }
+anchor-cli = { git = "https://github.com/coral-xyz/anchor.git" }
+anchor-client = { git = "https://github.com/coral-xyz/anchor.git" }
 anyhow = "1.0.32"
 heck = "0.4.0"
 clap = { version = "4.2.4", features = ["derive"] }
+syn = { version = "1.0.60", features = ["full", "extra-traits"] }

+ 311 - 13
cli/src/lib.rs

@@ -1,36 +1,50 @@
 mod rust_template;
 
-use anchor_cli::config::{Config, ConfigOverride, WithPath};
+use crate::rust_template::{create_component, create_system};
+use anchor_cli::config::{
+    Config, ConfigOverride, ProgramDeployment, TestValidator, Validator, WithPath,
+};
+use anchor_client::Cluster;
 use anyhow::{anyhow, Result};
 use clap::{Parser, Subcommand};
-use std::fs;
+use heck::{ToKebabCase, ToSnakeCase};
+use std::collections::BTreeMap;
+use std::fs::{self, File};
+use std::io::Write;
+use std::process::Stdio;
 
 pub const VERSION: &str = env!("CARGO_PKG_VERSION");
+pub const ANCHOR_VERSION: &str = anchor_cli::VERSION;
 
 #[derive(Debug, Subcommand)]
 pub enum BoltCommand {
-    // Include all existing commands from anchor_cli::Command
-    #[clap(flatten)]
-    Anchor(anchor_cli::Command),
     #[clap(about = "Create a new component")]
     Component(ComponentCommand),
     #[clap(about = "Create a new system")]
     System(SystemCommand),
+    // Include all existing commands from anchor_cli::Command
+    #[clap(flatten)]
+    Anchor(anchor_cli::Command),
+}
+
+#[derive(Debug, Parser)]
+pub struct InitCommand {
+    #[clap(short, long, help = "Workspace name")]
+    pub workspace_name: String,
 }
 
 #[derive(Debug, Parser)]
 pub struct ComponentCommand {
-    #[clap(short, long, help = "Name of the component")]
     pub name: String,
 }
 
 #[derive(Debug, Parser)]
 pub struct SystemCommand {
-    #[clap(short, long, help = "Name of the system")]
     pub name: String,
 }
 
 #[derive(Debug, Parser)]
+#[clap(version = VERSION)]
 pub struct Opts {
     #[clap(flatten)]
     pub cfg_override: ConfigOverride,
@@ -41,18 +55,302 @@ pub struct Opts {
 pub fn entry(opts: Opts) -> Result<()> {
     match opts.command {
         BoltCommand::Anchor(command) => {
-            // Delegate to the existing anchor_cli handler
-            let ops = anchor_cli::Opts {
-                cfg_override: opts.cfg_override,
-                command,
-            };
-            anchor_cli::entry(ops)
+            if let anchor_cli::Command::Init {
+                name,
+                javascript,
+                solidity,
+                no_git,
+                jest,
+                template,
+                force,
+            } = command
+            {
+                init(
+                    &opts.cfg_override,
+                    name,
+                    javascript,
+                    solidity,
+                    no_git,
+                    jest,
+                    template,
+                    force,
+                )
+            } else {
+                // Delegate to the existing anchor_cli handler
+                let opts = anchor_cli::Opts {
+                    cfg_override: opts.cfg_override,
+                    command,
+                };
+                anchor_cli::entry(opts)
+            }
         }
         BoltCommand::Component(command) => new_component(&opts.cfg_override, command.name),
         BoltCommand::System(command) => new_system(&opts.cfg_override, command.name),
     }
 }
 
+// Bolt Init
+
+#[allow(clippy::too_many_arguments)]
+fn init(
+    cfg_override: &ConfigOverride,
+    name: String,
+    javascript: bool,
+    solidity: bool,
+    no_git: bool,
+    jest: bool,
+    template: anchor_cli::rust_template::ProgramTemplate,
+    force: bool,
+) -> Result<()> {
+    if !force && Config::discover(cfg_override)?.is_some() {
+        return Err(anyhow!("Workspace already initialized"));
+    }
+
+    // We need to format different cases for the dir and the name
+    let rust_name = name.to_snake_case();
+    let project_name = if name == rust_name {
+        rust_name.clone()
+    } else {
+        name.to_kebab_case()
+    };
+
+    // Additional keywords that have not been added to the `syn` crate as reserved words
+    // https://github.com/dtolnay/syn/pull/1098
+    let extra_keywords = ["async", "await", "try"];
+    let component_name = "position";
+    let system_name = "movement";
+    // Anchor converts to snake case before writing the program name
+    if syn::parse_str::<syn::Ident>(&rust_name).is_err()
+        || extra_keywords.contains(&rust_name.as_str())
+    {
+        return Err(anyhow!(
+            "Anchor workspace name must be a valid Rust identifier. It may not be a Rust reserved word, start with a digit, or include certain disallowed characters. See https://doc.rust-lang.org/reference/identifiers.html for more detail.",
+        ));
+    }
+
+    if force {
+        fs::create_dir_all(&project_name)?;
+    } else {
+        fs::create_dir(&project_name)?;
+    }
+    std::env::set_current_dir(&project_name)?;
+    fs::create_dir_all("app")?;
+
+    let mut cfg = Config::default();
+    if jest {
+        cfg.scripts.insert(
+            "test".to_owned(),
+            if javascript {
+                "yarn run jest"
+            } else {
+                "yarn run jest --preset ts-jest"
+            }
+            .to_owned(),
+        );
+    } else {
+        cfg.scripts.insert(
+            "test".to_owned(),
+            if javascript {
+                "yarn run mocha -t 1000000 tests/"
+            } else {
+                "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
+            }
+            .to_owned(),
+        );
+    }
+
+    let mut localnet = BTreeMap::new();
+    let program_id = anchor_cli::rust_template::get_or_create_program_id(&rust_name);
+    localnet.insert(
+        rust_name,
+        ProgramDeployment {
+            address: program_id,
+            path: None,
+            idl: None,
+        },
+    );
+    if !solidity {
+        let component_id = anchor_cli::rust_template::get_or_create_program_id(component_name);
+        let system_id = anchor_cli::rust_template::get_or_create_program_id(system_name);
+        localnet.insert(
+            component_name.to_owned(),
+            ProgramDeployment {
+                address: component_id,
+                path: None,
+                idl: None,
+            },
+        );
+        localnet.insert(
+            system_name.to_owned(),
+            ProgramDeployment {
+                address: system_id,
+                path: None,
+                idl: None,
+            },
+        );
+        cfg.workspace.members.push("programs/*".to_owned());
+        cfg.workspace
+            .members
+            .push("programs-ecs/components/*".to_owned());
+        cfg.workspace
+            .members
+            .push("programs-ecs/systems/*".to_owned());
+    }
+
+    // Setup the test validator to clone Bolt programs from devnet
+    let validator = Validator {
+        url: Some("https://rpc.magicblock.app/devnet/".to_owned()),
+        rpc_port: 8899,
+        bind_address: "0.0.0.0".to_owned(),
+        ledger: ".bolt/test-ledger".to_owned(),
+        clone: Some(vec![
+            // World program
+            anchor_cli::config::CloneEntry {
+                address: "WorLD15A7CrDwLcLy4fRqtaTb9fbd8o8iqiEMUDse2n".to_owned(),
+            },
+            // World executable data
+            anchor_cli::config::CloneEntry {
+                address: "CrsqUXPpJYpVAAx5qMKU6K8RT1TzT81T8BL6JndWSeo3".to_owned(),
+            },
+            // Registry
+            anchor_cli::config::CloneEntry {
+                address: "EHLkWwAT9oebVv9ht3mtqrvHhRVMKrt54tF3MfHTey2K".to_owned(),
+            },
+        ]),
+        ..Default::default()
+    };
+
+    let test_validator = TestValidator {
+        startup_wait: 5000,
+        shutdown_wait: 2000,
+        validator: Some(validator),
+        ..Default::default()
+    };
+
+    cfg.test_validator = Some(test_validator);
+    cfg.programs.insert(Cluster::Localnet, localnet);
+    let toml = cfg.to_string();
+    fs::write("Anchor.toml", toml)?;
+
+    // Initialize .gitignore file
+    fs::write(".gitignore", rust_template::git_ignore())?;
+
+    // Initialize .prettierignore file
+    fs::write(".prettierignore", rust_template::prettier_ignore())?;
+
+    // Remove the default programs if `--force` is passed
+    if force {
+        let programs_path = std::env::current_dir()?
+            .join(if solidity { "solidity" } else { "programs" })
+            .join(&project_name);
+        fs::create_dir_all(&programs_path)?;
+        fs::remove_dir_all(&programs_path)?;
+        let programs_ecs_path = std::env::current_dir()?
+            .join("programs-ecs")
+            .join(&project_name);
+        fs::create_dir_all(&programs_ecs_path)?;
+        fs::remove_dir_all(&programs_ecs_path)?;
+    }
+
+    // Build the program.
+    if solidity {
+        anchor_cli::solidity_template::create_program(&project_name)?;
+    } else {
+        create_component(component_name)?;
+        create_system(system_name)?;
+        anchor_cli::rust_template::create_program(&project_name, template)?;
+    }
+
+    // Build the test suite.
+    fs::create_dir_all("tests")?;
+    // Build the migrations directory.
+    fs::create_dir_all("migrations")?;
+
+    if javascript {
+        // Build javascript config
+        let mut package_json = File::create("package.json")?;
+        package_json.write_all(rust_template::package_json(jest).as_bytes())?;
+
+        if jest {
+            let mut test = File::create(format!("tests/{}.test.js", &project_name))?;
+            if solidity {
+                test.write_all(anchor_cli::solidity_template::jest(&project_name).as_bytes())?;
+            } else {
+                test.write_all(rust_template::jest(&project_name).as_bytes())?;
+            }
+        } else {
+            let mut test = File::create(format!("tests/{}.js", &project_name))?;
+            if solidity {
+                test.write_all(anchor_cli::solidity_template::mocha(&project_name).as_bytes())?;
+            } else {
+                test.write_all(rust_template::mocha(&project_name).as_bytes())?;
+            }
+        }
+
+        let mut deploy = File::create("migrations/deploy.js")?;
+
+        deploy.write_all(anchor_cli::rust_template::deploy_script().as_bytes())?;
+    } else {
+        // Build typescript config
+        let mut ts_config = File::create("tsconfig.json")?;
+        ts_config.write_all(anchor_cli::rust_template::ts_config(jest).as_bytes())?;
+
+        let mut ts_package_json = File::create("package.json")?;
+        ts_package_json.write_all(rust_template::ts_package_json(jest).as_bytes())?;
+
+        let mut deploy = File::create("migrations/deploy.ts")?;
+        deploy.write_all(anchor_cli::rust_template::ts_deploy_script().as_bytes())?;
+
+        let mut mocha = File::create(format!("tests/{}.ts", &project_name))?;
+        if solidity {
+            mocha.write_all(anchor_cli::solidity_template::ts_mocha(&project_name).as_bytes())?;
+        } else {
+            mocha.write_all(rust_template::ts_mocha(&project_name).as_bytes())?;
+        }
+    }
+
+    let yarn_result = install_node_modules("yarn")?;
+    if !yarn_result.status.success() {
+        println!("Failed yarn install will attempt to npm install");
+        install_node_modules("npm")?;
+    }
+
+    if !no_git {
+        let git_result = std::process::Command::new("git")
+            .arg("init")
+            .stdout(Stdio::inherit())
+            .stderr(Stdio::inherit())
+            .output()
+            .map_err(|e| anyhow::format_err!("git init failed: {}", e.to_string()))?;
+        if !git_result.status.success() {
+            eprintln!("Failed to automatically initialize a new git repository");
+        }
+    }
+
+    println!("{project_name} initialized");
+
+    Ok(())
+}
+
+// Install node modules
+fn install_node_modules(cmd: &str) -> Result<std::process::Output> {
+    let mut command = std::process::Command::new(if cfg!(target_os = "windows") {
+        "cmd"
+    } else {
+        cmd
+    });
+    if cfg!(target_os = "windows") {
+        command.arg(format!("/C {} install", cmd));
+    } else {
+        command.arg("install");
+    }
+    command
+        .stdout(Stdio::inherit())
+        .stderr(Stdio::inherit())
+        .output()
+        .map_err(|e| anyhow::format_err!("{} install failed: {}", cmd, e.to_string()))
+}
+
 // Create a new component from the template
 fn new_component(cfg_override: &ConfigOverride, name: String) -> Result<()> {
     with_workspace(cfg_override, |cfg| {

+ 321 - 11
cli/src/rust_template.rs

@@ -1,12 +1,13 @@
+use crate::ANCHOR_VERSION;
 use crate::VERSION;
 use anchor_cli::Files;
 use anyhow::Result;
-use heck::{ToKebabCase, ToSnakeCase, ToUpperCamelCase};
+use heck::{ToSnakeCase, ToUpperCamelCase};
 use std::path::{Path, PathBuf};
 
 /// Create a component from the given name.
 pub fn create_component(name: &str) -> Result<()> {
-    let program_path = Path::new("programs").join(name);
+    let program_path = Path::new("programs-ecs/components").join(name);
     let common_files = vec![
         (
             PathBuf::from("Cargo.toml".to_string()),
@@ -22,7 +23,7 @@ pub fn create_component(name: &str) -> Result<()> {
 
 /// Create a system from the given name.
 pub(crate) fn create_system(name: &str) -> Result<()> {
-    let program_path = Path::new("programs").join(name);
+    let program_path = Path::new("programs-ecs/systems").join(name);
     let common_files = vec![
         (
             PathBuf::from("Cargo.toml".to_string()),
@@ -36,7 +37,7 @@ pub(crate) fn create_system(name: &str) -> Result<()> {
     anchor_cli::create_files(&[common_files, template_files].concat())
 }
 
-/// Create a program with a single `lib.rs` file.
+/// Create a component which holds position data.
 fn create_component_template_simple(name: &str, program_path: &Path) -> Files {
     vec![(
         program_path.join("src").join("lib.rs"),
@@ -52,7 +53,7 @@ pub mod {} {{
 }}
 
 #[account]
-#[bolt_account(component_id = "{}")]
+#[bolt_account(component_id = "")]
 pub struct {} {{
     pub x: i64,
     pub y: i64,
@@ -64,13 +65,12 @@ pub struct {} {{
             anchor_cli::rust_template::get_or_create_program_id(name),
             name.to_upper_camel_case(),
             name.to_snake_case(),
-            name.to_kebab_case(),
             name.to_upper_camel_case(),
         ),
     )]
 }
 
-/// Create a program with a single `lib.rs` file.
+/// Create a system which operates on a Position component.
 fn create_system_template_simple(name: &str, program_path: &Path) -> Files {
     vec![(
         program_path.join("src").join("lib.rs"),
@@ -85,10 +85,8 @@ pub mod {} {{
     use super::*;
 
     pub fn execute(ctx: Context<Component>, args: Vec<u8>) -> Result<Position> {{
-
         let mut position = Position::from_account_info(&ctx.accounts.position)?;
         position.x += 1;
-
         Ok(position)
     }}
 }}
@@ -105,7 +103,6 @@ pub struct Position {{
     pub x: i64,
     pub y: i64,
     pub z: i64,
-    #[max_len(20)]
     pub description: String,
 }}
 "#,
@@ -118,7 +115,9 @@ pub struct Position {{
 const fn workspace_manifest() -> &'static str {
     r#"[workspace]
 members = [
-    "programs/*"
+    "programs/*",
+    "programs-ecs/components/*",
+    "programs-ecs/systems/*"
 ]
 resolver = "2"
 
@@ -133,6 +132,292 @@ codegen-units = 1
 "#
 }
 
+pub fn package_json(jest: bool) -> String {
+    if jest {
+        format!(
+            r#"{{
+        "scripts": {{
+            "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
+            "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
+        }},
+        "dependencies": {{
+            "@coral-xyz/anchor": "^{VERSION}"
+        }},
+        "devDependencies": {{
+            "jest": "^29.0.3",
+            "prettier": "^2.6.2"
+        }}
+    }}
+    "#
+        )
+    } else {
+        format!(
+            r#"{{
+    "scripts": {{
+        "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
+        "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
+    }},
+    "dependencies": {{
+        "@coral-xyz/anchor": "^{VERSION}"
+    }},
+    "devDependencies": {{
+        "chai": "^4.3.4",
+        "mocha": "^9.0.3",
+        "prettier": "^2.6.2",
+        "@metaplex-foundation/beet": "^0.7.1",
+        "@metaplex-foundation/beet-solana": "^0.4.0",
+         "bolt-sdk": "latest"
+    }}
+}}
+"#
+        )
+    }
+}
+
+pub fn ts_package_json(jest: bool) -> String {
+    if jest {
+        format!(
+            r#"{{
+        "scripts": {{
+            "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
+            "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
+        }},
+        "dependencies": {{
+            "@coral-xyz/anchor": "^{ANCHOR_VERSION}"
+        }},
+        "devDependencies": {{
+            "@types/bn.js": "^5.1.0",
+            "@types/jest": "^29.0.3",
+            "jest": "^29.0.3",
+            "prettier": "^2.6.2",
+            "ts-jest": "^29.0.2",
+            "typescript": "^4.3.5",
+            "@metaplex-foundation/beet": "^0.7.1",
+            "@metaplex-foundation/beet-solana": "^0.4.0",
+            "bolt-sdk": "latest"
+        }}
+    }}
+    "#
+        )
+    } else {
+        format!(
+            r#"{{
+    "scripts": {{
+        "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
+        "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
+    }},
+    "dependencies": {{
+        "@coral-xyz/anchor": "^{ANCHOR_VERSION}"
+    }},
+    "devDependencies": {{
+        "chai": "^4.3.4",
+        "mocha": "^9.0.3",
+        "ts-mocha": "^10.0.0",
+        "@types/bn.js": "^5.1.0",
+        "@types/chai": "^4.3.0",
+        "@types/mocha": "^9.0.0",
+        "typescript": "^4.3.5",
+        "prettier": "^2.6.2",
+        "@metaplex-foundation/beet": "^0.7.1",
+        "@metaplex-foundation/beet-solana": "^0.4.0",
+        "bolt-sdk": "latest"
+    }}
+}}
+"#
+        )
+    }
+}
+
+pub fn mocha(name: &str) -> String {
+    format!(
+        r#"const anchor = require("@coral-xyz/anchor");
+const boltSdk = require("bolt-sdk");
+const {{
+    createInitializeNewWorldInstruction,
+    FindWorldPda,
+    FindWorldRegistryPda,
+    Registry,
+    World
+}} = boltSdk;
+
+describe("{}", () => {{
+  // Configure the client to use the local cluster.
+  const provider = anchor.AnchorProvider.env();
+  anchor.setProvider(provider);
+
+  it("InitializeNewWorld", async () => {{
+      const registry = await Registry.fromAccountAddress(provider.connection, registryPda);
+      worldId = new anchor.BN(registry.worlds);
+      worldPda = FindWorldPda(new anchor.BN(worldId))
+      const initializeWorldIx = createInitializeNewWorldInstruction(
+          {{
+              world: worldPda,
+              registry: registryPda,
+              payer: provider.wallet.publicKey,
+          }});
+
+      const tx = new anchor.web3.Transaction().add(initializeWorldIx);
+      const txSign = await provider.sendAndConfirm(tx);
+      console.log(`Initialized a new world (ID=${{worldId}}). Initialization signature: ${{txSign}}`);
+    }});
+  }});
+}});
+"#,
+        name,
+    )
+}
+
+pub fn jest(name: &str) -> String {
+    format!(
+        r#"const anchor = require("@coral-xyz/anchor");
+const boltSdk = require("bolt-sdk");
+const {{
+    createInitializeNewWorldInstruction,
+    FindWorldPda,
+    FindWorldRegistryPda,
+    Registry,
+    World
+}} = boltSdk;
+
+describe("{}", () => {{
+  // Configure the client to use the local cluster.
+  const provider = anchor.AnchorProvider.env();
+  anchor.setProvider(provider);
+
+  // Constants used to test the program.
+  const registryPda = FindWorldRegistryPda();
+  let worldId: anchor.BN;
+  let worldPda: PublicKey;
+
+  it("InitializeNewWorld", async () => {{
+      const registry = await Registry.fromAccountAddress(provider.connection, registryPda);
+      worldId = new anchor.BN(registry.worlds);
+      worldPda = FindWorldPda(new anchor.BN(worldId))
+      const initializeWorldIx = createInitializeNewWorldInstruction(
+          {{
+              world: worldPda,
+              registry: registryPda,
+              payer: provider.wallet.publicKey,
+          }});
+
+      const tx = new anchor.web3.Transaction().add(initializeWorldIx);
+      const txSign = await provider.sendAndConfirm(tx);
+      console.log(`Initialized a new world (ID=${{worldId}}). Initialization signature: ${{txSign}}`);
+    }});
+  }});
+"#,
+        name,
+    )
+}
+
+pub fn ts_mocha(name: &str) -> String {
+    format!(
+        r#"import * as anchor from "@coral-xyz/anchor";
+import {{ Program }} from "@coral-xyz/anchor";
+import {{ PublicKey }} from "@solana/web3.js";
+import {{ Position }} from "../target/types/position";
+import {{ Movement }} from "../target/types/movement";
+import {{
+    createInitializeNewWorldInstruction,
+    FindWorldPda,
+    FindWorldRegistryPda,
+    FindEntityPda,
+    Registry,
+    World,
+    createAddEntityInstruction,
+    createInitializeComponentInstruction,
+    FindComponentPda, createApplyInstruction
+}} from "bolt-sdk"
+import {{expect}} from "chai";
+
+describe("{}", () => {{
+  // Configure the client to use the local cluster.
+  const provider = anchor.AnchorProvider.env();
+  anchor.setProvider(provider);
+
+  // Constants used to test the program.
+  const registryPda = FindWorldRegistryPda();
+  let worldId: anchor.BN;
+  let worldPda: PublicKey;
+  let entityPda: PublicKey;
+
+  const positionComponent = anchor.workspace.Position as Program<Position>;
+  const systemMovement = anchor.workspace.Movement as Program<Movement>;
+
+  it("InitializeNewWorld", async () => {{
+        const registry = await Registry.fromAccountAddress(provider.connection, registryPda);
+        worldId = new anchor.BN(registry.worlds);
+        worldPda = FindWorldPda(new anchor.BN(worldId))
+        const initializeWorldIx = createInitializeNewWorldInstruction(
+            {{
+                world: worldPda,
+                registry: registryPda,
+                payer: provider.wallet.publicKey,
+            }});
+
+        const tx = new anchor.web3.Transaction().add(initializeWorldIx);
+        const txSign = await provider.sendAndConfirm(tx);
+        console.log(`Initialized a new world (ID=${{worldId}}). Initialization signature: ${{txSign}}`);
+    }});
+
+  it("Add an entity", async () => {{
+      const world = await World.fromAccountAddress(provider.connection, worldPda);
+      const entityId = new anchor.BN(world.entities);
+      entityPda = FindEntityPda(worldId, entityId);
+
+      let createEntityIx = createAddEntityInstruction({{
+          world: worldPda,
+          payer: provider.wallet.publicKey,
+          entity: entityPda,
+      }});
+      const tx = new anchor.web3.Transaction().add(createEntityIx);
+      const txSign = await provider.sendAndConfirm(tx);
+      console.log(`Initialized a new Entity (ID=${{worldId}}). Initialization signature: ${{txSign}}`);
+  }});
+
+  it("Add a component", async () => {{
+      const positionComponentPda = FindComponentPda(positionComponent.programId, entityPda, "");
+      let initComponentIx = createInitializeComponentInstruction({{
+          payer: provider.wallet.publicKey,
+          entity: entityPda,
+          data: positionComponentPda,
+          componentProgram: positionComponent.programId,
+      }});
+
+      const tx = new anchor.web3.Transaction().add(initComponentIx);
+      const txSign = await provider.sendAndConfirm(tx);
+      console.log(`Initialized a new component. Initialization signature: ${{txSign}}`);
+  }});
+
+  it("Apply a system", async () => {{
+      const positionComponentPda = FindComponentPda(positionComponent.programId, entityPda, "");
+      // Check that the component has been initialized and x is 0
+      let positionData = await positionComponent.account.position.fetch(
+          positionComponentPda
+      );
+      expect(positionData.x.toNumber()).to.eq(0);
+      let applySystemIx = createApplyInstruction({{
+          componentProgram: positionComponent.programId,
+          boltSystem: systemMovement.programId,
+          boltComponent: positionComponentPda,
+      }}, {{args: new Uint8Array()}});
+
+      const tx = new anchor.web3.Transaction().add(applySystemIx);
+      await provider.sendAndConfirm(tx);
+
+      // Check that the system has been applied and x is > 0
+      positionData = await positionComponent.account.position.fetch(
+          positionComponentPda
+      );
+      expect(positionData.x.toNumber()).to.gt(0);
+  }});
+
+}});
+"#,
+        name.to_upper_camel_case(),
+    )
+}
+
 fn cargo_toml(name: &str) -> String {
     format!(
         r#"[package]
@@ -168,3 +453,28 @@ fn xargo_toml() -> &'static str {
 features = []
 "#
 }
+pub fn git_ignore() -> &'static str {
+    r#"
+.anchor
+.bolt
+.DS_Store
+target
+**/*.rs.bk
+node_modules
+test-ledger
+.yarn
+"#
+}
+
+pub fn prettier_ignore() -> &'static str {
+    r#"
+.anchor
+.bolt
+.DS_Store
+target
+node_modules
+dist
+build
+test-ledger
+"#
+}

+ 4 - 1
crates/bolt-lang/Cargo.toml

@@ -20,4 +20,7 @@ bolt-system = { path = "../../programs/bolt-system", features = ["cpi"], version
 
 # Other dependencies
 serde = { version = "1.0", features = ["derive"] }
-serde_json = "1.0"
+serde_json = "1.0"
+
+# TODO: Remove once https://github.com/solana-labs/solana/issues/33504 is resolved.
+ahash = "=0.8.6"