feat: allow custom name

This commit is contained in:
zhom
2025-07-25 03:32:19 +04:00
parent b2ba28fdaa
commit 7b2bda3f7b
5 changed files with 35 additions and 41 deletions
+14 -6
View File
@@ -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
+11 -17
View File
@@ -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<PathBuf>) -> Result<()> {
pub async fn bundle_project(project_path: PathBuf, output_path: Option<PathBuf>, custom_name: Option<String>) -> 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<PathBuf>)
// 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<u8>, app_name: &str) -> Result<()> {
fn create_self_extracting_executable(out: &Path, zip_data: Vec<u8>, _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<u8>, app_name: &str, build_id: &str) -> Result<()> {
fn create_unix_executable(out: &Path, zip_data: Vec<u8>, 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<u8>, app_name: &str, build_id: &str) -> Result<()> {
fn create_windows_executable(out: &Path, zip_data: Vec<u8>, 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())?;
+5 -2
View File
@@ -26,6 +26,9 @@ enum Commands {
/// Output path for the bundle (optional)
#[arg(short, long)]
output: Option<PathBuf>,
/// Custom name for the executable (optional)
#[arg(short, long)]
name: Option<String>,
},
}
@@ -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?;
}
}
+1
View File
@@ -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(),
+4 -16
View File
@@ -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);