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

360 lines (308 loc) โ€ข 13.8 kB
const path = require('path'); const fs = require('fs-extra'); const chalk = require('chalk'); // Import modular components const { validateConfig, validatePaths, sanitizeConfig, validateSigningConfig } = require('./validation'); const { getTempDir, cleanup, readPackageJson, validateRequiredTools, getSystemInfo } = require('./utils'); const { findCodeSigningCertificates } = require('./certificates'); const { createMsixPackage: createPackage, signMsixPackage: signPackage, preparePackageDirectory, validatePackage } = require('./package'); const { CONSTANTS, MSIXError, ValidationError } = require('./constants'); /** * Creates an MSIX package from a Node.js application * * @param {Object} config - Configuration object * @param {string} config.inputPath - Path to the Node.js application directory * @param {string} config.outputPath - Output directory for the MSIX package * @param {string} config.appName - Application name (required) * @param {string} config.publisher - Publisher name in format "CN=PublisherName" (required) * @param {string} config.version - Application version in format "x.x.x.x" (optional, defaults to "1.0.0.0") * @param {string} config.description - Application description (optional) * @param {string} config.executable - Main executable file (optional, defaults to "node.exe") * @param {string} config.icon - Path to application icon file (optional) * @param {string} config.architecture - Target architecture (optional, defaults to "x64") * @param {string} config.displayName - Display name shown to users (optional, defaults to appName) * @param {string} config.packageName - Package identity name (optional, auto-generated if not provided) * @param {Array<string>} config.capabilities - Application capabilities (optional, defaults to ["internetClient"]) * @param {string} config.backgroundColor - Background color for tiles (optional, defaults to "transparent") * * @param {boolean} config.sign - Whether to sign the MSIX package (optional, defaults to true) * @param {string} config.certificateThumbprint - Certificate thumbprint for signing (optional) * @param {string} config.certificateSubject - Certificate subject name for signing (optional) * @param {string} config.certificatePath - Path to PFX certificate file (optional) * @param {string} config.certificatePassword - Password for PFX certificate (optional) * @param {string} config.timestampUrl - Timestamp server URL (optional) * * @returns {Promise<Object>} Result object containing package information * @throws {ValidationError} When configuration is invalid * @throws {MSIXError} When package creation fails * * @example * const { createMsixPackage } = require('node-msix'); * * const result = await createMsixPackage({ * inputPath: './my-node-app', * outputPath: './dist', * appName: 'My Node App', * publisher: 'CN=My Company', * version: '1.0.0.0' * }); * * console.log('Package created:', result.packagePath); */ async function createMsixPackage(config) { const cleanupPaths = []; try { // Validate system requirements console.log(chalk.blue('๐Ÿ” Validating system requirements...')); validateRequiredTools(); // Validate and sanitize configuration console.log(chalk.blue('โœ… Validating configuration...')); validateConfig(config); await validatePaths(config); const sanitizedConfig = sanitizeConfig(config); // Set default signing configuration const signingConfig = { sign: sanitizedConfig.sign !== false, // Default to true certificateThumbprint: sanitizedConfig.certificateThumbprint, certificateSubject: sanitizedConfig.certificateSubject, certificatePath: sanitizedConfig.certificatePath, certificatePassword: sanitizedConfig.certificatePassword, timestampUrl: sanitizedConfig.timestampUrl }; validateSigningConfig(signingConfig); // Read package.json from input directory console.log(chalk.blue('๐Ÿ“– Reading package.json...')); const packageJson = await readPackageJson(sanitizedConfig.inputPath); // Create temporary directory const tempDir = getTempDir('msix-package'); cleanupPaths.push(tempDir); // Prepare package directory const packageDir = await preparePackageDirectory(sanitizedConfig, tempDir, packageJson); // Validate prepared package console.log(chalk.blue('๐Ÿ” Validating package contents...')); const validation = await validatePackage(packageDir); if (!validation.isValid) { throw new ValidationError('package contents', 'valid package structure', validation.issues.join('; ')); } if (validation.warnings.length > 0) { console.log(chalk.yellow('โš ๏ธ Package validation warnings:')); validation.warnings.forEach(warning => { console.log(chalk.yellow(` โ€ข ${warning}`)); }); } console.log(chalk.green(`๐Ÿ“ฆ Package size: ${validation.packageSize}`)); // Generate output filename const packageName = sanitizedConfig.packageName.replace(/[^a-zA-Z0-9.-]/g, ''); const version = sanitizedConfig.version; const architecture = sanitizedConfig.architecture; const outputFilename = `${packageName}_${version}_${architecture}.msix`; const outputPath = path.join(sanitizedConfig.outputPath, outputFilename); // Create MSIX package console.log(chalk.blue('๐Ÿ—๏ธ Creating MSIX package...')); await createPackage(packageDir, outputPath); let signedSuccessfully = false; // Sign the package if requested if (signingConfig.sign) { try { console.log(chalk.blue('๐Ÿ” Signing MSIX package...')); signedSuccessfully = await signPackage(outputPath, signingConfig); } catch (error) { console.log(chalk.yellow(`โš ๏ธ Package signing failed: ${error.message}`)); console.log(chalk.yellow('The unsigned package has still been created successfully.')); } } // Clean up temporary files console.log(chalk.blue('๐Ÿงน Cleaning up temporary files...')); await cleanup(cleanupPaths); const result = { success: true, packagePath: outputPath, packageSize: validation.packageSize, signed: signedSuccessfully, config: { appName: sanitizedConfig.appName, version: sanitizedConfig.version, publisher: sanitizedConfig.publisher, architecture: sanitizedConfig.architecture, packageName: sanitizedConfig.packageName }, systemInfo: getSystemInfo(), timestamp: new Date().toISOString() }; console.log(chalk.green('๐ŸŽ‰ MSIX package created successfully!')); console.log(chalk.green(`๐Ÿ“ Location: ${outputPath}`)); if (signedSuccessfully) { console.log(chalk.green('๐Ÿ” Package signed successfully')); } else if (signingConfig.sign) { console.log(chalk.yellow('โš ๏ธ Package created but not signed')); } return result; } catch (error) { // Clean up on error if (cleanupPaths.length > 0) { try { await cleanup(cleanupPaths); } catch (cleanupError) { console.warn(chalk.yellow(`Warning: Cleanup failed: ${cleanupError.message}`)); } } // Re-throw the original error throw error; } } /** * Signs an existing MSIX package * * @param {string} packagePath - Path to the MSIX package to sign * @param {Object} signingConfig - Signing configuration * @param {string} signingConfig.certificateThumbprint - Certificate thumbprint (optional) * @param {string} signingConfig.certificateSubject - Certificate subject (optional) * @param {string} signingConfig.certificatePath - Path to PFX certificate (optional) * @param {string} signingConfig.certificatePassword - PFX password (optional) * @param {string} signingConfig.timestampUrl - Timestamp server URL (optional) * * @returns {Promise<boolean>} True if signing was successful * @throws {ValidationError} When signing configuration is invalid * @throws {SigningError} When signing fails * * @example * const { signMsixPackage } = require('node-msix'); * * const success = await signMsixPackage('./dist/MyApp.msix', { * certificatePath: './certificates/mycert.pfx', * certificatePassword: 'mypassword' * }); */ async function signMsixPackage(packagePath, signingConfig = {}) { try { console.log(chalk.blue('๐Ÿ” Signing MSIX package...')); // Validate inputs if (!packagePath || !(await fs.pathExists(packagePath))) { throw new ValidationError('packagePath', 'existing MSIX file', packagePath); } // Default signing configuration const config = { sign: true, timestampUrl: signingConfig.timestampUrl || CONSTANTS.DEFAULT_TIMESTAMP_URL, ...signingConfig }; validateSigningConfig(config); const result = await signPackage(packagePath, config); if (result) { console.log(chalk.green('๐Ÿ” Package signed successfully')); } return result; } catch (error) { console.error(chalk.red(`โŒ Signing failed: ${error.message}`)); throw error; } } /** * Lists available code signing certificates on the system * * @returns {Promise<Array>} Array of certificate objects * @throws {CertificateError} When certificate enumeration fails * * @example * const { listCertificates } = require('node-msix'); * * const certificates = await listCertificates(); * certificates.forEach(cert => { * console.log(`Subject: ${cert.subject}`); * console.log(`Thumbprint: ${cert.thumbprint}`); * console.log(`Valid: ${cert.isValid}`); * }); */ async function listCertificates() { try { console.log(chalk.blue('๐Ÿ” Searching for code signing certificates...')); const certificates = await findCodeSigningCertificates(); if (certificates.length === 0) { console.log(chalk.yellow('โš ๏ธ No code signing certificates found')); } else { console.log(chalk.green(`โœ… Found ${certificates.length} certificate(s)`)); } return certificates; } catch (error) { console.error(chalk.red(`โŒ Certificate search failed: ${error.message}`)); throw error; } } /** * Initializes a new MSIX configuration file * * @param {string} targetDir - Directory to create config file in (optional, defaults to current directory) * @param {Object} options - Configuration options (optional) * @returns {Promise<string>} Path to created config file * * @example * const { initConfig } = require('node-msix'); * * const configPath = await initConfig('./my-app', { * appName: 'My Application', * publisher: 'CN=My Company' * }); */ async function initConfig(targetDir = process.cwd(), options = {}) { try { const configPath = path.join(targetDir, CONSTANTS.CONFIG_FILENAME); // Check if config already exists if (await fs.pathExists(configPath)) { throw new ValidationError('config file', 'non-existing file', 'Config file already exists'); } // Read package.json for defaults if it exists let packageJson = {}; const packageJsonPath = path.join(targetDir, 'package.json'); if (await fs.pathExists(packageJsonPath)) { packageJson = await readPackageJson(targetDir); } // Create default configuration const config = { appName: options.appName || packageJson.name || path.basename(targetDir), publisher: options.publisher || 'CN=DefaultPublisher', version: options.version || (packageJson.version ? `${packageJson.version}.0` : '1.0.0.0'), description: options.description || packageJson.description || 'Node.js application packaged as MSIX', executable: options.executable || CONSTANTS.DEFAULT_EXECUTABLE, architecture: options.architecture || CONSTANTS.DEFAULT_ARCHITECTURE, capabilities: options.capabilities || CONSTANTS.DEFAULT_CAPABILITIES, sign: options.sign !== false, // Paths (relative to config file) inputPath: options.inputPath || '.', outputPath: options.outputPath || './dist', icon: options.icon || null, // Optional signing configuration certificateThumbprint: options.certificateThumbprint || null, certificateSubject: options.certificateSubject || null, certificatePath: options.certificatePath || null, certificatePassword: options.certificatePassword || null, // Generated info _generated: { version: require('../package.json').version, timestamp: new Date().toISOString(), node: process.version, platform: process.platform } }; // Write config file await fs.writeFile(configPath, JSON.stringify(config, null, 2)); console.log(chalk.green(`โœ… Configuration file created: ${configPath}`)); console.log(chalk.blue('๐Ÿ’ก Edit the configuration file and then run the packaging command')); return configPath; } catch (error) { console.error(chalk.red(`โŒ Config initialization failed: ${error.message}`)); throw error; } } // Export main functions module.exports = { createMsixPackage, signMsixPackage, listCertificates, initConfig, // For backward compatibility findCodeSigningCertificates: listCertificates, // Version info version: require('../package.json').version, // Constants for external use CONSTANTS };