mirror of
https://github.com/zhom/banderole.git
synced 2026-04-23 12:26:04 +02:00
feat: allow custom name
This commit is contained in:
@@ -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
@@ -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
@@ -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?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user