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
JavaScript
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
};