diff --git a/README.md b/README.md index 03809a0..7c6ddb9 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,10 @@ # Banderole -Create cross-platform single-executables for node.js projects. +Create cross-platform single-executables for Node.js projects. -Unlike [Node.js SEA](https://nodejs.org/api/single-executable-applications.html) or [pkg](https://github.com/yao-pkg/pkg), it bundles compiled node.js app, all node modules, and a portable node binary into a single executable, and on the first launch it will unpack everything into a cache directory. Every subsequent execution of the binary will point to the extract data. +Banderole bundles your Node.js app, all dependencies, and a portable Node binary into a single executable. On first launch, it unpacks to a cache directory for fast subsequent executions. -While it results in the same performance as executing `/path/to/portable/node my/app/index.js` (except for the first execution), it also means that binaries are a lot larger than, say, pkg, which traverses your project and dependencies to include only relevant files. - -You should stick to pkg (or Node.js SEA once it is stable enough) unless you have to deal with an app that has a nested dependency that has dynamic imports or imports non-javascript files, which makes it difficult to patch. +Unlike [Node.js SEA](https://nodejs.org/api/single-executable-applications.html) or [pkg](https://github.com/yao-pkg/pkg), banderole handles complex projects with dynamic imports and non-JavaScript files without requiring patches, but since it includes all dependencies by default, it has significantly large filesize. ## Installation @@ -17,7 +15,17 @@ cargo install banderole ## Usage ```sh -banderole project-dir output-dir +# Bundle a project using the project name +banderole bundle /path/to/project + +# Bundle with custom output path +banderole bundle /path/to/project --output /path/to/output/executable + +# Bundle with custom name +banderole bundle /path/to/project --name my-app + +# Bundle with both custom output and name +banderole bundle /path/to/project --output /path/to/my-app --name my-app ``` ## Feature List diff --git a/src/bundler_simple.rs b/src/bundler_simple.rs index ebdf77b..c4fcdce 100644 --- a/src/bundler_simple.rs +++ b/src/bundler_simple.rs @@ -14,9 +14,10 @@ use zip::ZipWriter; /// * `project_path` – path that contains a `package.json`. /// * `output_path` – optional path to the produced bundle file. If omitted, an /// automatically-generated name is used. +/// * `custom_name` – optional custom name for the executable. /// /// The implementation uses a simpler, more reliable approach based on Playwright's bundling strategy. -pub async fn bundle_project(project_path: PathBuf, output_path: Option) -> Result<()> { +pub async fn bundle_project(project_path: PathBuf, output_path: Option, custom_name: Option) -> Result<()> { // 1. Validate & canonicalize input directory. let project_path = project_path .canonicalize() @@ -45,13 +46,8 @@ pub async fn bundle_project(project_path: PathBuf, output_path: Option) // 4. Resolve output path. let output_path = output_path.unwrap_or_else(|| { let ext = if Platform::current().is_windows() { ".exe" } else { "" }; - PathBuf::from(format!( - "{name}-{ver}-{plat}{ext}", - name = &app_name, - ver = &app_version, - plat = Platform::current(), - ext = ext, - )) + let base_name = custom_name.as_ref().unwrap_or(&app_name); + PathBuf::from(format!("{base_name}{ext}")) }); // 5. Ensure portable Node binary is available. @@ -104,17 +100,17 @@ fn normalise_node_version(raw: &str) -> String { // Self-extracting executable generation using a more reliable approach // ──────────────────────────────────────────────────────────────────────────── -fn create_self_extracting_executable(out: &Path, zip_data: Vec, app_name: &str) -> Result<()> { +fn create_self_extracting_executable(out: &Path, zip_data: Vec, _app_name: &str) -> Result<()> { let build_id = Uuid::new_v4(); if Platform::current().is_windows() { - create_windows_executable(out, zip_data, app_name, &build_id.to_string()) + create_windows_executable(out, zip_data, &build_id.to_string()) } else { - create_unix_executable(out, zip_data, app_name, &build_id.to_string()) + create_unix_executable(out, zip_data, &build_id.to_string()) } } -fn create_unix_executable(out: &Path, zip_data: Vec, app_name: &str, build_id: &str) -> Result<()> { +fn create_unix_executable(out: &Path, zip_data: Vec, build_id: &str) -> Result<()> { use std::os::unix::fs::PermissionsExt; let mut file = fs::File::create(out).context("Failed to create output executable")?; @@ -151,7 +147,6 @@ if [ -f "$APP_DIR/app/package.json" ] && [ -x "$APP_DIR/node/bin/node" ]; then fi # Extract application -echo "Extracting {} to cache..." >&2 mkdir -p "$APP_DIR" # Create a temporary file for the zip data @@ -193,7 +188,7 @@ else fi __DATA__ -"#, build_id, app_name); +"#, build_id); file.write_all(script.as_bytes())?; @@ -210,7 +205,7 @@ __DATA__ Ok(()) } -fn create_windows_executable(out: &Path, zip_data: Vec, app_name: &str, build_id: &str) -> Result<()> { +fn create_windows_executable(out: &Path, zip_data: Vec, build_id: &str) -> Result<()> { let mut file = fs::File::create(out).context("Failed to create output executable")?; // Create a more reliable Windows batch script @@ -239,7 +234,6 @@ if exist "!APP_DIR!\app\package.json" if exist "!APP_DIR!\node\node.exe" ( ) REM Extract application -echo Extracting {} to cache... >&2 if not exist "!CACHE_DIR!" mkdir "!CACHE_DIR!" if not exist "!APP_DIR!" mkdir "!APP_DIR!" @@ -292,7 +286,7 @@ if exist "!MAIN_SCRIPT!" ( ) __DATA__ -"#, build_id, app_name); +"#, build_id); file.write_all(script.as_bytes())?; diff --git a/src/main.rs b/src/main.rs index e55e72c..c88c2a2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,9 @@ enum Commands { /// Output path for the bundle (optional) #[arg(short, long)] output: Option, + /// Custom name for the executable (optional) + #[arg(short, long)] + name: Option, }, } @@ -34,8 +37,8 @@ async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); match cli.command { - Commands::Bundle { path, output } => { - bundler_simple::bundle_project(path, output).await?; + Commands::Bundle { path, output, name } => { + bundler_simple::bundle_project(path, output, name).await?; } } diff --git a/src/node_downloader.rs b/src/node_downloader.rs index 7930d92..3b8fa0a 100644 --- a/src/node_downloader.rs +++ b/src/node_downloader.rs @@ -19,6 +19,7 @@ pub struct NodeDownloader { } impl NodeDownloader { + #[allow(dead_code)] pub fn new(cache_dir: PathBuf, node_version: String) -> Self { Self { platform: Platform::current(), diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 870f0ba..4a1bc1f 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -113,15 +113,9 @@ process.exit(0);"#; // Find the created executable let executable_path = temp_dir.path().join(if cfg!(windows) { - "integration-test-app-1.0.0-win32-x64.exe" - } else if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") { - "integration-test-app-1.0.0-darwin-arm64" - } else if cfg!(target_os = "macos") { - "integration-test-app-1.0.0-darwin-x64" - } else if cfg!(target_arch = "aarch64") { - "integration-test-app-1.0.0-linux-arm64" + "integration-test-app.exe" } else { - "integration-test-app-1.0.0-linux-x64" + "integration-test-app" }); if !executable_path.exists() { @@ -289,15 +283,9 @@ process.exit(0);"#; // Find and run the created executable to verify it uses the correct Node version let executable_name = if cfg!(target_os = "windows") { - "nvmrc-test-app-1.0.0-win32-x64.exe" - } else if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") { - "nvmrc-test-app-1.0.0-darwin-arm64" - } else if cfg!(target_os = "macos") { - "nvmrc-test-app-1.0.0-darwin-x64" - } else if cfg!(target_arch = "aarch64") { - "nvmrc-test-app-1.0.0-linux-arm64" + "nvmrc-test-app.exe" } else { - "nvmrc-test-app-1.0.0-linux-x64" + "nvmrc-test-app" }; let executable_path = temp_dir.path().join(executable_name);