Workspaces
This commit is contained in:
parent
453c0d95de
commit
9570830b65
File diff suppressed because it is too large
Load Diff
|
@ -17,3 +17,10 @@ syn = { version = "1.0.54", features = ["full", "extra-traits"] }
|
|||
anchor-syn = { path = "../syn", features = ["idl"] }
|
||||
serde_json = "1.0"
|
||||
shellexpand = "2.1.0"
|
||||
serde_yaml = "0.8"
|
||||
toml = "0.5.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
solana-sdk = "1.5.0"
|
||||
serum-common = { git = "https://github.com/project-serum/serum-dex", features = ["client"] }
|
||||
dirs = "3.0"
|
||||
heck = "0.3.1"
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
use anchor_syn::idl::Idl;
|
||||
use anyhow::{anyhow, Error, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serum_common::client::Cluster;
|
||||
use std::fs::{self, File};
|
||||
use std::io::prelude::*;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Config {
|
||||
cluster: Cluster,
|
||||
wallet: WalletPath,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
// Searches all parent directories for an Anchor.toml file.
|
||||
pub fn discover() -> Result<Option<(Self, PathBuf, Option<PathBuf>)>> {
|
||||
// Set to true if we ever see a Cargo.toml file when traversing the
|
||||
// parent directories.
|
||||
let mut cargo_toml = None;
|
||||
|
||||
let _cwd = std::env::current_dir()?;
|
||||
let mut cwd_opt = Some(_cwd.as_path());
|
||||
|
||||
while let Some(cwd) = cwd_opt {
|
||||
let files = fs::read_dir(cwd)?;
|
||||
// Cargo.toml file for this directory level.
|
||||
let mut cargo_toml_level = None;
|
||||
let mut anchor_toml = None;
|
||||
for f in files {
|
||||
let p = f?.path();
|
||||
if let Some(filename) = p.file_name() {
|
||||
if filename.to_str() == Some("Cargo.toml") {
|
||||
cargo_toml_level = Some(PathBuf::from(p));
|
||||
} else if filename.to_str() == Some("Anchor.toml") {
|
||||
let mut cfg_file = File::open(&p)?;
|
||||
let mut cfg_contents = String::new();
|
||||
cfg_file.read_to_string(&mut cfg_contents)?;
|
||||
let cfg = cfg_contents.parse()?;
|
||||
anchor_toml = Some((cfg, PathBuf::from(p)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((cfg, parent)) = anchor_toml {
|
||||
return Ok(Some((cfg, parent, cargo_toml)));
|
||||
}
|
||||
|
||||
if cargo_toml.is_none() {
|
||||
cargo_toml = cargo_toml_level;
|
||||
}
|
||||
|
||||
cwd_opt = cwd.parent();
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
// Pubkey serializes as a byte array so use this type a hack to serialize
|
||||
// into base 58 strings.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct _Config {
|
||||
cluster: String,
|
||||
wallet: String,
|
||||
}
|
||||
|
||||
impl ToString for Config {
|
||||
fn to_string(&self) -> String {
|
||||
let cfg = _Config {
|
||||
cluster: format!("{}", self.cluster),
|
||||
wallet: self.wallet.to_string(),
|
||||
};
|
||||
|
||||
toml::to_string(&cfg).expect("Must be well formed")
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Config {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let cfg: _Config = toml::from_str(s)
|
||||
.map_err(|e| anyhow::format_err!("Unable to deserialize config: {}", e.to_string()))?;
|
||||
|
||||
Ok(Config {
|
||||
cluster: cfg.cluster.parse()?,
|
||||
wallet: cfg.wallet.parse()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_cargo_toml() -> Result<Option<PathBuf>> {
|
||||
let _cwd = std::env::current_dir()?;
|
||||
let mut cwd_opt = Some(_cwd.as_path());
|
||||
while let Some(cwd) = cwd_opt {
|
||||
let files = fs::read_dir(cwd)?;
|
||||
for f in files {
|
||||
let p = f?.path();
|
||||
if let Some(filename) = p.file_name() {
|
||||
if filename.to_str() == Some("Cargo.toml") {
|
||||
return Ok(Some(PathBuf::from(p)));
|
||||
}
|
||||
}
|
||||
}
|
||||
cwd_opt = cwd.parent();
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub fn read_all_programs() -> Result<Vec<Program>> {
|
||||
let files = fs::read_dir("programs")?;
|
||||
let mut r = vec![];
|
||||
for f in files {
|
||||
let path = f?.path();
|
||||
let idl = anchor_syn::parser::file::parse(path.join("src/lib.rs"))?;
|
||||
let lib_name = extract_lib_name(&path.join("Cargo.toml"))?;
|
||||
r.push(Program {
|
||||
lib_name,
|
||||
path,
|
||||
idl,
|
||||
});
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
pub fn extract_lib_name(path: impl AsRef<Path>) -> Result<String> {
|
||||
let mut toml = File::open(path)?;
|
||||
let mut contents = String::new();
|
||||
toml.read_to_string(&mut contents)?;
|
||||
|
||||
let cargo_toml: toml::Value = contents.parse()?;
|
||||
|
||||
match cargo_toml {
|
||||
toml::Value::Table(t) => match t.get("lib") {
|
||||
None => Err(anyhow!("lib not found in Cargo.toml")),
|
||||
Some(lib) => match lib
|
||||
.get("name")
|
||||
.ok_or(anyhow!("lib name not found in Cargo.toml"))?
|
||||
{
|
||||
toml::Value::String(n) => Ok(n.to_string()),
|
||||
_ => Err(anyhow!("lib name must be a string")),
|
||||
},
|
||||
},
|
||||
_ => Err(anyhow!("Invalid Cargo.toml")),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Program {
|
||||
pub lib_name: String,
|
||||
pub path: PathBuf,
|
||||
pub idl: Idl,
|
||||
}
|
||||
|
||||
serum_common::home_path!(WalletPath, ".config/solana/id.json");
|
380
cli/src/main.rs
380
cli/src/main.rs
|
@ -1,5 +1,18 @@
|
|||
use crate::config::{find_cargo_toml, read_all_programs, Config, Program};
|
||||
use anchor_syn::idl::Idl;
|
||||
use anyhow::Result;
|
||||
use clap::Clap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{self, File};
|
||||
use std::io::prelude::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Child, Stdio};
|
||||
use std::string::ToString;
|
||||
|
||||
mod config;
|
||||
mod template;
|
||||
|
||||
#[derive(Debug, Clap)]
|
||||
pub struct Opts {
|
||||
|
@ -9,6 +22,18 @@ pub struct Opts {
|
|||
|
||||
#[derive(Debug, Clap)]
|
||||
pub enum Command {
|
||||
/// Initializes a workspace.
|
||||
Init { name: String },
|
||||
/// Builds a Solana program.
|
||||
Build {
|
||||
/// Output directory for the IDL.
|
||||
#[clap(short, long)]
|
||||
idl: Option<String>,
|
||||
},
|
||||
/// Runs integration tests against a localnetwork.
|
||||
Test,
|
||||
/// Creates a new program.
|
||||
New { name: String },
|
||||
/// Outputs an interface definition file.
|
||||
Idl {
|
||||
/// Path to the program's interface definition.
|
||||
|
@ -18,40 +43,349 @@ pub enum Command {
|
|||
#[clap(short, long)]
|
||||
out: Option<String>,
|
||||
},
|
||||
/// Generates a client module.
|
||||
Gen {
|
||||
/// Path to the program's interface definition.
|
||||
#[clap(short, long, required_unless_present("idl"))]
|
||||
file: Option<String>,
|
||||
/// Output file (stdout if not specified).
|
||||
#[clap(short, long)]
|
||||
out: Option<String>,
|
||||
#[clap(short, long)]
|
||||
idl: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let opts = Opts::parse();
|
||||
|
||||
match opts.command {
|
||||
Command::Idl { file, out } => idl(file, out),
|
||||
Command::Gen { file, out, idl } => gen(file, out, idl),
|
||||
Command::Init { name } => init(name),
|
||||
Command::Build { idl } => build(idl),
|
||||
Command::Test => test(),
|
||||
Command::New { name } => new(name),
|
||||
Command::Idl { file, out } => {
|
||||
if out.is_none() {
|
||||
return idl(file, None);
|
||||
}
|
||||
idl(file, Some(&PathBuf::from(out.unwrap())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn idl(file: String, out: Option<String>) -> Result<()> {
|
||||
let file = shellexpand::tilde(&file);
|
||||
let idl = anchor_syn::parser::file::parse(&file)?;
|
||||
let idl_json = serde_json::to_string_pretty(&idl)?;
|
||||
if let Some(out) = out {
|
||||
std::fs::write(out, idl_json)?;
|
||||
return Ok(());
|
||||
fn init(name: String) -> Result<()> {
|
||||
let cfg = Config::discover()?;
|
||||
|
||||
if cfg.is_some() {
|
||||
println!("Anchor workspace already initialized");
|
||||
}
|
||||
println!("{}", idl_json);
|
||||
|
||||
fs::create_dir(name.clone())?;
|
||||
std::env::set_current_dir(&name)?;
|
||||
fs::create_dir("app")?;
|
||||
|
||||
let cfg = Config::default();
|
||||
let toml = cfg.to_string();
|
||||
let mut file = File::create("Anchor.toml")?;
|
||||
file.write_all(toml.as_bytes())?;
|
||||
|
||||
// Build virtual manifest.
|
||||
let mut virt_manifest = File::create("Cargo.toml")?;
|
||||
virt_manifest.write_all(template::virtual_manifest().as_bytes())?;
|
||||
|
||||
// Build the program.
|
||||
fs::create_dir("programs")?;
|
||||
|
||||
new_program(&name)?;
|
||||
|
||||
// Build the test suite.
|
||||
fs::create_dir("tests")?;
|
||||
let mut mocha = File::create(&format!("tests/{}.js", name))?;
|
||||
mocha.write_all(template::mocha(&name).as_bytes())?;
|
||||
|
||||
println!("{} initialized", name);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn gen(file: Option<String>, out: Option<String>, idl: Option<String>) -> Result<()> {
|
||||
// TODO. Generate clients in any language.
|
||||
// Creates a new program crate in the `programs/<name>` directory.
|
||||
fn new(name: String) -> Result<()> {
|
||||
match Config::discover()? {
|
||||
None => {
|
||||
println!("Not in anchor workspace.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Some((_cfg, cfg_path, _inside_cargo)) => {
|
||||
match cfg_path.parent() {
|
||||
None => {
|
||||
println!("Unable to make new program");
|
||||
}
|
||||
Some(parent) => {
|
||||
std::env::set_current_dir(&parent)?;
|
||||
new_program(&name)?;
|
||||
println!("Created new program.");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Creates a new program crate in the current directory with `name`.
|
||||
fn new_program(name: &str) -> Result<()> {
|
||||
fs::create_dir(&format!("programs/{}", name))?;
|
||||
fs::create_dir(&format!("programs/{}/src/", name))?;
|
||||
let mut cargo_toml = File::create(&format!("programs/{}/Cargo.toml", name))?;
|
||||
cargo_toml.write_all(template::cargo_toml(&name).as_bytes())?;
|
||||
let mut xargo_toml = File::create(&format!("programs/{}/Xargo.toml", name))?;
|
||||
xargo_toml.write_all(template::xargo_toml(&name).as_bytes())?;
|
||||
let mut lib_rs = File::create(&format!("programs/{}/src/lib.rs", name))?;
|
||||
lib_rs.write_all(template::lib_rs(&name).as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build(idl: Option<String>) -> Result<()> {
|
||||
match Config::discover()? {
|
||||
None => build_cwd(idl),
|
||||
Some((cfg, cfg_path, inside_cargo)) => build_ws(cfg, cfg_path, inside_cargo, idl),
|
||||
}
|
||||
}
|
||||
|
||||
// Runs the build inside a workspace.
|
||||
//
|
||||
// * Builds a single program if the current dir is within a Cargo subdirectory,
|
||||
// e.g., `programs/my-program/src`.
|
||||
// * Builds *all* programs if thje current dir is anywhere else in the workspace.
|
||||
//
|
||||
fn build_ws(
|
||||
cfg: Config,
|
||||
cfg_path: PathBuf,
|
||||
cargo_toml: Option<PathBuf>,
|
||||
idl: Option<String>,
|
||||
) -> Result<()> {
|
||||
let idl_out = match idl {
|
||||
Some(idl) => Some(PathBuf::from(idl)),
|
||||
None => {
|
||||
let cfg_parent = match cfg_path.parent() {
|
||||
None => return Err(anyhow::anyhow!("Invalid Anchor.toml")),
|
||||
Some(parent) => parent,
|
||||
};
|
||||
fs::create_dir_all(cfg_parent.join("target/idl"))?;
|
||||
Some(cfg_parent.join("target/idl"))
|
||||
}
|
||||
};
|
||||
match cargo_toml {
|
||||
None => build_all(cfg, cfg_path, idl_out),
|
||||
Some(ct) => _build_cwd(ct, idl_out),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_all(_cfg: Config, cfg_path: PathBuf, idl_out: Option<PathBuf>) -> Result<()> {
|
||||
match cfg_path.parent() {
|
||||
None => Err(anyhow::anyhow!(
|
||||
"Invalid Anchor.toml at {}",
|
||||
cfg_path.display()
|
||||
)),
|
||||
Some(parent) => {
|
||||
let files = fs::read_dir(parent.join("programs"))?;
|
||||
for f in files {
|
||||
let p = f?.path();
|
||||
_build_cwd(p.join("Cargo.toml"), idl_out.clone())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_cwd(idl_out: Option<String>) -> Result<()> {
|
||||
match find_cargo_toml()? {
|
||||
None => {
|
||||
println!("Cargo.toml not found");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Some(cargo_toml) => _build_cwd(cargo_toml, idl_out.map(PathBuf::from)),
|
||||
}
|
||||
}
|
||||
|
||||
// Runs the build command outside of a workspace.
|
||||
fn _build_cwd(cargo_toml: PathBuf, idl_out: Option<PathBuf>) -> Result<()> {
|
||||
match cargo_toml.parent() {
|
||||
None => return Err(anyhow::anyhow!("Unable to find parent")),
|
||||
Some(p) => std::env::set_current_dir(&p)?,
|
||||
};
|
||||
|
||||
let exit = std::process::Command::new("cargo")
|
||||
.arg("build-bpf")
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.output()
|
||||
.map_err(|e| anyhow::format_err!("{}", e.to_string()))?;
|
||||
if !exit.status.success() {
|
||||
std::process::exit(exit.status.code().unwrap_or(1));
|
||||
}
|
||||
|
||||
// Always assume idl is located ar src/lib.rs.
|
||||
let idl = extract_idl("src/lib.rs")?;
|
||||
|
||||
let out = match idl_out {
|
||||
None => PathBuf::from(".").join(&idl.name).with_extension("json"),
|
||||
Some(o) => PathBuf::from(&o.join(&idl.name).with_extension("json")),
|
||||
};
|
||||
|
||||
write_idl(&idl, Some(&out))
|
||||
}
|
||||
|
||||
fn idl(file: String, out: Option<&Path>) -> Result<()> {
|
||||
let idl = extract_idl(&file)?;
|
||||
write_idl(&idl, out)
|
||||
}
|
||||
|
||||
fn extract_idl(file: &str) -> Result<Idl> {
|
||||
let file = shellexpand::tilde(file);
|
||||
anchor_syn::parser::file::parse(&*file)
|
||||
}
|
||||
|
||||
fn write_idl(idl: &Idl, out: Option<&Path>) -> Result<()> {
|
||||
let idl_json = serde_json::to_string_pretty(idl)?;
|
||||
match out.as_ref() {
|
||||
None => println!("{}", idl_json),
|
||||
Some(out) => std::fs::write(out, idl_json)?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Builds, deploys, and tests all workspace programs in a single command.
|
||||
fn test() -> Result<()> {
|
||||
// Switch directories to top level workspace.
|
||||
set_workspace_dir_or_exit();
|
||||
|
||||
// Build everything.
|
||||
build(None)?;
|
||||
|
||||
// Switch again (todo: restore cwd in `build` command).
|
||||
set_workspace_dir_or_exit();
|
||||
|
||||
// Bootup validator.
|
||||
let mut validator_handle = start_test_validator()?;
|
||||
|
||||
// Deploy all programs.
|
||||
let programs = deploy_ws()?;
|
||||
|
||||
// Store deployed program addresses in IDL metadata (for consumption by
|
||||
// client + tests).
|
||||
for (program, address) in programs {
|
||||
// Add metadata to the IDL.
|
||||
let mut idl = program.idl;
|
||||
idl.metadata = Some(serde_json::to_value(IdlTestMetadata {
|
||||
address: address.to_string(),
|
||||
})?);
|
||||
// Persist it.
|
||||
let idl_out = PathBuf::from("target/idl")
|
||||
.join(&idl.name)
|
||||
.with_extension("json");
|
||||
write_idl(&idl, Some(&idl_out))?;
|
||||
}
|
||||
|
||||
// Run the tests.
|
||||
if let Err(e) = std::process::Command::new("mocha")
|
||||
.arg("tests/")
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.output()
|
||||
{
|
||||
validator_handle.kill()?;
|
||||
return Err(anyhow::format_err!("{}", e.to_string()));
|
||||
}
|
||||
validator_handle.kill()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct IdlTestMetadata {
|
||||
address: String,
|
||||
}
|
||||
|
||||
fn start_test_validator() -> Result<Child> {
|
||||
fs::create_dir_all(".anchor")?;
|
||||
let test_ledger_filename = ".anchor/test-ledger";
|
||||
let test_ledger_log_filename = ".anchor/test-ledger-log.txt";
|
||||
|
||||
if Path::new(test_ledger_filename).exists() {
|
||||
std::fs::remove_dir_all(test_ledger_filename)?;
|
||||
}
|
||||
if Path::new(test_ledger_log_filename).exists() {
|
||||
std::fs::remove_file(test_ledger_log_filename)?;
|
||||
}
|
||||
|
||||
// Start a validator for testing.
|
||||
let test_validator_stdout = File::create(test_ledger_log_filename)?;
|
||||
let test_validator_stderr = test_validator_stdout.try_clone()?;
|
||||
let validator_handle = std::process::Command::new("solana-test-validator")
|
||||
.arg("--ledger")
|
||||
.arg(test_ledger_filename)
|
||||
.stdout(Stdio::from(test_validator_stdout))
|
||||
.stderr(Stdio::from(test_validator_stderr))
|
||||
.spawn()
|
||||
.map_err(|e| anyhow::format_err!("{}", e.to_string()))?;
|
||||
|
||||
// TODO: do something more sensible than sleeping.
|
||||
std::thread::sleep(std::time::Duration::from_millis(2000));
|
||||
|
||||
Ok(validator_handle)
|
||||
}
|
||||
|
||||
fn deploy_ws() -> Result<Vec<(Program, Pubkey)>> {
|
||||
let mut programs = vec![];
|
||||
println!("Deploying workspace to http://localhost:8899...");
|
||||
for program in read_all_programs()? {
|
||||
let binary_path = format!(
|
||||
"target/bpfel-unknown-unknown/release/{}.so",
|
||||
program.lib_name
|
||||
);
|
||||
println!("Deploying {}...", binary_path);
|
||||
let exit = std::process::Command::new("solana")
|
||||
.arg("deploy")
|
||||
.arg(&binary_path)
|
||||
.arg("--url")
|
||||
.arg("http://localhost:8899") // TODO: specify network via cli.
|
||||
.arg("--keypair")
|
||||
.arg(".anchor/test-ledger/faucet-keypair.json") // TODO: specify wallet.
|
||||
.output()
|
||||
.expect("Must deploy");
|
||||
if !exit.status.success() {
|
||||
println!("There was a problem deploying.");
|
||||
std::process::exit(exit.status.code().unwrap_or(1));
|
||||
}
|
||||
let stdout: DeployStdout = serde_json::from_str(std::str::from_utf8(&exit.stdout)?)?;
|
||||
programs.push((program, stdout.program_id.parse()?));
|
||||
}
|
||||
println!("Deploy success!");
|
||||
Ok(programs)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeployStdout {
|
||||
program_id: String,
|
||||
}
|
||||
|
||||
fn set_workspace_dir_or_exit() {
|
||||
let d = match Config::discover() {
|
||||
Err(_) => {
|
||||
println!("Not in anchor workspace.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Ok(d) => d,
|
||||
};
|
||||
match d {
|
||||
None => {
|
||||
println!("Not in anchor workspace.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Some((_cfg, cfg_path, _inside_cargo)) => {
|
||||
match cfg_path.parent() {
|
||||
None => {
|
||||
println!("Unable to make new program");
|
||||
}
|
||||
Some(parent) => match std::env::set_current_dir(&parent) {
|
||||
Err(_) => {
|
||||
println!("Not in anchor workspace.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Ok(_) => {}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
use heck::CamelCase;
|
||||
use heck::SnakeCase;
|
||||
|
||||
pub fn virtual_manifest() -> String {
|
||||
r#"[workspace]
|
||||
members = [
|
||||
"programs/*"
|
||||
]
|
||||
"#
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn cargo_toml(name: &str) -> String {
|
||||
format!(
|
||||
r#"[package]
|
||||
name = "{0}"
|
||||
version = "0.1.0"
|
||||
description = "Created with Anchor"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
name = "{1}"
|
||||
|
||||
[dependencies]
|
||||
borsh = {{ git = "https://github.com/project-serum/borsh", branch = "serum", features = ["serum-program"] }}
|
||||
solana-program = "1.4.3"
|
||||
solana-sdk = {{ version = "1.3.14", default-features = false, features = ["program"] }}
|
||||
# anchor = {{ git = "https://github.com/project-serum/anchor", features = ["derive"] }}
|
||||
anchor = {{ path = "/home/armaniferrante/Documents/code/src/github.com/project-serum/anchor", features = ["derive"] }}
|
||||
"#,
|
||||
name,
|
||||
name.to_snake_case(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn xargo_toml(name: &str) -> String {
|
||||
r#"[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []"#
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn lib_rs(name: &str) -> String {
|
||||
format!(
|
||||
r#"#![feature(proc_macro_hygiene)]
|
||||
|
||||
use anchor::prelude::*;
|
||||
|
||||
#[program]
|
||||
mod {} {{
|
||||
use super::*;
|
||||
pub fn initialize(ctx: Context<Initialize>) -> ProgramResult {{
|
||||
Ok(())
|
||||
}}
|
||||
}}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct Initialize {{}}"#,
|
||||
name.to_snake_case(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn mocha(name: &str) -> String {
|
||||
format!(
|
||||
r#"const anchor = require('@project-serum/anchor');
|
||||
|
||||
describe('{}', () => {{
|
||||
|
||||
// Configure the client to use the local cluster.
|
||||
anchor.setProvider(anchor.Provider.local());
|
||||
|
||||
it('Is initialized!', async () => {{
|
||||
// Add your test here.
|
||||
const program = anchor.workspace.{};
|
||||
const tx = await program.rpc.initialize();
|
||||
console.log("Your transaction signature", tx);
|
||||
}});
|
||||
}});
|
||||
"#,
|
||||
name,
|
||||
name.to_camel_case(),
|
||||
)
|
||||
}
|
|
@ -42,13 +42,19 @@ module.exports = {
|
|||
title: "Getting Started",
|
||||
children: [
|
||||
"/getting-started/introduction",
|
||||
"/getting-started/installation",
|
||||
"/getting-started/installation",
|
||||
"/getting-started/quick-start",
|
||||
],
|
||||
},
|
||||
{
|
||||
collapsable: false,
|
||||
title: "Tutorials",
|
||||
children: ["/tutorials/tutorial-0", "/tutorials/tutorial-1"],
|
||||
children: [
|
||||
"/tutorials/tutorial-0",
|
||||
"/tutorials/tutorial-1",
|
||||
"/tutorials/tutorial-2",
|
||||
"/tutorials/tutorial-3",
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
|
|
|
@ -1,32 +1,44 @@
|
|||
# Install
|
||||
# Installing Dependencies
|
||||
|
||||
To get started, make sure to setup all the prerequisite tools on your local machine.
|
||||
To get started, make sure to setup all the prerequisite tools on your local machine
|
||||
(an installer has not yet been developed).
|
||||
|
||||
## Install Rust
|
||||
|
||||
For an introduction to Rust, see the excellent Rust [book](https://doc.rust-lang.org/book/).
|
||||
|
||||
```bash
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source $HOME/.cargo/env
|
||||
rustup component add rustfmt
|
||||
```
|
||||
|
||||
For an introduction to Rust, see the excellent Rust [book](https://doc.rust-lang.org/book/).
|
||||
## Install Solana
|
||||
|
||||
See the solana [docs](https://docs.solana.com/cli/install-solana-cli-tools) for installation instructions. On macOS and Linux,
|
||||
|
||||
```bash
|
||||
sh -c "$(curl -sSfL https://release.solana.com/v1.5.0/install)"
|
||||
```
|
||||
|
||||
## Install Mocha
|
||||
|
||||
Program integration tests are run using [Mocha](https://mochajs.org/).
|
||||
|
||||
```bash
|
||||
npm install -g mocha
|
||||
```
|
||||
|
||||
## Install Anchor
|
||||
|
||||
For now, we can use Cargo.
|
||||
For now, we can use Cargo to install the CLI.
|
||||
|
||||
```bash
|
||||
cargo install --git https://github.com/project-serum/anchor anchor-cli
|
||||
```
|
||||
|
||||
## Install Solana
|
||||
To install the JavaScript package.
|
||||
|
||||
```bash
|
||||
curl -sSf https://raw.githubusercontent.com/solana-labs/solana/v1.4.14/install/solana-install-init.sh | sh -s - v1.4.14
|
||||
export PATH="/home/ubuntu/.local/share/solana/install/active_release/bin:$PATH"
|
||||
npm install -g @project-serum/anchor
|
||||
```
|
||||
|
||||
## Setup a Localnet
|
||||
|
||||
The easiest way to run a local cluster is to run the docker container provided by Solana. Instructions can be found [here](https://solana-labs.github.io/solana-web3.js/). (Note: `solana-test-validator` is the new, preferred way to run a local validator, though I haven't tested it yet).
|
||||
|
|
|
@ -1,21 +1,17 @@
|
|||
# Introduction
|
||||
|
||||
Anchor is a framework for Solana's [Sealevel](https://medium.com/solana-labs/sealevel-parallel-processing-thousands-of-smart-contracts-d814b378192) runtime, exposing a safer and more convenient programming model to the Solana developer by providing a
|
||||
Anchor is a framework for Solana's [Sealevel](https://medium.com/solana-labs/sealevel-parallel-processing-thousands-of-smart-contracts-d814b378192) runtime, exposing a more convenient programming model by providing several different developer tools.
|
||||
|
||||
- Rust Crate for writing Solana programs
|
||||
- Rust crates and DSL for writing Solana programs
|
||||
- CLI for extracting an [IDL](https://en.wikipedia.org/wiki/Interface_description_language) from source
|
||||
- TypeScript package for generating clients from IDL
|
||||
- Workspace management for developing complete applications
|
||||
|
||||
If you're familiar with developing in Ethereum's [Solidity](https://docs.soliditylang.org/en/v0.7.4/) and [web3.js](https://github.com/ethereum/web3.js) or Parity's [Ink!](https://github.com/paritytech/ink), then the experience will be familiar. Although the DSL syntax and semantics are targeted at Solana, the high level flow of writing RPC request handlers, emitting an IDL, and generating clients from IDL is the same.
|
||||
If you're familiar with developing in Ethereum's [Solidity](https://docs.soliditylang.org/en/v0.7.4/), [Truffle](https://www.trufflesuite.com/), [web3.js](https://github.com/ethereum/web3.js) or Parity's [Ink!](https://github.com/paritytech/ink), then the experience will be familiar. Although the DSL syntax and semantics are targeted at Solana, the high level flow of writing RPC request handlers, emitting an IDL, and generating clients from IDL is the same.
|
||||
|
||||
Here, we'll walkthrough a tutorial demonstrating how to use Anchor. To skip the tutorial and jump straight to a full example, go [here](https://github.com/project-serum/anchor/tree/master/examples/basic/src/lib.rs).
|
||||
Here, we'll walkthrough several tutorials demonstrating how to use Anchor. To skip the tutorials and jump straight to a full example, checkout the
|
||||
[quickstart](./quick-start.md) or go [here](https://github.com/project-serum/anchor/tree/master/examples/basic/src/lib.rs). For an introduction to Solana, see the [docs](https://docs.solana.com/developing/programming-model/overview).
|
||||
|
||||
## Contributing
|
||||
|
||||
It would be great to have clients generated for languages other than TypeScript. If you're
|
||||
interested in developing a client generator, feel free to reach out, or go ahead and just
|
||||
do it :P.
|
||||
|
||||
## Note
|
||||
|
||||
Anchor is in active development, so all APIs are subject to change. If you have feedback, please reach out by [filing an issue](https://github.com/project-serum/anchor/issues/new). This documentation is a work in progress and is expected to change dramatically as features continue to be built out. If you have any problems, consult the [source](https://github.com/project-serum/anchor) or feel free to ask questions on the [Serum Discord](https://discord.com/channels/739225212658122886/752530209848295555).
|
||||
::: tip NOTE
|
||||
Anchor is in active development, so all APIs are subject to change. If you are one of the early developers to try it out and have feedback, please reach out by [filing an issue](https://github.com/project-serum/anchor/issues/new). This documentation is a work in progress and is expected to change dramatically as features continue to be built out. If you have any problems, consult the [source](https://github.com/project-serum/anchor) or feel free to ask questions on the [Serum Discord](https://discord.com/channels/739225212658122886/752530209848295555).
|
||||
:::
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
# Quick Start
|
||||
|
||||
The quick start provides a whirlwind tour through creating, deploying, and testing a project
|
||||
using Anchor, targeted at developers who are familiar with blockchain development. For an in depth
|
||||
guide of Anchor from the ground up, see the subequent tutorials.
|
||||
|
||||
## Initialize a project
|
||||
|
||||
Anchor follows the principle of "Convention is better than configuration".
|
||||
To initialize your project workspace, run
|
||||
|
||||
```bash
|
||||
anchor init my-project && cd my-project
|
||||
```
|
||||
|
||||
Your repo will be laid out with the following structure
|
||||
|
||||
* `Anchor.toml`: Anchor configuration file.
|
||||
* `programs/`: Directory for Solana program crates.
|
||||
* `app/`: Directory for your application frontend.
|
||||
* `tests/`: Directory for TypeScript integration tests.
|
||||
|
||||
## Build
|
||||
|
||||
To build your program targeting Solana's BPF runtime and emit an IDL that can be
|
||||
consumed by clients, run
|
||||
|
||||
```bash
|
||||
anchor build
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
It's [recommended](https://www.parity.io/paritys-checklist-for-secure-smart-contract-development/)
|
||||
to test your program using integration tests in a language other
|
||||
than Rust to make sure that bugs related to syntax misunderstandings
|
||||
are coverable with tests and not just replicated in tests.
|
||||
|
||||
```
|
||||
anchor test
|
||||
```
|
||||
|
||||
You just built a program, deployed it to a local network, and
|
||||
ran integration tests in one command. It's that easy. ;)
|
||||
|
||||
## Deploy
|
||||
|
||||
To deploy all programs in your workspace, run
|
||||
|
||||
```
|
||||
anchor deploy
|
||||
```
|
|
@ -4,7 +4,7 @@ Here, we introduce a minimal example demonstrating the Anchor workflow and core
|
|||
elements. This tutorial assumes all [prerequisites](./prerequisites.md) are installed and
|
||||
a local network is running.
|
||||
|
||||
## Clone the repo
|
||||
## Clone the Repo
|
||||
|
||||
To get started, clone the repo.
|
||||
|
||||
|
@ -12,71 +12,52 @@ To get started, clone the repo.
|
|||
git clone https://github.com/project-serum/anchor
|
||||
```
|
||||
|
||||
And change directories to the [example](https://github.com/project-serum/anchor/tree/master/examples/basic-0).
|
||||
And change directories to the [example](https://github.com/project-serum/anchor/tree/master/examples/tutorial/basic-0).
|
||||
|
||||
```bash
|
||||
cd anchor/examples/tutorial/basic-0
|
||||
```
|
||||
|
||||
## Defining a program
|
||||
## Defining a Program
|
||||
|
||||
We define the minimum viable program as follows.
|
||||
|
||||
<<< @/../examples/tutorial/basic-0/program/src/lib.rs
|
||||
|
||||
There are a couple of syntax elements to point out here.
|
||||
|
||||
### `#[program]`
|
||||
|
||||
First, notice that a program is defined with the `#[program]` attribute, where each
|
||||
* `#[program]` First, notice that a program is defined with the `#[program]` attribute, where each
|
||||
inner method defines an RPC request handler, or, in Solana parlance, an "instruction"
|
||||
handler. These handlers are the entrypoints to your program that clients may invoke, as
|
||||
we will see soon.
|
||||
|
||||
### `Context<Initialize>`
|
||||
|
||||
The first parameter of _every_ RPC handler is the `Context` struct, which is a simple
|
||||
* `Context<Initialize>` The first parameter of _every_ RPC handler is the `Context` struct, which is a simple
|
||||
container for the currently executing `program_id` generic over
|
||||
`Accounts`--here, the `Initialize` struct.
|
||||
|
||||
### `#[derive(Accounts)]`
|
||||
|
||||
The `Accounts` derive macro marks a struct containing all the accounts that must be
|
||||
* `#[derive(Accounts)]` The `Accounts` derive macro marks a struct containing all the accounts that must be
|
||||
specified for a given instruction. To understand Accounts on Solana, see the
|
||||
[docs](https://docs.solana.com/developing/programming-model/accounts).
|
||||
In subsequent tutorials, we'll demonstrate how an `Accounts` struct can be used to
|
||||
specify constraints on accounts given to your program. Since this example doesn't touch any
|
||||
accounts, we skip this (important) detail.
|
||||
|
||||
## Building a program
|
||||
## Building and Emitting an IDL
|
||||
|
||||
This program can be built in same way as any other Solana program.
|
||||
|
||||
```bash
|
||||
cargo build-bpf
|
||||
```
|
||||
|
||||
## Deploying a program
|
||||
|
||||
Similarly, we can deploy the program using the `solana deploy` command.
|
||||
|
||||
```bash
|
||||
solana deploy <path-to-your-repo>/anchor/target/deploy/basic_program_0.so
|
||||
```
|
||||
|
||||
Making sure to susbstitute paths to match your local filesystem. Now, save the address
|
||||
the program was deployed with. It will be useful later.
|
||||
|
||||
## Emmiting an IDL
|
||||
|
||||
After creating a program, one can use the Anchor CLI to emit an IDL, from which clients
|
||||
After creating a program, one can use the `anchor` CLI to build and emit an IDL, from which clients
|
||||
can be generated.
|
||||
|
||||
```bash
|
||||
anchor idl -f src/lib.rs -o idl.js
|
||||
anchor build
|
||||
```
|
||||
|
||||
Inspecting the contents of `idl.js` one should see
|
||||
::: details
|
||||
The `build` command is a convenience combining two steps.
|
||||
|
||||
1) `cargo build-bpf`
|
||||
2) `anchor idl -f src/lib.rs -o basic.json`.
|
||||
:::
|
||||
|
||||
Once run, you should see your build artifacts, as usual, in your `target/` directory. Additionally,
|
||||
a `basic.json` file is created. Inspecting its contents you should see
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -92,7 +73,23 @@ Inspecting the contents of `idl.js` one should see
|
|||
}
|
||||
```
|
||||
|
||||
For experienced Ethereum developers, this is analogous to an `abi.json` file.
|
||||
From which a client can be generated. Note that this file is created by parsing the `src/lib.rs`
|
||||
file in your program's crate.
|
||||
|
||||
::: tip
|
||||
If you've developed on Ethereum, the IDL is analogous to the `abi.json`.
|
||||
:::
|
||||
|
||||
## Deploying a program
|
||||
|
||||
Once built, we can deploy the program using the `solana deploy` command.
|
||||
|
||||
```bash
|
||||
solana deploy <path-to-your-repo>/anchor/target/deploy/basic_program_0.so
|
||||
```
|
||||
|
||||
Making sure to susbstitute paths to match your local filesystem. Now, save the address
|
||||
the program was deployed with. It will be useful later.
|
||||
|
||||
## Generating a Client
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
# Tutorial 2: Constraints and Access Control
|
|
@ -0,0 +1 @@
|
|||
# Tutorial 3: Workspaces
|
|
@ -1,5 +1,3 @@
|
|||
// TODO: replace path once the package is published.
|
||||
//
|
||||
// Before running this script, make sure to run `yarn && yarn build` inside
|
||||
// the `ts` directory.
|
||||
const anchor = require('../../../../ts');
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
// Before running this script, make sure to run `yarn && yarn build` inside
|
||||
// the `ts` directory.
|
||||
const anchor = require('../../../../ts');
|
||||
const fs = require('fs');
|
||||
|
||||
// Configure the local cluster.
|
||||
anchor.setProvider(anchor.Provider.local());
|
||||
|
||||
// #region main
|
||||
async function main() {
|
||||
// Read the generated IDL.
|
||||
const idl = JSON.parse(fs.readFileSync('../idl.json', 'utf8'));
|
||||
|
||||
// Address of the deployed program.
|
||||
const programId = new anchor.web3.PublicKey('<YOUR-PROGRAM-ID>');
|
||||
|
||||
// Generate the program client from IDL.
|
||||
const program = new anchor.Program(idl, programId);
|
||||
|
||||
// Execute the RPC.
|
||||
await program.rpc.initialize();
|
||||
}
|
||||
// #endregion main
|
||||
|
||||
main();
|
|
@ -15,3 +15,4 @@ syn = { version = "1.0.54", features = ["full", "extra-traits", "parsing"] }
|
|||
anyhow = "1.0.32"
|
||||
heck = "0.3.1"
|
||||
serde = { version = "1.0.118", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
|
|
@ -9,6 +9,8 @@ pub struct Idl {
|
|||
pub accounts: Vec<IdlTypeDef>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||||
pub types: Vec<IdlTypeDef>,
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
|
|
@ -2,7 +2,6 @@ use crate::{
|
|||
AccountsStruct, Constraint, ConstraintBelongsTo, ConstraintLiteral, ConstraintOwner,
|
||||
ConstraintSigner, Field, ProgramAccountTy, Ty,
|
||||
};
|
||||
use quote::quote;
|
||||
|
||||
pub fn parse(strct: &syn::ItemStruct) -> AccountsStruct {
|
||||
let fields = match &strct.fields {
|
||||
|
|
|
@ -3,17 +3,17 @@ use crate::parser::anchor;
|
|||
use crate::parser::program;
|
||||
use crate::AccountsStruct;
|
||||
use anyhow::Result;
|
||||
use heck::MixedCase;
|
||||
use quote::ToTokens;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use heck::MixedCase;
|
||||
use heck::CamelCase;
|
||||
use std::path::Path;
|
||||
|
||||
static DERIVE_NAME: &'static str = "Accounts";
|
||||
|
||||
// Parse an entire interface file.
|
||||
pub fn parse(filename: &str) -> Result<Idl> {
|
||||
pub fn parse(filename: impl AsRef<Path>) -> Result<Idl> {
|
||||
let mut file = File::open(&filename)?;
|
||||
|
||||
let mut src = String::new();
|
||||
|
@ -91,6 +91,7 @@ pub fn parse(filename: &str) -> Result<Idl> {
|
|||
instructions,
|
||||
types,
|
||||
accounts,
|
||||
metadata: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -164,12 +165,7 @@ fn parse_ty_defs(f: &syn::File) -> Result<Vec<IdlTypeDef>> {
|
|||
let mut tts = proc_macro2::TokenStream::new();
|
||||
f.ty.to_tokens(&mut tts);
|
||||
Ok(IdlField {
|
||||
name: f
|
||||
.ident
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.to_mixed_case(),
|
||||
name: f.ident.as_ref().unwrap().to_string().to_mixed_case(),
|
||||
ty: tts.to_string().parse()?,
|
||||
})
|
||||
})
|
||||
|
|
|
@ -28,7 +28,8 @@
|
|||
"@types/bn.js": "^4.11.6",
|
||||
"bn.js": "^5.1.2",
|
||||
"buffer-layout": "^1.2.0",
|
||||
"camelcase": "^5.3.1"
|
||||
"camelcase": "^5.3.1",
|
||||
"find": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^8.2.0",
|
||||
|
|
|
@ -3,6 +3,7 @@ import * as web3 from "@solana/web3.js";
|
|||
import { Provider } from "@project-serum/common";
|
||||
import { Program } from "./program";
|
||||
import Coder from "./coder";
|
||||
import workspace from './workspace';
|
||||
|
||||
let _provider: Provider | null = null;
|
||||
|
||||
|
@ -14,4 +15,4 @@ function getProvider(): Provider {
|
|||
return _provider;
|
||||
}
|
||||
|
||||
export { Program, Coder, setProvider, getProvider, Provider, BN, web3 };
|
||||
export { workspace, Program, Coder, setProvider, getProvider, Provider, BN, web3 };
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import camelCase from "camelcase";
|
||||
import { Program } from './program';
|
||||
|
||||
let _populatedWorkspace = false;
|
||||
|
||||
// Workspace program discovery only works for node environments.
|
||||
export default new Proxy({} as any, {
|
||||
get(
|
||||
workspaceCache: { [key: string]: Program },
|
||||
programName: string
|
||||
) {
|
||||
const find = require('find');
|
||||
const fs = require('fs');
|
||||
const process = require('process');
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
throw new Error(
|
||||
'`anchor.workspace` is not available in the browser'
|
||||
);
|
||||
}
|
||||
|
||||
if (!_populatedWorkspace) {
|
||||
const path = require('path');
|
||||
|
||||
let projectRoot = process.cwd();
|
||||
while (!fs.existsSync(path.join(projectRoot, 'Anchor.toml'))) {
|
||||
const parentDir = path.dirname(projectRoot);
|
||||
if (parentDir === projectRoot) {
|
||||
projectRoot = undefined;
|
||||
}
|
||||
projectRoot = parentDir;
|
||||
}
|
||||
|
||||
|
||||
if (projectRoot === undefined) {
|
||||
throw new Error(
|
||||
'Could not find workspace root. Perhaps set the `OASIS_WORKSPACE` env var?'
|
||||
);
|
||||
}
|
||||
|
||||
find
|
||||
.fileSync(/target\/idl\/.*\.json/, projectRoot)
|
||||
.reduce((programs: any, path: string) => {
|
||||
const idlStr = fs.readFileSync(path);
|
||||
const idl = JSON.parse(idlStr);
|
||||
const name = camelCase(idl.name, { pascalCase: true });
|
||||
programs[name] = new Program(idl, idl.metadata.address);
|
||||
return programs;
|
||||
}, workspaceCache);
|
||||
|
||||
_populatedWorkspace = true;
|
||||
}
|
||||
|
||||
return workspaceCache[programName];
|
||||
},
|
||||
}
|
||||
);
|
|
@ -10,6 +10,10 @@ const WORKSPACE = {
|
|||
};
|
||||
|
||||
describe('Constraints program tests', () => {
|
||||
it('Parses a workspace', async () => {
|
||||
|
||||
});
|
||||
|
||||
it('Runs against a localnetwork', async () => {
|
||||
// Configure the local cluster.
|
||||
anchor.setProvider(WORKSPACE.provider);
|
||||
|
|
12
ts/yarn.lock
12
ts/yarn.lock
|
@ -2515,6 +2515,13 @@ find-versions@^3.2.0:
|
|||
dependencies:
|
||||
semver-regex "^2.0.0"
|
||||
|
||||
find@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/find/-/find-0.3.0.tgz#4082e8fc8d8320f1a382b5e4f521b9bc50775cb8"
|
||||
integrity sha512-iSd+O4OEYV/I36Zl8MdYJO0xD82wH528SaCieTVHhclgiYNe9y+yPKSwK+A7/WsmHL1EZ+pYUJBXWTL5qofksw==
|
||||
dependencies:
|
||||
traverse-chain "~0.1.0"
|
||||
|
||||
flat-cache@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
|
||||
|
@ -5667,6 +5674,11 @@ tr46@^2.0.2:
|
|||
dependencies:
|
||||
punycode "^2.1.1"
|
||||
|
||||
traverse-chain@~0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/traverse-chain/-/traverse-chain-0.1.0.tgz#61dbc2d53b69ff6091a12a168fd7d433107e40f1"
|
||||
integrity sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE=
|
||||
|
||||
trim-newlines@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20"
|
||||
|
|
Loading…
Reference in New Issue