From ff2fbed445775dfff1d1c2e51faddbd6230e2561 Mon Sep 17 00:00:00 2001 From: obasi Bethel Date: Thu, 28 May 2026 02:53:55 +0100 Subject: [PATCH] Fix duplicate fields and add dApp flags --- src/commands/new.rs | 372 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 330 insertions(+), 42 deletions(-) diff --git a/src/commands/new.rs b/src/commands/new.rs index 05c88306..e44debee 100644 --- a/src/commands/new.rs +++ b/src/commands/new.rs @@ -1,6 +1,6 @@ use crate::utils::print as p; use crate::utils::templates; -use anyhow::Result; +use anyhow::{Context, Result}; use clap::Subcommand; use colored::*; use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select}; @@ -18,19 +18,13 @@ pub enum NewCommands { /// Contract template #[arg(long, default_value = "hello-world")] template: String, - /// Template source label (example: marketplace) - #[arg(long)] - from: Option, - /// Search available templates - #[arg(long)] - search: Option, /// Interactively customize the generated contract #[arg(long)] interactive: bool, - /// Use a template from the marketplace + /// Template source label (example: marketplace) #[arg(long)] from: Option, - /// Search for templates in the marketplace + /// Search available templates #[arg(long)] search: Option, /// Filter templates by tags (comma-separated) @@ -41,16 +35,34 @@ pub enum NewCommands { Dapp { /// Project name name: String, + /// Generate TypeScript sources (tsx) instead of JavaScript (jsx) + #[arg(long)] + typescript: bool, + /// Include Stellar Wallets Kit integration scaffolding + #[arg(long)] + wallet_kit: bool, }, } pub fn handle(cmd: NewCommands) -> Result<()> { match cmd { - NewCommands::Contract { name, template, from, search, interactive } => { + NewCommands::Contract { + name, + template, + from, + search, + tags, + interactive, + } => { if let Some(query) = search { - return search_templates(&query); + return handle_template_search(&query, tags.as_deref()); } let name = name.ok_or_else(|| anyhow::anyhow!("A contract name is required unless --search is used"))?; + + if !interactive && from.as_deref() == Some("marketplace") { + return scaffold_from_marketplace(name, template); + } + if interactive { scaffold_contract_interactive(name) } else { @@ -65,12 +77,16 @@ pub fn handle(cmd: NewCommands) -> Result<()> { ) } } - NewCommands::Dapp { name } => scaffold_dapp(name), + NewCommands::Dapp { + name, + typescript, + wallet_kit, + } => scaffold_dapp(name, typescript, wallet_kit), } } fn search_templates(query: &str) -> Result<()> { - let results = templates::search_templates(query)?; + let results = templates::search_templates(query, None)?; p::header(&format!("Template search results for '{}'", query)); if results.is_empty() { p::info("No templates matched that query."); @@ -240,27 +256,66 @@ fn scaffold_contract( Ok(()) } -fn scaffold_dapp(name: String) -> Result<()> { +fn scaffold_dapp(name: String, typescript: bool, wallet_kit: bool) -> Result<()> { let dir = Path::new(&name); if dir.exists() { anyhow::bail!("Directory '{}' already exists", name); } p::header(&format!("Scaffolding Stellar dApp: {}", name)); + if typescript { + p::kv("TypeScript", "enabled"); + } + if wallet_kit { + p::kv("Stellar Wallets Kit", "enabled"); + } + println!(); - p::step(1, 3, "Creating project structure…"); + let ext = if typescript { "tsx" } else { "jsx" }; + let total_steps = if typescript { 4 } else { 3 }; + + p::step(1, total_steps, "Creating project structure…"); fs::create_dir_all(dir.join("src/components"))?; fs::create_dir_all(dir.join("public"))?; - p::step(2, 3, "Writing package.json…"); - fs::write(dir.join("package.json"), dapp_package(&name))?; + p::step(2, total_steps, "Writing package.json…"); + fs::write( + dir.join("package.json"), + dapp_package(&name, typescript, wallet_kit), + )?; + + let mut step = 3; + if typescript { + p::step(step, total_steps, "Writing TypeScript config…"); + fs::write(dir.join("tsconfig.json"), dapp_tsconfig())?; + fs::write(dir.join("tsconfig.node.json"), dapp_tsconfig_node())?; + fs::write( + dir.join(format!("src/vite-env.d.ts")), + dapp_vite_env_types(wallet_kit), + )?; + step += 1; + } - p::step(3, 3, "Writing app scaffold…"); - fs::write(dir.join("index.html"), dapp_index(&name))?; - fs::write(dir.join("src/main.jsx"), dapp_main())?; - fs::write(dir.join("src/App.jsx"), dapp_app(&name))?; - fs::write(dir.join(".gitignore"), "node_modules/\ndist/\n")?; - fs::write(dir.join("README.md"), dapp_readme(&name))?; + p::step(step, total_steps, "Writing app scaffold…"); + fs::write(dir.join("index.html"), dapp_index(&name, ext))?; + fs::write( + dir.join(format!("src/main.{ext}")), + dapp_main(typescript, wallet_kit), + )?; + fs::write( + dir.join(format!("src/App.{ext}")), + dapp_app(&name, typescript, wallet_kit), + )?; + fs::write( + dir.join(if typescript { + "vite.config.ts" + } else { + "vite.config.js" + }), + dapp_vite_config(typescript, wallet_kit), + )?; + fs::write(dir.join(".gitignore"), "node_modules/\ndist/\n.env.local\n")?; + fs::write(dir.join("README.md"), dapp_readme(&name, typescript, wallet_kit))?; println!(); p::success(&format!("dApp '{}' scaffolded!", name)); @@ -743,8 +798,36 @@ mod test {{ // ── dApp scaffold files ─────────────────────────────────────────────────────── -fn dapp_package(name: &str) -> String { - format!(r#"{{ +/// Default Stellar testnet settings (aligned with starforge `~/.starforge/config.toml`). +const DAPP_TESTNET_ENV: &[(&str, &str)] = &[ + ("VITE_STELLAR_NETWORK", "testnet"), + ( + "VITE_STELLAR_NETWORK_PASSPHRASE", + "Test SDF Network ; September 2015", + ), + ("VITE_HORIZON_URL", "https://horizon-testnet.stellar.org"), + ("VITE_SOROBAN_RPC_URL", "https://soroban-testnet.stellar.org"), +]; + +fn dapp_package(name: &str, typescript: bool, wallet_kit: bool) -> String { + let env_block = dapp_package_env_block(); + let ts_deps = if typescript { + r#", + "typescript": "^5.6.0", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0""# + } else { + "" + }; + let wallet_deps = if wallet_kit { + r#", + "@creit.tech/stellar-wallets-kit": "^2.2.0""# + } else { + "" + }; + + format!( + r#"{{ "name": "{name}", "version": "0.1.0", "type": "module", @@ -753,21 +836,35 @@ fn dapp_package(name: &str) -> String { "build": "vite build", "preview": "vite preview" }}, + "env": {{ +{env_block} + }}, "dependencies": {{ "@stellar/stellar-sdk": "^12.3.0", "react": "^18.3.0", - "react-dom": "^18.3.0" + "react-dom": "^18.3.0"{wallet_deps} }}, "devDependencies": {{ "@vitejs/plugin-react": "^4.3.1", - "vite": "^5.4.0" + "vite": "^5.4.0"{ts_deps} }} }} -"#) +"#, + env_block = env_block, + ) +} + +fn dapp_package_env_block() -> String { + DAPP_TESTNET_ENV + .iter() + .map(|(key, value)| format!(r#" "{key}": "{value}""#)) + .collect::>() + .join(",\n") } -fn dapp_index(name: &str) -> String { - format!(r#" +fn dapp_index(name: &str, main_ext: &str) -> String { + format!( + r#" @@ -776,49 +873,240 @@ fn dapp_index(name: &str) -> String {
- + -"#) +"# + ) } -fn dapp_main() -> &'static str { - r#"import React from 'react' +fn dapp_main(typescript: bool, wallet_kit: bool) -> String { + let app_import = if typescript { "./App.tsx" } else { "./App.jsx" }; + let root_el = if typescript { + "document.getElementById('root')!" + } else { + "document.getElementById('root')" + }; + + let mut out = format!( + r#"import React from 'react' import ReactDOM from 'react-dom/client' -import App from './App.jsx' +import App from '{app_import}' +"# + ); -ReactDOM.createRoot(document.getElementById('root')).render( + if wallet_kit { + out.push_str( + r#"import { StellarWalletsKit } from '@creit.tech/stellar-wallets-kit/sdk' +import { defaultModules } from '@creit.tech/stellar-wallets-kit/modules/utils' +import { Networks } from '@stellar/stellar-sdk' + +StellarWalletsKit.init({ modules: defaultModules() }) +StellarWalletsKit.setNetwork(Networks.TESTNET) + +"#, + ); + } + + out.push_str(&format!( + r#"ReactDOM.createRoot({root_el}).render( ) "# + )); + + out } -fn dapp_app(name: &str) -> String { - format!(r#"import React from 'react' +fn dapp_app(name: &str, typescript: bool, wallet_kit: bool) -> String { + let network_expr = if typescript { + "{import.meta.env.VITE_STELLAR_NETWORK ?? 'testnet'}" + } else { + "{import.meta.env.VITE_STELLAR_NETWORK || 'testnet'}" + }; + + if wallet_kit { + let state_hook = if typescript { + "const [address, setAddress] = React.useState(null)" + } else { + "const [address, setAddress] = React.useState(null)" + }; + + return format!( + r#"import React from 'react' +import {{ StellarWalletsKit }} from '@creit.tech/stellar-wallets-kit/sdk' + +export default function App() {{ + {state_hook} + + const connectWallet = async () => {{ + const {{ address }} = await StellarWalletsKit.getAddress() + setAddress(address) + }} + + return ( +
+

⚡ {name}

+

Network: {network_expr}

+ + {{address &&

Connected: {{address}}

}} +
+ ) +}} +"#, + network_expr = network_expr, + ); + } + + format!( + r#"import React from 'react' export default function App() {{ return (

⚡ {name}

Your Stellar dApp is ready. Start building!

+

Network: {network_expr}

) }} -"#) +"#, + network_expr = network_expr, + ) } -fn dapp_readme(name: &str) -> String { - format!(r#"# {name} +fn dapp_vite_define_block() -> String { + DAPP_TESTNET_ENV + .iter() + .map(|(key, value)| { + format!( + " 'import.meta.env.{key}': JSON.stringify('{value}')," + ) + }) + .collect::>() + .join("\n") +} + +fn dapp_vite_config(_typescript: bool, wallet_kit: bool) -> String { + let define_block = dapp_vite_define_block(); + let wallet_ssr = if wallet_kit { + r#" + ssr: { + noExternal: [ + '@creit.tech/stellar-wallets-kit', + '@stellar/freighter-api', + '@lobstrco/signer-extension-api', + ], + },"# + } else { + "" + }; + + format!( + r#"import {{ defineConfig }} from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({{ + plugins: [react()], + define: {{ +{define_block} + }},{wallet_ssr} +}}) +"# + ) +} + +fn dapp_tsconfig() -> &'static str { + r#"{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} +"# +} + +fn dapp_tsconfig_node() -> &'static str { + r#"{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler" + }, + "include": ["vite.config.ts"] +} +"# +} + +fn dapp_vite_env_types(_wallet_kit: bool) -> String { + r#"/// + +interface ImportMetaEnv { + readonly VITE_STELLAR_NETWORK: string + readonly VITE_STELLAR_NETWORK_PASSPHRASE: string + readonly VITE_HORIZON_URL: string + readonly VITE_SOROBAN_RPC_URL: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} +"# + .to_string() +} + +fn dapp_readme(name: &str, typescript: bool, wallet_kit: bool) -> String { + let flags = { + let mut parts = Vec::new(); + if typescript { + parts.push("TypeScript"); + } + if wallet_kit { + parts.push("Stellar Wallets Kit"); + } + if parts.is_empty() { + String::new() + } else { + format!("\n\nScaffold options: {}.", parts.join(", ")) + } + }; + + format!( + r#"# {name} A Stellar dApp scaffolded with [starforge](https://github.com/YOUR_USERNAME/starforge). +Testnet settings are defined in `package.json` under the `env` key and exposed to Vite via `vite.config.{ext}`. +{flags} + ## Getting Started ```bash npm install npm run dev ``` -"#) +"#, + ext = if typescript { "ts" } else { "js" }, + flags = flags, + ) } fn readme(name: &str, template: &str, source: &str) -> String {