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
189 lines (156 loc) • 6.56 kB
JavaScript
const path = require('path');
const fs = require('fs-extra');
const { CONSTANTS, ValidationError } = require('./constants');
/**
* Validates configuration object for MSIX package creation
* @param {Object} config - Configuration object to validate
* @throws {ValidationError} When validation fails
*/
function validateConfig(config) {
const errors = [];
// Required fields
const requiredFields = [
{ field: 'inputPath', type: 'string' },
{ field: 'outputPath', type: 'string' },
{ field: 'appName', type: 'string' },
{ field: 'publisher', type: 'string' }
];
for (const { field, type } of requiredFields) {
if (!config[field]) {
errors.push(`Missing required field: ${field}`);
} else if (typeof config[field] !== type) {
errors.push(`Field ${field} must be of type ${type}, got ${typeof config[field]}`);
}
}
// Validate publisher format
if (config.publisher && !CONSTANTS.PUBLISHER_PATTERN.test(config.publisher)) {
errors.push('Publisher must start with "CN=" (e.g., "CN=MyCompany")');
}
// Validate version format
if (config.version) {
if (!CONSTANTS.VERSION_PATTERN.test(config.version)) {
errors.push('Version must be in format x.x.x.x (e.g., "1.0.0.0")');
}
}
// Validate architecture
if (config.architecture && !CONSTANTS.SUPPORTED_ARCHITECTURES.includes(config.architecture)) {
errors.push(`Architecture must be one of: ${CONSTANTS.SUPPORTED_ARCHITECTURES.join(', ')}`);
}
// Validate package name length
if (config.packageName && config.packageName.length > CONSTANTS.MAX_PACKAGE_NAME_LENGTH) {
errors.push(`Package name must be ${CONSTANTS.MAX_PACKAGE_NAME_LENGTH} characters or less`);
}
// Validate capabilities
if (config.capabilities && !Array.isArray(config.capabilities)) {
errors.push('Capabilities must be an array');
}
if (errors.length > 0) {
throw new ValidationError('config', 'valid configuration object', errors.join('; '));
}
}
/**
* Validates that required paths exist
* @param {Object} config - Configuration object
* @throws {ValidationError} When paths don't exist
*/
async function validatePaths(config) {
const errors = [];
// Check input path exists
if (!(await fs.pathExists(config.inputPath))) {
errors.push(`Input path does not exist: ${config.inputPath}`);
} else {
// Check if it's a directory
const stats = await fs.stat(config.inputPath);
if (!stats.isDirectory()) {
errors.push(`Input path must be a directory: ${config.inputPath}`);
}
// Check for package.json in input directory
const packageJsonPath = path.join(config.inputPath, 'package.json');
if (!(await fs.pathExists(packageJsonPath))) {
errors.push(`No package.json found in input directory: ${config.inputPath}`);
}
}
// Validate icon path if provided
if (config.icon && !(await fs.pathExists(config.icon))) {
errors.push(`Icon file does not exist: ${config.icon}`);
}
if (errors.length > 0) {
throw new ValidationError('paths', 'existing paths', errors.join('; '));
}
}
/**
* Sanitizes configuration values
* @param {Object} config - Configuration object to sanitize
* @returns {Object} Sanitized configuration
*/
function sanitizeConfig(config) {
const sanitized = { ...config };
// Set defaults
sanitized.version = sanitized.version || CONSTANTS.DEFAULT_VERSION;
sanitized.architecture = sanitized.architecture || CONSTANTS.DEFAULT_ARCHITECTURE;
sanitized.executable = sanitized.executable || CONSTANTS.DEFAULT_EXECUTABLE;
sanitized.timestampUrl = sanitized.timestampUrl || CONSTANTS.DEFAULT_TIMESTAMP_URL;
sanitized.capabilities = sanitized.capabilities || CONSTANTS.DEFAULT_CAPABILITIES;
// Sanitize strings
sanitized.appName = sanitized.appName.trim();
sanitized.publisher = sanitized.publisher.trim();
// Generate package name if not provided
if (!sanitized.packageName) {
const publisherName = sanitized.publisher.replace(/^CN=/, '').split(',')[0].trim();
const appNameSafe = sanitized.appName.replace(/[^a-zA-Z0-9]/g, '');
sanitized.packageName = `${publisherName}.${appNameSafe}`.substring(0, CONSTANTS.MAX_PACKAGE_NAME_LENGTH);
}
// Generate display name if not provided
sanitized.displayName = sanitized.displayName || sanitized.appName;
// Set description if not provided
sanitized.description = sanitized.description || `${sanitized.appName} - Node.js application packaged as MSIX`;
// Set build options
sanitized.skipBuild = sanitized.skipBuild || false;
sanitized.installDevDeps = sanitized.installDevDeps !== false; // Default to true
// Normalize paths
sanitized.inputPath = path.resolve(sanitized.inputPath);
sanitized.outputPath = path.resolve(sanitized.outputPath);
if (sanitized.icon) {
sanitized.icon = path.resolve(sanitized.icon);
}
return sanitized;
}
/**
* Validates certificate configuration
* @param {Object} signingConfig - Signing configuration
* @throws {ValidationError} When signing configuration is invalid
*/
function validateSigningConfig(signingConfig) {
if (!signingConfig.sign) {
return; // No validation needed if signing is disabled
}
const errors = [];
// At least one certificate identification method should be provided
const hasCertId = signingConfig.certificateThumbprint ||
signingConfig.certificateSubject ||
signingConfig.certificatePath;
if (!hasCertId) {
// This is okay - we'll auto-discover certificates
return;
}
// If certificatePath is provided, check if password is needed
if (signingConfig.certificatePath && !signingConfig.certificatePassword) {
// Warning but not error - some PFX files don't need passwords
}
// Validate thumbprint format if provided
if (signingConfig.certificateThumbprint) {
const thumbprintPattern = /^[A-Fa-f0-9]{40}$/;
if (!thumbprintPattern.test(signingConfig.certificateThumbprint.replace(/\s/g, ''))) {
errors.push('Certificate thumbprint must be a 40-character hexadecimal string');
}
}
if (errors.length > 0) {
throw new ValidationError('signingConfig', 'valid signing configuration', errors.join('; '));
}
}
module.exports = {
validateConfig,
validatePaths,
sanitizeConfig,
validateSigningConfig
};