executable-wrapper
Version:
Universal packaging tool that converts Node.js projects into native executables for macOS (.app/.pkg) and Windows (.exe)
195 lines (173 loc) ⢠6.72 kB
JavaScript
/**
* Executable Wrapper - Main Entry Point
*
* Usage:
* node index.js --env /path/to/project/.env [--platform macos-x64|macos-arm64|win-x64] [--format app|pkg|exe]
*
* Examples:
* node index.js --env ../myproject/.env --platform macos-x64 --format app
* node index.js --env ../myproject/.env --platform win-x64
*/
const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');
const path = require('path');
const fs = require('fs');
const { loadConfig } = require('./lib/config');
const { buildExecutable } = require('./lib/builder');
const { createMacOSApp } = require('./lib/macos/app-builder');
const { createMacOSPkg } = require('./lib/macos/pkg-builder');
const { createUniversalApp } = require('./lib/macos/universal-builder');
const { createUniversalPkg } = require('./lib/macos/universal-pkg-builder');
const { signMacOS } = require('./lib/macos/signer');
const { notarizeMacOS } = require('./lib/macos/notarizer');
const { signWindows } = require('./lib/windows/signer');
// Parse command line arguments
const argv = yargs(hideBin(process.argv))
.option('env', {
alias: 'e',
type: 'string',
description: 'Path to the .env configuration file',
demandOption: true,
})
.option('platform', {
alias: 'p',
type: 'string',
description: 'Target platform',
choices: ['macos-x64', 'macos-arm64', 'macos-universal', 'win-x64'],
default: 'macos-x64',
})
.option('format', {
alias: 'f',
type: 'string',
description: 'Output format',
choices: ['executable', 'app', 'pkg', 'exe'],
default: 'executable',
})
.option('sign', {
type: 'boolean',
description: 'Sign the output (requires signing configuration)',
default: false,
})
.option('notarize', {
type: 'boolean',
description: 'Notarize the output (macOS only, requires notarization configuration)',
default: false,
})
.option('terminal-script', {
type: 'boolean',
description: 'Generate wrapper script that shows terminal output (macOS only)',
default: false,
})
.option('output', {
alias: 'o',
type: 'string',
description: 'Output directory (optional, defaults to project releases/ directory)',
})
.example('$0 --env ../myapp/.env', 'Build executable for default platform')
.example('$0 --env ../myapp/.env --platform macos-arm64 --format app --sign --notarize', 'Build signed and notarized macOS .app')
.example('$0 --env ../myapp/.env --platform win-x64 --sign', 'Build signed Windows executable')
.help()
.alias('help', 'h')
.version()
.alias('version', 'v')
.argv;
async function main() {
try {
console.log('š Executable Wrapper\n');
// Resolve and validate .env path
const envPath = path.resolve(process.cwd(), argv.env);
if (!fs.existsSync(envPath)) {
throw new Error(`Environment file not found: ${envPath}`);
}
console.log(`š Configuration:`);
console.log(` Environment: ${envPath}`);
console.log(` Platform: ${argv.platform}`);
console.log(` Format: ${argv.format}`);
console.log(` Sign: ${argv.sign ? 'Yes' : 'No'}`);
console.log(` Notarize: ${argv.notarize ? 'Yes' : 'No'}`);
console.log(` Terminal Script: ${argv.terminalScript ? 'Yes' : 'No'}`);
console.log('');
// Load configuration
console.log('š Loading configuration...');
const config = loadConfig(envPath);
console.log(` App: ${config.app.displayName} (${config.app.name})`);
console.log(` Version: ${config.version.full}`);
console.log(` Project: ${config.paths.projectRoot}`);
console.log('');
// Determine output directory
const outputDir = argv.output
? path.resolve(process.cwd(), argv.output)
: path.join(config.paths.projectRoot, 'executable-wrapper-app-releases');
config.paths.outputDir = outputDir;
let finalOutputPath;
// Handle universal builds specially
if (argv.platform === 'macos-universal') {
if (argv.format === 'app') {
// Universal .app builds both architectures internally
finalOutputPath = await createUniversalApp(config, argv.terminalScript);
} else if (argv.format === 'pkg') {
// Universal .pkg builds both architectures internally
finalOutputPath = await createUniversalPkg(config, argv.terminalScript);
} else {
throw new Error('Universal builds only support --format app or --format pkg');
}
} else {
// Normal single-architecture build
console.log('šØ Building executable...');
const executablePath = await buildExecutable(config, argv.platform);
console.log(` ā
Built: ${executablePath}\n`);
finalOutputPath = executablePath;
// Create app/pkg for macOS
if (argv.platform.startsWith('macos')) {
if (argv.format === 'app') {
console.log('š¦ Creating .app bundle...');
finalOutputPath = await createMacOSApp(config, executablePath, argv.platform);
console.log(` ā
Created: ${finalOutputPath}\n`);
} else if (argv.format === 'pkg') {
console.log('š¦ Creating .pkg installer...');
finalOutputPath = await createMacOSPkg(config, executablePath, argv.platform);
console.log(` ā
Created: ${finalOutputPath}\n`);
}
}
}
// Sign if requested
if (argv.sign) {
if (argv.platform.startsWith('macos')) {
console.log('š Signing...');
const signType = argv.platform === 'macos-universal' ? 'app' : argv.format;
await signMacOS(config, finalOutputPath, signType);
console.log(` ā
Signed\n`);
} else if (argv.platform.startsWith('win')) {
console.log('š Signing...');
await signWindows(config, finalOutputPath);
console.log(` ā
Signed\n`);
}
}
// Notarize if requested (macOS only)
if (argv.notarize && argv.platform.startsWith('macos')) {
if (!argv.sign) {
console.warn('ā ļø Notarization requires signing. Skipping notarization.\n');
} else {
console.log('š Notarizing...');
const notarizeType = argv.platform === 'macos-universal' ? 'app' : argv.format;
await notarizeMacOS(config, finalOutputPath, notarizeType);
console.log(` ā
Notarized\n`);
}
}
console.log('š Done!\n');
console.log(`š Output: ${finalOutputPath}`);
console.log('');
} catch (error) {
console.error('\nā Error:', error.message);
if (process.env.DEBUG) {
console.error(error.stack);
}
process.exit(1);
}
}
// Run
if (require.main === module) {
main();
}
module.exports = { main };