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
278 lines (250 loc) • 8.19 kB
JavaScript
const path = require('path');
const fs = require('fs-extra');
const { execSync } = require('child_process');
const https = require('https');
const { CONSTANTS, ToolNotFoundError, MSIXError } = require('./constants');
/**
* Executes a command safely with error handling
* @param {string} command - Command to execute
* @param {Object} options - Execution options
* @returns {string} Command output
* @throws {MSIXError} When command execution fails
*/
function executeCommand(command, options = {}) {
try {
const result = execSync(command, {
encoding: 'utf8',
stdio: options.silent ? 'pipe' : 'inherit',
windowsHide: true,
...options
});
return result;
} catch (error) {
const errorMessage = `Command failed: ${command}\n${error.message}`;
throw new MSIXError(errorMessage);
}
}
/**
* Finds Windows SDK tools
* @returns {Object} Object containing paths to makeappx and signtool
* @throws {ToolNotFoundError} When tools are not found
*/
function findWindowsSDKTools() {
const possiblePaths = CONSTANTS.WINDOWS_SDK_PATHS;
for (const basePath of possiblePaths) {
try {
const makeappxPath = path.join(basePath, 'makeappx.exe');
const signtoolPath = path.join(basePath, 'signtool.exe');
if (fs.existsSync(makeappxPath) && fs.existsSync(signtoolPath)) {
return {
makeappx: makeappxPath,
signtool: signtoolPath
};
}
} catch (error) {
// Continue searching
}
}
throw new ToolNotFoundError('Windows SDK tools (makeappx.exe, signtool.exe) not found. Please install Windows SDK.');
}
/**
* Generates a unique temporary directory name
* @param {string} prefix - Prefix for the temporary directory
* @returns {string} Temporary directory path
*/
function getTempDir(prefix = 'msix-temp') {
const tempBase = process.env.TEMP || process.env.TMP || path.join(process.cwd(), 'temp');
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 8);
return path.join(tempBase, `${prefix}-${timestamp}-${random}`);
}
/**
* Cleans up temporary directories and files
* @param {string[]} paths - Array of paths to clean up
*/
async function cleanup(paths) {
for (const cleanupPath of paths) {
try {
if (await fs.pathExists(cleanupPath)) {
await fs.remove(cleanupPath);
console.log(`Cleaned up: ${cleanupPath}`);
}
} catch (error) {
console.warn(`Warning: Could not clean up ${cleanupPath}: ${error.message}`);
}
}
}
/**
* Creates a directory structure ensuring all parent directories exist
* @param {string} dirPath - Directory path to create
*/
async function ensureDir(dirPath) {
try {
await fs.ensureDir(dirPath);
} catch (error) {
throw new MSIXError(`Failed to create directory ${dirPath}: ${error.message}`);
}
}
/**
* Copies files with progress indication
* @param {string} src - Source path
* @param {string} dest - Destination path
* @param {boolean} showProgress - Whether to show progress
*/
async function copyFiles(src, dest, showProgress = true) {
try {
if (showProgress) {
console.log(`Copying files from ${src} to ${dest}...`);
}
await fs.copy(src, dest, {
overwrite: true,
errorOnExist: false,
filter: (src) => {
// Skip node_modules and common temporary directories
const relativePath = path.relative(src, src);
return !CONSTANTS.COPY_EXCLUDE_PATTERNS.some(pattern =>
relativePath.includes(pattern)
);
}
});
if (showProgress) {
console.log('Files copied successfully');
}
} catch (error) {
throw new MSIXError(`Failed to copy files: ${error.message}`);
}
}
/**
* Reads and parses package.json from a directory
* @param {string} directoryPath - Path to directory containing package.json
* @returns {Object} Parsed package.json content
* @throws {MSIXError} When package.json cannot be read or parsed
*/
async function readPackageJson(directoryPath) {
const packageJsonPath = path.join(directoryPath, 'package.json');
try {
const content = await fs.readFile(packageJsonPath, 'utf8');
return JSON.parse(content);
} catch (error) {
throw new MSIXError(`Failed to read package.json from ${directoryPath}: ${error.message}`);
}
}
/**
* Formats file size for human reading
* @param {number} bytes - Size in bytes
* @returns {string} Formatted size string
*/
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* Validates that all required tools are available
* @throws {ToolNotFoundError} When required tools are missing
*/
function validateRequiredTools() {
try {
// Check for Node.js
execSync('node --version', { stdio: 'pipe' });
} catch {
throw new ToolNotFoundError('Node.js is required but not found in PATH');
}
try {
// Check for npm
execSync('npm --version', { stdio: 'pipe' });
} catch {
throw new ToolNotFoundError('npm is required but not found in PATH');
}
// Check for Windows SDK tools
findWindowsSDKTools(); // This will throw if not found
}
/**
* Gets system information for debugging
* @returns {Object} System information
*/
function getSystemInfo() {
try {
const nodeVersion = execSync('node --version', { encoding: 'utf8' }).trim();
const npmVersion = execSync('npm --version', { encoding: 'utf8' }).trim();
const osVersion = require('os').release();
const platform = process.platform;
const arch = process.arch;
return {
node: nodeVersion,
npm: npmVersion,
os: osVersion,
platform,
architecture: arch,
timestamp: new Date().toISOString()
};
} catch (error) {
return {
error: 'Could not gather system information',
timestamp: new Date().toISOString()
};
}
}
/**
* Gets the latest Node.js LTS version dynamically
* @returns {Promise<string>} Latest LTS version (e.g., 'v22.8.0')
*/
async function getLatestNodeVersion() {
try {
return new Promise((resolve, reject) => {
const options = {
hostname: 'nodejs.org',
path: '/dist/index.json',
method: 'GET',
timeout: 10000
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const releases = JSON.parse(data);
// Find latest LTS version
const latestLTS = releases.find(release => release.lts);
if (latestLTS) {
resolve(latestLTS.version);
} else {
// Fallback to latest stable
resolve(releases[0].version);
}
} catch (parseError) {
console.warn('Warning: Could not parse Node.js releases, using fallback version');
resolve('v22.8.0'); // Fallback version
}
});
});
req.on('error', (error) => {
console.warn('Warning: Could not fetch latest Node.js version, using fallback');
resolve('v22.8.0'); // Fallback version
});
req.on('timeout', () => {
console.warn('Warning: Timeout fetching Node.js version, using fallback');
resolve('v22.8.0'); // Fallback version
});
req.end();
});
} catch (error) {
console.warn('Warning: Error getting Node.js version, using fallback');
return 'v22.8.0'; // Fallback version
}
}
module.exports = {
executeCommand,
findWindowsSDKTools,
getTempDir,
cleanup,
ensureDir,
copyFiles,
readPackageJson,
formatFileSize,
validateRequiredTools,
getSystemInfo,
getLatestNodeVersion
};