UNPKG

node-msix-packager

Version:

A utility to create MSIX packages from Node.js applications with MCP server support, Node.js Single Executable Application (SEA) bundling using @vercel/ncc and postject, and enhanced build options

304 lines (252 loc) 10.5 kB
const path = require('path'); const fs = require('fs-extra'); const { executeCommand } = require('./utils'); /** * Creates a Single Executable Application (SEA) using Node.js built-in capabilities with NCC bundling * @param {string} packageDir - Directory containing the package * @param {Object} config - Configuration object * @param {Object} packageJson - package.json content * @returns {Promise<boolean>} Success status */ async function createSingleExecutableApp(packageDir, config, packageJson) { try { console.log('🔨 Creating Single Executable Application (SEA) with NCC bundling...'); const appName = config.appName || 'app'; const executableName = `${appName.replace(/[^a-zA-Z0-9.-]/g, '_')}.exe`; const executablePath = path.join(packageDir, executableName); // Step 1: Install NCC if not available console.log('📦 Installing @vercel/ncc...'); try { executeCommand('npm install @vercel/ncc --no-save', { cwd: packageDir }); } catch (nccError) { console.log('Installing NCC globally...'); executeCommand('npm install -g @vercel/ncc', { cwd: packageDir }); } // Step 2: Create entry point that handles both SEA and regular execution const mainScript = packageJson.main || 'index.js'; const entryContent = `#!/usr/bin/env node // SEA Entry Point for ${config.displayName || config.appName} console.log('🚀 Starting ${config.displayName || config.appName}...'); // Check if running as SEA let isSEA = false; try { const { sea } = require('node:sea'); isSEA = sea.isSea(); if (isSEA) { console.log('Running in SEA mode'); // Set process title process.title = '${config.displayName || config.appName}'; } } catch (err) { // SEA module not available, running in regular Node.js console.log('Running in development mode'); } // Set working directory to executable directory try { const path = require('path'); const execDir = path.dirname(process.execPath); process.chdir(execDir); } catch (err) { console.warn('Could not change working directory:', err.message); } // Load the main application try { require('./${mainScript}'); } catch (error) { console.error('❌ Failed to start application:', error.message); console.error(error.stack); process.exit(1); } `; const entryPath = path.join(packageDir, 'sea-entry.js'); await fs.writeFile(entryPath, entryContent); // Step 3: Bundle with NCC console.log('📦 Bundling application with NCC...'); const bundleCommand = `npx ncc build sea-entry.js -o sea-dist --minify --no-source-map-register`; executeCommand(bundleCommand, { cwd: packageDir }); const bundledPath = path.join(packageDir, 'sea-dist', 'index.js'); if (!await fs.pathExists(bundledPath)) { throw new Error('NCC bundling failed - bundled file not found'); } // Step 4: Create SEA configuration const seaConfig = { main: 'sea-dist/index.js', output: 'sea-prep.blob', disableExperimentalSEAWarning: true, useSnapshot: false, useCodeCache: true }; const seaConfigPath = path.join(packageDir, 'sea-config.json'); await fs.writeJson(seaConfigPath, seaConfig, { spaces: 2 }); // Step 5: Generate SEA blob console.log('🗜️ Generating SEA blob...'); const originalCwd = process.cwd(); process.chdir(packageDir); try { executeCommand('node --experimental-sea-config sea-config.json'); const blobPath = path.join(packageDir, 'sea-prep.blob'); if (!await fs.pathExists(blobPath)) { throw new Error('SEA blob generation failed'); } console.log('✅ SEA blob created successfully'); // Step 6: Copy Node.js executable console.log('📄 Creating executable base...'); const nodeBinaryPath = await getNodeBinaryPath(); await fs.copyFile(nodeBinaryPath, executablePath); if (!await fs.pathExists(executablePath)) { throw new Error('Failed to create executable base'); } // Step 7: Remove signature (Windows) console.log('🔓 Removing executable signature...'); try { const { findWindowsSDKTools } = require('./utils'); const tools = findWindowsSDKTools(); const removeSignCommand = `"${tools.signtool}" remove /s "${executableName}"`; executeCommand(removeSignCommand); } catch (sigError) { console.warn('⚠️ Could not remove signature (this is usually fine)'); } // Step 8: Inject blob with postject console.log('💉 Injecting SEA blob into executable...'); const postjectCommand = `npx postject "${executableName}" NODE_SEA_BLOB sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2`; executeCommand(postjectCommand); // Step 9: Sign the executable await signSEAExecutable(executablePath, config); // Step 10: Cleanup temporary files await fs.remove(entryPath); await fs.remove(path.join(packageDir, 'sea-dist')); await fs.remove(seaConfigPath); await fs.remove(blobPath); // Update config config.executable = executableName; delete config.executableArgs; console.log(`✅ SEA executable created: ${executableName}`); return true; } finally { process.chdir(originalCwd); } } catch (error) { console.warn(`⚠️ SEA creation failed: ${error.message}`); console.log('Falling back to PKG...'); return false; } } /** * Gets the Node.js binary path for the current platform * @returns {Promise<string>} Path to Node.js binary */ async function getNodeBinaryPath() { return process.execPath; } /** * Signs the SEA executable * @param {string} executablePath - Path to executable * @param {Object} config - Configuration object */ async function signSEAExecutable(executablePath, config) { try { const { determineSigningMethod } = require('./certificates'); const { findWindowsSDKTools } = require('./utils'); const signingMethod = await determineSigningMethod(config); if (!signingMethod) { console.log('No signing configuration found, skipping...'); return; } const tools = findWindowsSDKTools(); let signCommand; if (signingMethod.method === 'pfx') { const { pfxPath, password, timestampUrl } = signingMethod.parameters; signCommand = `"${tools.signtool}" sign /f "${pfxPath}" /p "${password}" /tr "${timestampUrl}" /td SHA256 /fd SHA256 "${executablePath}"`; } else if (signingMethod.method === 'store') { const { thumbprint, store, timestampUrl } = signingMethod.parameters; const storeLocation = store.toLowerCase() === 'localmachine' ? '/sm' : ''; signCommand = `"${tools.signtool}" sign /sha1 "${thumbprint}" /s "My" ${storeLocation} /fd SHA256 "${executablePath}"`; } if (signCommand) { executeCommand(signCommand); console.log('✅ SEA executable signed successfully'); } } catch (error) { console.warn(`Warning: Could not sign SEA executable: ${error.message}`); } } /** * Creates a PKG fallback executable * @param {string} packageDir - Package directory path * @param {Object} config - Configuration object * @param {Object} packageJson - package.json content */ async function createFallbackLauncher(packageDir, config, packageJson) { try { console.log('📦 Creating PKG fallback executable...'); const executableName = `${config.appName || 'app'}.exe`; const executablePath = path.join(packageDir, executableName); const mainScript = packageJson.main || 'index.js'; // Create launcher script with proper directory handling const launcherContent = `#!/usr/bin/env node // PKG Launcher for ${config.displayName || config.appName} console.log('🚀 Starting ${config.displayName || config.appName}...'); // Set working directory - handle PKG snapshot case try { if (__dirname.includes('snapshot')) { // Running from PKG, use the executable's directory const path = require('path'); const execDir = path.dirname(process.execPath); process.chdir(execDir); } else { // Running in development mode process.chdir(__dirname); } } catch (err) { console.warn('Could not change working directory:', err.message); } // Load the main application try { require('./${mainScript}'); } catch (error) { console.error('❌ Failed to start application:', error.message); console.error(error.stack); process.exit(1); } `; const launcherScript = path.join(packageDir, 'launcher.js'); await fs.writeFile(launcherScript, launcherContent); // Create PKG configuration const pkgConfig = { name: config.appName || 'app', version: packageJson.version || '1.0.0', main: 'launcher.js', bin: 'launcher.js', pkg: { scripts: [mainScript], targets: ['node18-win-x64'], outputPath: '.' }, dependencies: packageJson.dependencies || {} }; const pkgConfigPath = path.join(packageDir, 'pkg-config.json'); await fs.writeJson(pkgConfigPath, pkgConfig, { spaces: 2 }); // Create executable with PKG const finalExeName = path.basename(executablePath, '.exe'); const pkgCommand = `npx pkg launcher.js --target node18-win-x64 --output "${finalExeName}" --config pkg-config.json`; executeCommand(pkgCommand, { cwd: packageDir }); // Verify executable was created if (await fs.pathExists(executablePath)) { console.log(`✅ Created executable with PKG: ${executableName}`); // Clean up temporary files await fs.remove(launcherScript); await fs.remove(pkgConfigPath); // Update config config.executable = executableName; delete config.executableArgs; return true; } } catch (pkgError) { console.warn(`⚠️ PKG fallback failed: ${pkgError.message}`); return false; } } module.exports = { createSingleExecutableApp, createFallbackLauncher };