diff --git a/.gitignore b/.gitignore index 0e84c52e5..830f14b75 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ target *.swp .pnpm-store +# Claude Code session files +.claude/ + #All generated code for scenarios scenarios/**/*.bs.js scenarios/**/*.res.js diff --git a/codegenerator/Cargo.lock b/codegenerator/Cargo.lock index bfda71d38..4c31e914c 100644 --- a/codegenerator/Cargo.lock +++ b/codegenerator/Cargo.lock @@ -1004,6 +1004,7 @@ dependencies = [ "strum_macros", "subenum", "tempdir", + "tempfile", "thiserror", "tokio", "tracing-subscriber", diff --git a/codegenerator/cli/Cargo.toml b/codegenerator/cli/Cargo.toml index b94e96c03..6ea0521e7 100644 --- a/codegenerator/cli/Cargo.toml +++ b/codegenerator/cli/Cargo.toml @@ -47,6 +47,7 @@ dotenvy = { git = "https://github.com/enviodev/dotenvy", rev = "e2da110668572cf2 [dev-dependencies] tempdir = "0.3" +tempfile = "3.14" paste = "1.0.12" tracing-subscriber = "0.3.17" pretty_assertions = "1.4.0" diff --git a/codegenerator/cli/CommandLineHelp.md b/codegenerator/cli/CommandLineHelp.md index 7ec235685..a13eb231b 100644 --- a/codegenerator/cli/CommandLineHelp.md +++ b/codegenerator/cli/CommandLineHelp.md @@ -77,6 +77,10 @@ Initialize an indexer with one of the initialization options Possible values: `typescript`, `rescript` * `--api-token ` — The hypersync API key to be initialized in your templates .env file +* `-p`, `--package-manager ` — The package manager to use (npm, yarn, pnpm, bun). Auto-detected from lockfiles if not specified + + Possible values: `npm`, `yarn`, `pnpm`, `bun` + diff --git a/codegenerator/cli/npm/envio/evm.schema.json b/codegenerator/cli/npm/envio/evm.schema.json index ffdd7d2fd..963e2e3a4 100644 --- a/codegenerator/cli/npm/envio/evm.schema.json +++ b/codegenerator/cli/npm/envio/evm.schema.json @@ -45,6 +45,17 @@ "format": "uint64", "minimum": 0 }, + "package_manager": { + "description": "Package manager to use: npm, yarn, pnpm, or bun. If not specified, auto-detected from lockfiles in the project directory.", + "anyOf": [ + { + "$ref": "#/$defs/PackageManager" + }, + { + "type": "null" + } + ] + }, "ecosystem": { "description": "Ecosystem of the project.", "anyOf": [ @@ -145,6 +156,16 @@ "chains" ], "$defs": { + "PackageManager": { + "description": "Supported package managers", + "type": "string", + "enum": [ + "npm", + "yarn", + "pnpm", + "bun" + ] + }, "EcosystemTag": { "type": "string", "enum": [ diff --git a/codegenerator/cli/npm/envio/fuel.schema.json b/codegenerator/cli/npm/envio/fuel.schema.json index 4c826e4b7..7c7893b8b 100644 --- a/codegenerator/cli/npm/envio/fuel.schema.json +++ b/codegenerator/cli/npm/envio/fuel.schema.json @@ -45,6 +45,17 @@ "format": "uint64", "minimum": 0 }, + "package_manager": { + "description": "Package manager to use: npm, yarn, pnpm, or bun. If not specified, auto-detected from lockfiles in the project directory.", + "anyOf": [ + { + "$ref": "#/$defs/PackageManager" + }, + { + "type": "null" + } + ] + }, "ecosystem": { "description": "Ecosystem of the project.", "$ref": "#/$defs/EcosystemTag" @@ -81,6 +92,16 @@ "chains" ], "$defs": { + "PackageManager": { + "description": "Supported package managers", + "type": "string", + "enum": [ + "npm", + "yarn", + "pnpm", + "bun" + ] + }, "EcosystemTag": { "type": "string", "enum": [ diff --git a/codegenerator/cli/npm/envio/svm.schema.json b/codegenerator/cli/npm/envio/svm.schema.json index 500982cfe..e488013d9 100644 --- a/codegenerator/cli/npm/envio/svm.schema.json +++ b/codegenerator/cli/npm/envio/svm.schema.json @@ -45,6 +45,17 @@ "format": "uint64", "minimum": 0 }, + "package_manager": { + "description": "Package manager to use: npm, yarn, pnpm, or bun. If not specified, auto-detected from lockfiles in the project directory.", + "anyOf": [ + { + "$ref": "#/$defs/PackageManager" + }, + { + "type": "null" + } + ] + }, "ecosystem": { "description": "Ecosystem of the project.", "$ref": "#/$defs/EcosystemTag" @@ -64,6 +75,16 @@ "chains" ], "$defs": { + "PackageManager": { + "description": "Supported package managers", + "type": "string", + "enum": [ + "npm", + "yarn", + "pnpm", + "bun" + ] + }, "EcosystemTag": { "type": "string", "enum": [ @@ -101,4 +122,3 @@ } } } - diff --git a/codegenerator/cli/src/cli_args/clap_definitions.rs b/codegenerator/cli/src/cli_args/clap_definitions.rs index 52ed83182..b54999dbe 100644 --- a/codegenerator/cli/src/cli_args/clap_definitions.rs +++ b/codegenerator/cli/src/cli_args/clap_definitions.rs @@ -1,4 +1,5 @@ use crate::constants::project_paths::{DEFAULT_CONFIG_PATH, DEFAULT_GENERATED_PATH}; +use crate::package_manager::PackageManager; use clap::{Args, Parser, Subcommand}; use clap_markdown::MarkdownOptions; @@ -145,6 +146,11 @@ pub struct InitArgs { ///The hypersync API key to be initialized in your templates .env file #[arg(global = true, long)] pub api_token: Option, + + ///The package manager to use (npm, yarn, pnpm, bun). Auto-detected from lockfiles if not specified. + #[arg(global = true, short = 'p', long = "package-manager")] + #[clap(value_enum)] + pub package_manager: Option, } #[subenum(EvmInitFlowInteractive)] diff --git a/codegenerator/cli/src/cli_args/init_config.rs b/codegenerator/cli/src/cli_args/init_config.rs index 5673f59a8..ec5a3c281 100644 --- a/codegenerator/cli/src/cli_args/init_config.rs +++ b/codegenerator/cli/src/cli_args/init_config.rs @@ -164,6 +164,7 @@ pub mod evm { output: None, handlers: None, full_batch_size: None, + package_manager: init_config.package_manager, }, ecosystem: None, contracts, @@ -306,6 +307,7 @@ pub mod fuel { output: None, handlers: None, full_batch_size: None, + package_manager: init_config.package_manager, }, ecosystem: EcosystemTag::Fuel, contracts: None, @@ -374,4 +376,5 @@ pub struct InitConfig { pub ecosystem: Ecosystem, pub language: Language, pub api_token: Option, + pub package_manager: Option, } diff --git a/codegenerator/cli/src/cli_args/interactive_init/mod.rs b/codegenerator/cli/src/cli_args/interactive_init/mod.rs index 89479b4c2..ff5c45c5f 100644 --- a/codegenerator/cli/src/cli_args/interactive_init/mod.rs +++ b/codegenerator/cli/src/cli_args/interactive_init/mod.rs @@ -189,5 +189,6 @@ pub async fn prompt_missing_init_args( ecosystem, language, api_token, + package_manager: init_args.package_manager, }) } diff --git a/codegenerator/cli/src/commands.rs b/codegenerator/cli/src/commands.rs index a8dfb7a0f..fa62c3290 100644 --- a/codegenerator/cli/src/commands.rs +++ b/codegenerator/cli/src/commands.rs @@ -29,21 +29,36 @@ async fn execute_command( )) } +/// Execute a command with String arguments (needed for dynamic package manager commands) +async fn execute_command_string_args( + cmd: &str, + args: Vec, + current_dir: &Path, +) -> anyhow::Result { + let args_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); + execute_command(cmd, args_refs, current_dir).await +} + pub mod rescript { - use super::execute_command; + use super::execute_command_string_args; + use crate::package_manager::PackageManagerConfig; use anyhow::Result; use std::path::Path; - pub async fn build(path: &Path) -> Result { - let args = vec!["rescript"]; - execute_command("pnpm", args, path).await + pub async fn build( + path: &Path, + pm_config: &PackageManagerConfig, + ) -> Result { + let pm = &pm_config.package_manager; + execute_command_string_args(pm.command(), pm.run_script_args("rescript"), path).await } } pub mod codegen { use super::{execute_command, rescript}; use crate::{ - config_parsing::system_config::SystemConfig, hbs_templating, template_dirs::TemplateDirs, + config_parsing::system_config::SystemConfig, hbs_templating, + package_manager::PackageManagerConfig, template_dirs::TemplateDirs, }; use anyhow::{self, Context, Result}; use std::path::Path; @@ -71,53 +86,139 @@ pub mod codegen { Ok(()) } - pub async fn check_and_install_pnpm(current_dir: &Path) -> Result<()> { - // Check if pnpm is already installed - let check_pnpm = execute_command("pnpm", vec!["--version"], current_dir).await; + /// Check if the package manager is available, and auto-install if supported + pub async fn check_package_manager( + pm_config: &PackageManagerConfig, + current_dir: &Path, + ) -> Result<()> { + let pm = &pm_config.package_manager; + let check = execute_command(pm.command(), vec!["--version"], current_dir).await; - // If pnpm is not installed, run the installation command - match check_pnpm { + match check { Ok(status) if status.success() => { - println!("Package pnpm is already installed. Continuing..."); + println!("{} is available. Continuing...", pm.display_name()); } _ => { - println!("Package pnpm is not installed. Installing now..."); - let args = vec!["install", "--global", "pnpm"]; - execute_command("npm", args, current_dir).await?; + if pm.can_auto_install() { + println!( + "{} is not installed. Installing now...", + pm.display_name() + ); + let args = vec!["install", "--global", pm.command()]; + execute_command("npm", args, current_dir).await?; + } else { + return Err(anyhow::anyhow!( + "{} is not installed. Please install it first.", + pm.display_name() + )); + } } } Ok(()) } - async fn pnpm_install(project_paths: &ParsedProjectPaths) -> Result { - println!("Checking for pnpm package..."); - check_and_install_pnpm(&project_paths.generated).await?; + async fn install_dependencies( + project_paths: &ParsedProjectPaths, + pm_config: &PackageManagerConfig, + ) -> Result { + let pm = &pm_config.package_manager; + println!("Checking for {} package...", pm.display_name()); + check_package_manager(pm_config, &project_paths.generated).await?; + // Install in generated directory (without lockfile) execute_command( - "pnpm", - vec!["install", "--no-lockfile", "--prefer-offline"], + pm.command(), + pm.install_args_no_lockfile(), &project_paths.generated, ) .await?; + + // Install in project root (with lockfile preservation) execute_command( - "pnpm", - vec!["install", "--prefer-offline"], + pm.command(), + pm.install_args_optimized(), &project_paths.project_root, ) .await } + /// Fix the node_modules/generated symlink for bun projects. + /// + /// Bun copies local file dependencies instead of symlinking them, which causes + /// module duplication issues when the handler files import from "generated". + /// This ensures node_modules/generated is a symlink to ./generated. + fn fix_bun_generated_symlink(project_paths: &ParsedProjectPaths) -> anyhow::Result<()> { + let node_modules_generated = project_paths.project_root.join("node_modules/generated"); + + // Check if node_modules/generated exists + if node_modules_generated.exists() { + // Check if it's already a symlink + if node_modules_generated.is_symlink() { + // Already a symlink, nothing to do + return Ok(()); + } + + // It's a directory (bun copied instead of symlinking) + // Remove the directory + std::fs::remove_dir_all(&node_modules_generated).with_context(|| { + format!( + "Failed to remove node_modules/generated directory at {}", + node_modules_generated.display() + ) + })?; + } + + // Create symlink from node_modules/generated -> ../generated + // Use relative path for portability + #[cfg(unix)] + { + std::os::unix::fs::symlink("../generated", &node_modules_generated).with_context( + || { + format!( + "Failed to create symlink at {}", + node_modules_generated.display() + ) + }, + )?; + } + + #[cfg(windows)] + { + // On Windows, use junction for directory symlinks (doesn't require admin) + std::os::windows::fs::symlink_dir( + project_paths.project_root.join("generated"), + &node_modules_generated, + ) + .with_context(|| { + format!( + "Failed to create symlink at {}", + node_modules_generated.display() + ) + })?; + } + + Ok(()) + } + async fn run_post_codegen_command_sequence( project_paths: &ParsedProjectPaths, + pm_config: &PackageManagerConfig, ) -> anyhow::Result { println!("Installing packages... "); - let exit1 = pnpm_install(project_paths).await?; + let exit1 = install_dependencies(project_paths, pm_config).await?; if !exit1.success() { return Ok(exit1); } + // Fix bun's module duplication issue by ensuring node_modules/generated is a symlink + // Bun copies local file dependencies instead of symlinking them, which breaks + // module identity when handler files import from "generated" + if pm_config.is_bun_runtime() { + fix_bun_generated_symlink(project_paths)?; + } + println!("Generating HyperIndex code..."); - let exit3 = rescript::build(&project_paths.generated) + let exit3 = rescript::build(&project_paths.generated, pm_config) .await .context("Failed running rescript build")?; if !exit3.success() { @@ -143,16 +244,41 @@ pub mod codegen { .generate_templates(&config.parsed_project_paths) .context("Failed generating dynamic codegen files")?; - run_post_codegen_command_sequence(&config.parsed_project_paths) - .await - .context("Failed running post codegen command sequence")?; + // Create bunfig.toml for bun projects in generated folder + if config.package_manager_config.is_bun_runtime() { + let bunfig_content = r#"# Bun configuration file + +[install] +# Disable telemetry +telemetry = false + +# Use node_modules (default behavior) +linkStrategy = "node" +linker = "hoisted" + +# Save exact versions +save = "exact" +"#; + std::fs::write( + config.parsed_project_paths.generated.join("bunfig.toml"), + bunfig_content, + ) + .context("Failed to create bunfig.toml in generated folder")?; + } + + run_post_codegen_command_sequence( + &config.parsed_project_paths, + &config.package_manager_config, + ) + .await + .context("Failed running post codegen command sequence")?; Ok(()) } } pub mod start { - use super::execute_command; + use super::execute_command_string_args; use crate::config_parsing::system_config::SystemConfig; use anyhow::anyhow; @@ -169,14 +295,19 @@ pub mod start { ); } } - let cmd = "npm"; - let args = vec!["run", "start"]; - let exit = execute_command(cmd, args, &config.parsed_project_paths.generated).await?; + let pm = &config.package_manager_config.package_manager; + let exit = execute_command_string_args( + pm.command(), + pm.run_script_args("start"), + &config.parsed_project_paths.generated, + ) + .await?; if !exit.success() { return Err(anyhow!( "Indexer crashed. For more details see the error logs above the TUI. Can't find \ - them? Restart the indexer with the 'TUI_OFF=true pnpm start' command." + them? Restart the indexer with the 'TUI_OFF=true {} start' command.", + pm.command() )); } println!( @@ -215,16 +346,18 @@ pub mod db_migrate { use std::process::ExitStatus; - use super::execute_command; + use super::execute_command_string_args; use crate::{config_parsing::system_config::SystemConfig, persisted_state::PersistedState}; pub async fn run_up_migrations( config: &SystemConfig, persisted_state: &PersistedState, ) -> anyhow::Result<()> { - let args = vec!["db-up"]; + let pm = &config.package_manager_config.package_manager; let current_dir = &config.parsed_project_paths.generated; - let exit = execute_command("pnpm", args, current_dir).await?; + let exit = + execute_command_string_args(pm.command(), pm.run_script_args("db-up"), current_dir) + .await?; if !exit.success() { return Err(anyhow!("Failed to run db migrations")); @@ -238,18 +371,20 @@ pub mod db_migrate { } pub async fn run_drop_schema(config: &SystemConfig) -> anyhow::Result { - let args = vec!["db-down"]; + let pm = &config.package_manager_config.package_manager; let current_dir = &config.parsed_project_paths.generated; - execute_command("pnpm", args, current_dir).await + execute_command_string_args(pm.command(), pm.run_script_args("db-down"), current_dir).await } pub async fn run_db_setup( config: &SystemConfig, persisted_state: &PersistedState, ) -> anyhow::Result<()> { - let args = vec!["db-setup"]; + let pm = &config.package_manager_config.package_manager; let current_dir = &config.parsed_project_paths.generated; - let exit = execute_command("pnpm", args, current_dir).await?; + let exit = + execute_command_string_args(pm.command(), pm.run_script_args("db-setup"), current_dir) + .await?; if !exit.success() { return Err(anyhow!("Failed to run db migrations")); @@ -264,14 +399,19 @@ pub mod db_migrate { } pub mod benchmark { - use super::execute_command; + use super::execute_command_string_args; use crate::config_parsing::system_config::SystemConfig; use anyhow::{anyhow, Result}; pub async fn print_summary(config: &SystemConfig) -> Result<()> { - let args = vec!["print-benchmark-summary"]; + let pm = &config.package_manager_config.package_manager; let current_dir = &config.parsed_project_paths.generated; - let exit = execute_command("pnpm", args, current_dir).await?; + let exit = execute_command_string_args( + pm.command(), + pm.run_script_args("print-benchmark-summary"), + current_dir, + ) + .await?; if !exit.success() { return Err(anyhow!("Failed printing benchmark summary")); diff --git a/codegenerator/cli/src/config_parsing/graph_migration/mod.rs b/codegenerator/cli/src/config_parsing/graph_migration/mod.rs index 0fff2f0e4..ab008b905 100644 --- a/codegenerator/cli/src/config_parsing/graph_migration/mod.rs +++ b/codegenerator/cli/src/config_parsing/graph_migration/mod.rs @@ -267,6 +267,7 @@ pub async fn generate_config_from_subgraph_id( output: None, handlers: None, full_batch_size: None, + package_manager: None, }, ecosystem: None, contracts: None, diff --git a/codegenerator/cli/src/config_parsing/human_config.rs b/codegenerator/cli/src/config_parsing/human_config.rs index 61a057473..f1f81becb 100644 --- a/codegenerator/cli/src/config_parsing/human_config.rs +++ b/codegenerator/cli/src/config_parsing/human_config.rs @@ -1,3 +1,4 @@ +use crate::package_manager::PackageManager; use crate::utils::normalized_list::{NormalizedList, SingleOrList}; use schemars::{json_schema, JsonSchema, Schema, SchemaGenerator}; use serde::{Deserialize, Serialize}; @@ -87,6 +88,12 @@ pub struct BaseConfig { description = "Target number of events to be processed per batch. Set it to smaller number if you have many Effect API calls which are slow to resolve and can't be batched. (Default: 5000)" )] pub full_batch_size: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[schemars( + description = "Package manager to use: npm, yarn, pnpm, or bun. If not specified, \ + auto-detected from lockfiles in the project directory." + )] + pub package_manager: Option, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, JsonSchema)] @@ -999,6 +1006,7 @@ address: ["0x2E645469f354BB4F5c8a05B3b30A929361cf77eC"] output: None, handlers: None, full_batch_size: None, + package_manager: None, }, ecosystem: fuel::EcosystemTag::Fuel, contracts: None, @@ -1048,6 +1056,7 @@ address: ["0x2E645469f354BB4F5c8a05B3b30A929361cf77eC"] output: None, handlers: None, full_batch_size: None, + package_manager: None, }, ecosystem: fuel::EcosystemTag::Fuel, contracts: None, diff --git a/codegenerator/cli/src/config_parsing/system_config.rs b/codegenerator/cli/src/config_parsing/system_config.rs index 3175d6502..7b60077eb 100644 --- a/codegenerator/cli/src/config_parsing/system_config.rs +++ b/codegenerator/cli/src/config_parsing/system_config.rs @@ -18,6 +18,7 @@ use crate::{ constants::{links, project_paths::DEFAULT_SCHEMA_PATH}, evm::abi::AbiOrNestedAbi, fuel::abi::{FuelAbi, BURN_EVENT_NAME, CALL_EVENT_NAME, MINT_EVENT_NAME, TRANSFER_EVENT_NAME}, + package_manager::PackageManagerConfig, project_paths::{path_utils, ParsedProjectPaths}, rescript_types::RescriptTypeIdent, utils::unique_hashmap, @@ -414,6 +415,7 @@ pub struct SystemConfig { pub lowercase_addresses: bool, pub should_use_hypersync_client_decoder: bool, pub handlers: Option, + pub package_manager_config: PackageManagerConfig, } //Getter methods for system config @@ -689,6 +691,12 @@ impl SystemConfig { has_rpc_sync_src, )?; + // Detect package manager from lockfiles or config override + let package_manager_config = PackageManagerConfig::detect( + &final_project_paths.project_root, + base_config.package_manager, + ); + Ok(SystemConfig { name: base_config.name.clone(), parsed_project_paths: final_project_paths, @@ -719,6 +727,7 @@ impl SystemConfig { }, handlers: base_config.handlers.clone(), human_config, + package_manager_config, }) } HumanConfig::Fuel(ref fuel_config) => { @@ -841,6 +850,12 @@ impl SystemConfig { .context("Failed inserting network at chains map")?; } + // Detect package manager from lockfiles or config override + let package_manager_config = PackageManagerConfig::detect( + &final_project_paths.project_root, + base_config.package_manager, + ); + Ok(SystemConfig { name: base_config.name.clone(), parsed_project_paths: final_project_paths, @@ -860,6 +875,7 @@ impl SystemConfig { should_use_hypersync_client_decoder: true, handlers: base_config.handlers.clone(), human_config, + package_manager_config, }) } HumanConfig::Svm(ref svm_config) => { @@ -881,6 +897,12 @@ impl SystemConfig { .context("Failed inserting network at chains map")?; } + // Detect package manager from lockfiles or config override + let package_manager_config = PackageManagerConfig::detect( + &final_project_paths.project_root, + svm_config.base.package_manager, + ); + Ok(SystemConfig { name: svm_config.base.name.clone(), parsed_project_paths: final_project_paths, @@ -901,6 +923,7 @@ impl SystemConfig { should_use_hypersync_client_decoder: false, handlers: None, human_config, + package_manager_config, }) } } @@ -2122,6 +2145,7 @@ mod test { output: None, handlers: None, full_batch_size: None, + package_manager: None, }, ecosystem: None, contracts: None, @@ -2172,6 +2196,7 @@ mod test { output: Some("custom/output".to_string()), handlers: None, full_batch_size: None, + package_manager: None, }, ecosystem: None, contracts: None, diff --git a/codegenerator/cli/src/executor/init.rs b/codegenerator/cli/src/executor/init.rs index acf60f4cb..3159fc84e 100644 --- a/codegenerator/cli/src/executor/init.rs +++ b/codegenerator/cli/src/executor/init.rs @@ -15,6 +15,7 @@ use crate::{ contract_import_templates, hbs_dir_generator::HandleBarsDirGenerator, init_templates::InitTemplates, }, + package_manager::PackageManagerConfig, project_paths::ParsedProjectPaths, template_dirs::TemplateDirs, utils::file_system, @@ -244,12 +245,19 @@ pub async fn run_init_args(init_args: InitArgs, project_paths: &ProjectPaths) -> let envio_version = get_envio_version()?; + // Use CLI flag if provided, otherwise detect from lockfiles (defaults to npm for new projects) + let pm_config = PackageManagerConfig::detect( + &parsed_project_paths.project_root, + init_config.package_manager, + ); + let hbs_template = InitTemplates::new( init_config.name.clone(), &init_config.language, &parsed_project_paths, envio_version.clone(), init_config.api_token, + &pm_config, ) .context("Failed creating init templates")?; @@ -263,6 +271,28 @@ pub async fn run_init_args(init_args: InitArgs, project_paths: &ProjectPaths) -> hbs_generator.generate_hbs_templates()?; + // Create bunfig.toml for bun projects + if pm_config.is_bun_runtime() { + let bunfig_content = r#"# Bun configuration file + +[install] +# Disable telemetry +telemetry = false + +# Use node_modules (default behavior) +linkStrategy = "node" +linker = "hoisted" + +# Save exact versions +save = "exact" +"#; + std::fs::write( + parsed_project_paths.project_root.join("bunfig.toml"), + bunfig_content, + ) + .context("Failed to create bunfig.toml")?; + } + println!("Project template ready"); println!("Running codegen"); @@ -272,7 +302,11 @@ pub async fn run_init_args(init_args: InitArgs, project_paths: &ProjectPaths) -> commands::codegen::run_codegen(&config).await?; if init_config.language == Language::ReScript { - let res_build_exit = commands::rescript::build(&parsed_project_paths.project_root).await?; + let res_build_exit = commands::rescript::build( + &parsed_project_paths.project_root, + &config.package_manager_config, + ) + .await?; if !res_build_exit.success() { return Err(anyhow!("Failed to build rescript"))?; } diff --git a/codegenerator/cli/src/hbs_templating/codegen_templates.rs b/codegenerator/cli/src/hbs_templating/codegen_templates.rs index 274787789..9f78df938 100644 --- a/codegenerator/cli/src/hbs_templating/codegen_templates.rs +++ b/codegenerator/cli/src/hbs_templating/codegen_templates.rs @@ -1125,6 +1125,11 @@ pub struct ProjectTemplate { //Used for the package.json reference to handlers in generated relative_path_to_root_from_generated: String, relative_path_to_generated_from_root: String, + // Package manager and runtime info for templates + package_manager: String, + is_bun_runtime: bool, + // Package manager run prefix: "npm run ", "yarn ", "pnpm ", or "bun run " + pm_run_prefix: String, } impl ProjectTemplate { @@ -1647,6 +1652,9 @@ export default {{ //Used for the package.json reference to handlers in generated relative_path_to_root_from_generated, relative_path_to_generated_from_root, + package_manager: cfg.package_manager_config.package_manager.command().to_string(), + is_bun_runtime: cfg.package_manager_config.is_bun_runtime(), + pm_run_prefix: cfg.package_manager_config.run_script_prefix().to_string(), }) } } diff --git a/codegenerator/cli/src/hbs_templating/init_templates.rs b/codegenerator/cli/src/hbs_templating/init_templates.rs index e235bc1df..7e231b959 100644 --- a/codegenerator/cli/src/hbs_templating/init_templates.rs +++ b/codegenerator/cli/src/hbs_templating/init_templates.rs @@ -1,5 +1,6 @@ use crate::{ cli_args::init_config::Language, + package_manager::PackageManagerConfig, project_paths::{path_utils::add_leading_relative_dot, ParsedProjectPaths}, }; use anyhow::{anyhow, Context}; @@ -16,6 +17,11 @@ pub struct InitTemplates { //Used for the package.json reference to generated in handlers relative_path_from_root_to_generated: String, envio_api_token: Option, + // Package manager and runtime info for templates + package_manager: String, + is_bun_runtime: bool, + // Package manager run prefix: "npm run ", "yarn ", "pnpm ", or "bun run " + pm_run_prefix: String, } impl InitTemplates { @@ -25,6 +31,7 @@ impl InitTemplates { project_paths: &ParsedProjectPaths, envio_version: String, envio_api_token: Option, + pm_config: &PackageManagerConfig, ) -> anyhow::Result { //Take the absolute paths of project root and generated, diff them to get //relative path from root to generated and add a leading dot. So in a default project, if your @@ -50,6 +57,9 @@ impl InitTemplates { envio_version, relative_path_from_root_to_generated, envio_api_token, + package_manager: pm_config.package_manager.command().to_string(), + is_bun_runtime: pm_config.is_bun_runtime(), + pm_run_prefix: pm_config.run_script_prefix().to_string(), }; Ok(template) @@ -59,16 +69,19 @@ impl InitTemplates { #[cfg(test)] mod test { use super::*; + use crate::package_manager::PackageManager; use pretty_assertions::assert_eq; #[test] fn test_new_init_template() { + let pm_config = PackageManagerConfig::new(PackageManager::Npm); let init_temp = InitTemplates::new( "my-project".to_string(), &Language::ReScript, &ParsedProjectPaths::default(), "latest".to_string(), None, + &pm_config, ) .unwrap(); @@ -79,6 +92,9 @@ mod test { envio_version: "latest".to_string(), relative_path_from_root_to_generated: "./generated".to_string(), envio_api_token: None, + package_manager: "npm".to_string(), + is_bun_runtime: false, + pm_run_prefix: "npm run ".to_string(), }; assert_eq!(expected, init_temp); diff --git a/codegenerator/cli/src/lib.rs b/codegenerator/cli/src/lib.rs index d84bd4252..1e57bd9b7 100644 --- a/codegenerator/cli/src/lib.rs +++ b/codegenerator/cli/src/lib.rs @@ -8,6 +8,7 @@ mod evm; pub mod executor; mod fuel; mod hbs_templating; +pub mod package_manager; mod persisted_state; mod project_paths; mod rescript_types; diff --git a/codegenerator/cli/src/package_manager.rs b/codegenerator/cli/src/package_manager.rs new file mode 100644 index 000000000..1a9592e6c --- /dev/null +++ b/codegenerator/cli/src/package_manager.rs @@ -0,0 +1,396 @@ +//! Package manager and runtime detection for the Envio CLI. +//! +//! This module provides automatic detection of package managers (npm, yarn, pnpm, bun) +//! and JavaScript runtimes (node, bun) based on lockfiles in the project directory. + +use clap::ValueEnum; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::path::Path; + +/// Supported package managers +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default, ValueEnum)] +#[serde(rename_all = "lowercase")] +pub enum PackageManager { + #[default] + Npm, + Yarn, + Pnpm, + Bun, +} + +impl PackageManager { + /// Get the command name for this package manager + pub fn command(&self) -> &'static str { + match self { + Self::Npm => "npm", + Self::Yarn => "yarn", + Self::Pnpm => "pnpm", + Self::Bun => "bun", + } + } + + /// Get the display name (for user messages) + pub fn display_name(&self) -> &'static str { + match self { + Self::Npm => "npm", + Self::Yarn => "Yarn", + Self::Pnpm => "pnpm", + Self::Bun => "Bun", + } + } + + /// Get arguments for running `install` + pub fn install_args(&self) -> Vec<&'static str> { + match self { + Self::Npm => vec!["install"], + Self::Yarn => vec!["install"], + Self::Pnpm => vec!["install"], + Self::Bun => vec!["install"], + } + } + + /// Get arguments for running `install` with flags for CI/offline optimization + pub fn install_args_optimized(&self) -> Vec<&'static str> { + match self { + Self::Npm => vec!["install"], + Self::Yarn => vec!["install"], + Self::Pnpm => vec!["install", "--prefer-offline"], + Self::Bun => vec!["install"], + } + } + + /// Get arguments for running `install` without generating lockfile + pub fn install_args_no_lockfile(&self) -> Vec<&'static str> { + match self { + Self::Npm => vec!["install", "--no-package-lock"], + Self::Yarn => vec!["install", "--no-lockfile"], + Self::Pnpm => vec!["install", "--no-lockfile", "--prefer-offline"], + Self::Bun => vec!["install", "--no-save"], + } + } + + /// Get arguments for running a script (e.g., "start", "test") + pub fn run_script_args(&self, script: &str) -> Vec { + match self { + Self::Npm => vec!["run".to_string(), script.to_string()], + Self::Yarn => vec![script.to_string()], + Self::Pnpm => vec![script.to_string()], + Self::Bun => vec!["run".to_string(), script.to_string()], + } + } + + /// Check if this package manager can auto-install if missing + /// Only pnpm is auto-installed (via npm install -g pnpm) for backwards compatibility + pub fn can_auto_install(&self) -> bool { + matches!(self, Self::Pnpm) + } +} + +/// JavaScript runtimes +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "lowercase")] +pub enum Runtime { + #[default] + Node, + Bun, +} + +impl Runtime { + /// Get the command name for this runtime + pub fn command(&self) -> &'static str { + match self { + Self::Node => "node", + Self::Bun => "bun", + } + } +} + +/// Combined configuration for package manager and runtime +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PackageManagerConfig { + pub package_manager: PackageManager, + pub runtime: Runtime, +} + +impl Default for PackageManagerConfig { + fn default() -> Self { + Self { + package_manager: PackageManager::Npm, + runtime: Runtime::Node, + } + } +} + +impl PackageManagerConfig { + /// Create a new PackageManagerConfig with the given package manager + /// Runtime is automatically determined based on the package manager + pub fn new(package_manager: PackageManager) -> Self { + let runtime = match package_manager { + PackageManager::Bun => Runtime::Bun, + _ => Runtime::Node, + }; + Self { + package_manager, + runtime, + } + } + + /// Detect package manager from lockfiles in project directory, with optional config override + /// + /// Priority order (when no config override): + /// 1. bun.lockb or bun.lock → bun (runtime + package manager) + /// 2. pnpm-lock.yaml → pnpm + node + /// 3. yarn.lock → yarn + node + /// 4. package-lock.json → npm + node + /// 5. No lockfile → npm + node (most universal fallback) + pub fn detect(project_root: &Path, config_override: Option) -> Self { + // If config specifies a package manager, use it + if let Some(pm) = config_override { + return Self::new(pm); + } + + // Detect from lockfiles (priority order) + let pm = if project_root.join("bun.lockb").exists() + || project_root.join("bun.lock").exists() + { + PackageManager::Bun + } else if project_root.join("pnpm-lock.yaml").exists() { + PackageManager::Pnpm + } else if project_root.join("yarn.lock").exists() { + PackageManager::Yarn + } else if project_root.join("package-lock.json").exists() { + PackageManager::Npm + } else { + // Default fallback - npm is the most universal + PackageManager::Npm + }; + + Self::new(pm) + } + + /// Returns true if bun is used as the runtime + pub fn is_bun_runtime(&self) -> bool { + self.runtime == Runtime::Bun + } + + /// Generate package.json script prefix for chaining commands + /// e.g., "pnpm build && " for pnpm with ReScript + pub fn script_chain_prefix(&self, script: &str) -> String { + match self.package_manager { + PackageManager::Npm => format!("npm run {} && ", script), + PackageManager::Yarn => format!("yarn {} && ", script), + PackageManager::Pnpm => format!("pnpm {} && ", script), + PackageManager::Bun => format!("bun run {} && ", script), + } + } + + /// Generate the run script command for package.json scripts + /// e.g., "pnpm mocha" for pnpm + pub fn run_script(&self, script: &str) -> String { + match self.package_manager { + PackageManager::Npm => format!("npm run {}", script), + PackageManager::Yarn => format!("yarn {}", script), + PackageManager::Pnpm => format!("pnpm {}", script), + PackageManager::Bun => format!("bun run {}", script), + } + } + + /// Get the run script prefix for use in package.json templates + /// e.g., "npm run " for npm, "pnpm " for pnpm + pub fn run_script_prefix(&self) -> &'static str { + match self.package_manager { + PackageManager::Npm => "npm run ", + PackageManager::Yarn => "yarn ", + PackageManager::Pnpm => "pnpm ", + PackageManager::Bun => "bun run ", + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::File; + use tempfile::tempdir; + + #[test] + fn test_detect_bun_from_lockb() { + let dir = tempdir().unwrap(); + File::create(dir.path().join("bun.lockb")).unwrap(); + + let config = PackageManagerConfig::detect(dir.path(), None); + assert_eq!(config.package_manager, PackageManager::Bun); + assert_eq!(config.runtime, Runtime::Bun); + } + + #[test] + fn test_detect_bun_from_lock() { + let dir = tempdir().unwrap(); + File::create(dir.path().join("bun.lock")).unwrap(); + + let config = PackageManagerConfig::detect(dir.path(), None); + assert_eq!(config.package_manager, PackageManager::Bun); + assert_eq!(config.runtime, Runtime::Bun); + } + + #[test] + fn test_detect_pnpm_from_lockfile() { + let dir = tempdir().unwrap(); + File::create(dir.path().join("pnpm-lock.yaml")).unwrap(); + + let config = PackageManagerConfig::detect(dir.path(), None); + assert_eq!(config.package_manager, PackageManager::Pnpm); + assert_eq!(config.runtime, Runtime::Node); + } + + #[test] + fn test_detect_yarn_from_lockfile() { + let dir = tempdir().unwrap(); + File::create(dir.path().join("yarn.lock")).unwrap(); + + let config = PackageManagerConfig::detect(dir.path(), None); + assert_eq!(config.package_manager, PackageManager::Yarn); + assert_eq!(config.runtime, Runtime::Node); + } + + #[test] + fn test_detect_npm_from_lockfile() { + let dir = tempdir().unwrap(); + File::create(dir.path().join("package-lock.json")).unwrap(); + + let config = PackageManagerConfig::detect(dir.path(), None); + assert_eq!(config.package_manager, PackageManager::Npm); + assert_eq!(config.runtime, Runtime::Node); + } + + #[test] + fn test_detect_defaults_to_npm() { + let dir = tempdir().unwrap(); + // No lockfiles + + let config = PackageManagerConfig::detect(dir.path(), None); + assert_eq!(config.package_manager, PackageManager::Npm); + assert_eq!(config.runtime, Runtime::Node); + } + + #[test] + fn test_config_override_takes_precedence() { + let dir = tempdir().unwrap(); + // Create pnpm lockfile + File::create(dir.path().join("pnpm-lock.yaml")).unwrap(); + + // Override should take precedence + let config = PackageManagerConfig::detect(dir.path(), Some(PackageManager::Yarn)); + assert_eq!(config.package_manager, PackageManager::Yarn); + assert_eq!(config.runtime, Runtime::Node); + } + + #[test] + fn test_bun_lockfile_priority() { + let dir = tempdir().unwrap(); + // Create multiple lockfiles - bun should win + File::create(dir.path().join("bun.lockb")).unwrap(); + File::create(dir.path().join("pnpm-lock.yaml")).unwrap(); + File::create(dir.path().join("yarn.lock")).unwrap(); + + let config = PackageManagerConfig::detect(dir.path(), None); + assert_eq!(config.package_manager, PackageManager::Bun); + } + + #[test] + fn test_command_generation() { + assert_eq!(PackageManager::Npm.command(), "npm"); + assert_eq!(PackageManager::Yarn.command(), "yarn"); + assert_eq!(PackageManager::Pnpm.command(), "pnpm"); + assert_eq!(PackageManager::Bun.command(), "bun"); + } + + #[test] + fn test_run_script_args() { + assert_eq!( + PackageManager::Npm.run_script_args("test"), + vec!["run", "test"] + ); + assert_eq!(PackageManager::Yarn.run_script_args("test"), vec!["test"]); + assert_eq!(PackageManager::Pnpm.run_script_args("test"), vec!["test"]); + assert_eq!( + PackageManager::Bun.run_script_args("test"), + vec!["run", "test"] + ); + } + + #[test] + fn test_install_args_no_lockfile() { + assert_eq!( + PackageManager::Npm.install_args_no_lockfile(), + vec!["install", "--no-package-lock"] + ); + assert_eq!( + PackageManager::Yarn.install_args_no_lockfile(), + vec!["install", "--no-lockfile"] + ); + assert_eq!( + PackageManager::Pnpm.install_args_no_lockfile(), + vec!["install", "--no-lockfile", "--prefer-offline"] + ); + assert_eq!( + PackageManager::Bun.install_args_no_lockfile(), + vec!["install", "--no-save"] + ); + } + + #[test] + fn test_script_chain_prefix() { + let npm_config = PackageManagerConfig::new(PackageManager::Npm); + assert_eq!(npm_config.script_chain_prefix("build"), "npm run build && "); + + let yarn_config = PackageManagerConfig::new(PackageManager::Yarn); + assert_eq!(yarn_config.script_chain_prefix("build"), "yarn build && "); + + let pnpm_config = PackageManagerConfig::new(PackageManager::Pnpm); + assert_eq!(pnpm_config.script_chain_prefix("build"), "pnpm build && "); + + let bun_config = PackageManagerConfig::new(PackageManager::Bun); + assert_eq!(bun_config.script_chain_prefix("build"), "bun run build && "); + } + + #[test] + fn test_is_bun_runtime() { + let bun_config = PackageManagerConfig::new(PackageManager::Bun); + assert!(bun_config.is_bun_runtime()); + + let npm_config = PackageManagerConfig::new(PackageManager::Npm); + assert!(!npm_config.is_bun_runtime()); + } + + #[test] + fn test_serde_roundtrip() { + let pm = PackageManager::Pnpm; + let serialized = serde_json::to_string(&pm).unwrap(); + assert_eq!(serialized, "\"pnpm\""); + + let deserialized: PackageManager = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, pm); + } + + #[test] + fn test_serde_all_variants() { + assert_eq!( + serde_json::to_string(&PackageManager::Npm).unwrap(), + "\"npm\"" + ); + assert_eq!( + serde_json::to_string(&PackageManager::Yarn).unwrap(), + "\"yarn\"" + ); + assert_eq!( + serde_json::to_string(&PackageManager::Pnpm).unwrap(), + "\"pnpm\"" + ); + assert_eq!( + serde_json::to_string(&PackageManager::Bun).unwrap(), + "\"bun\"" + ); + } +} diff --git a/codegenerator/cli/templates/dynamic/codegen/package.json.hbs b/codegenerator/cli/templates/dynamic/codegen/package.json.hbs index 3cd4b2c62..863d9d7a7 100644 --- a/codegenerator/cli/templates/dynamic/codegen/package.json.hbs +++ b/codegenerator/cli/templates/dynamic/codegen/package.json.hbs @@ -12,18 +12,31 @@ "format": "rescript format -all", {{!-- We need this to always have cwd at the root of the project --}} {{!-- For imports and dotenv to correctly resolve --}} - {{!-- Also, tsx is needed for db- scripts only because of internal.config.ts --}} + {{#if is_bun_runtime}} + {{!-- Bun can run TS natively, no tsx needed --}} + "db-up": "cd {{relative_path_to_root_from_generated}} && bun -e 'import(\"{{relative_path_to_generated_from_root}}/src/db/Migrations.res.mjs\").then(m => m.runUpMigrations(true))'", + "db-down": "cd {{relative_path_to_root_from_generated}} && bun -e 'import(\"{{relative_path_to_generated_from_root}}/src/db/Migrations.res.mjs\").then(m => m.runDownMigrations(true))'", + "db-setup": "cd {{relative_path_to_root_from_generated}} && bun -e 'import(\"{{relative_path_to_generated_from_root}}/src/db/Migrations.res.mjs\").then(m => m.runUpMigrations(true, true))'", + "print-benchmark-summary": "cd {{relative_path_to_root_from_generated}} && bun -e 'import(\"{{relative_path_to_generated_from_root}}/src/Benchmark.res.mjs\").then(m => m.Summary.printSummary())'", + "start": "cd {{relative_path_to_root_from_generated}} && bun run {{relative_path_to_generated_from_root}}/src/Index.res.mjs" + {{else}} + {{!-- Node requires tsx for TypeScript imports --}} "db-up": "cd {{relative_path_to_root_from_generated}} && node --import {{relative_path_to_generated_from_root}}/node_modules/tsx/dist/esm/index.mjs -e 'import(\"{{relative_path_to_generated_from_root}}/src/db/Migrations.res.mjs\").then(m => m.runUpMigrations(true))'", "db-down": "cd {{relative_path_to_root_from_generated}} && node --import {{relative_path_to_generated_from_root}}/node_modules/tsx/dist/esm/index.mjs -e 'import(\"{{relative_path_to_generated_from_root}}/src/db/Migrations.res.mjs\").then(m => m.runDownMigrations(true))'", "db-setup": "cd {{relative_path_to_root_from_generated}} && node --import {{relative_path_to_generated_from_root}}/node_modules/tsx/dist/esm/index.mjs -e 'import(\"{{relative_path_to_generated_from_root}}/src/db/Migrations.res.mjs\").then(m => m.runUpMigrations(true, true))'", "print-benchmark-summary": "cd {{relative_path_to_root_from_generated}} && node --import {{relative_path_to_generated_from_root}}/node_modules/tsx/dist/esm/index.mjs -e 'import(\"{{relative_path_to_generated_from_root}}/src/Benchmark.res.mjs\").then(m => m.Summary.printSummary())'", "start": "cd {{relative_path_to_root_from_generated}} && tsc --noEmit && node --no-warnings --import {{relative_path_to_generated_from_root}}/node_modules/tsx/dist/esm/index.mjs {{relative_path_to_generated_from_root}}/src/Index.res.mjs" + {{/if}} }, "keywords": [ "ReScript" ], "engines": { + {{#if is_bun_runtime}} + "bun": ">=1.0.0" + {{else}} "node": ">=22.0.0" + {{/if}} }, "author": "", "license": "MIT", @@ -36,7 +49,9 @@ "@fuel-ts/utils": "0.96.1", "@fuel-ts/address": "0.96.1", {{/if}} + {{#unless is_bun_runtime}} "tsx": "4.21.0", + {{/unless}} "bignumber.js": "9.1.2", "date-fns": "3.3.1", "ethers": "6.8.0", diff --git a/codegenerator/cli/templates/dynamic/init_templates/shared/package.json.hbs b/codegenerator/cli/templates/dynamic/init_templates/shared/package.json.hbs index 07ccaaccb..b9d0a3882 100644 --- a/codegenerator/cli/templates/dynamic/init_templates/shared/package.json.hbs +++ b/codegenerator/cli/templates/dynamic/init_templates/shared/package.json.hbs @@ -3,33 +3,48 @@ "version": "0.1.0", "type": "module", "scripts": { + {{#if is_bun_runtime}} + {{#if is_typescript}} + "test": "bun test test/**/*.ts", + {{/if}} + {{#if is_rescript}} + "test": "bun run build && bun test test/**/*.res.mjs", + "clean": "rescript clean", + "build": "rescript", + "watch": "rescript -w", + {{/if}} + {{else}} {{#if is_typescript}} "mocha": "tsc --noEmit && NODE_OPTIONS='--no-warnings --import tsx' mocha --exit test/**/*.ts", + "test": "{{pm_run_prefix}}mocha", {{/if}} {{#if is_rescript}} "mocha": "NODE_OPTIONS='--no-warnings --import tsx' mocha --exit test/**/*.res.mjs", + "test": "{{pm_run_prefix}}build && {{pm_run_prefix}}mocha", "clean": "rescript clean", "build": "rescript", "watch": "rescript -w", + {{/if}} {{/if}} "codegen": "envio codegen", - "dev": "{{#if is_rescript}}pnpm build && {{/if}}envio dev", - "start": "{{#if is_rescript}}pnpm build && {{/if}}envio start", - "test": "{{#if is_rescript}}pnpm build && {{/if}}pnpm mocha" + "dev": "{{#if is_rescript}}{{pm_run_prefix}}build && {{/if}}envio dev", + "start": "{{#if is_rescript}}{{pm_run_prefix}}build && {{/if}}envio start" }, "devDependencies": { {{#if is_rescript}} - "rescript": "11.1.3", + "rescript": "11.1.3"{{#unless is_bun_runtime}}, + "tsx": "4.21.0", + "mocha": "10.2.0"{{/unless}} {{/if}} {{#if is_typescript}} - "@types/chai": "^4.3.11", + "@types/chai": "^4.3.11", "@types/mocha": "10.0.6", "@types/node": "^24.10.1", "typescript": "5.9.3", - "chai": "4.3.10", - {{/if}} + "chai": "4.3.10"{{#unless is_bun_runtime}}, "tsx": "4.21.0", - "mocha": "10.2.0" + "mocha": "10.2.0"{{/unless}} + {{/if}} }, "dependencies": { "envio": "{{envio_version}}" @@ -38,6 +53,10 @@ "generated": "{{relative_path_from_root_to_generated}}" }, "engines": { + {{#if is_bun_runtime}} + "bun": ">=1.0.0" + {{else}} "node": ">=22.0.0" + {{/if}} } }