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
800 lines (687 loc) • 29.7 kB
JavaScript
const path = require('path');
const fs = require('fs-extra');
const { executeCommand, findWindowsSDKTools, formatFileSize, getLatestNodeVersion } = require('./utils');
const { determineSigningMethod } = require('./certificates');
const { generateManifest, createDefaultAssets, generateResourceConfig } = require('./manifest');
const { CONSTANTS, MSIXError, SigningError } = require('./constants');
/**
* Creates an MSIX package
* @param {string} packageDir - Directory containing package contents
* @param {string} outputPath - Output MSIX file path
* @returns {string} Path to created MSIX file
*/
async function createMsixPackage(packageDir, outputPath) {
try {
console.log('Creating MSIX package...');
const tools = findWindowsSDKTools();
// Ensure output directory exists
const outputDir = path.dirname(outputPath);
await fs.ensureDir(outputDir);
// Create the MSIX package using makeappx
const command = `"${tools.makeappx}" pack /d "${packageDir}" /p "${outputPath}" /overwrite`;
console.log('Running makeappx...');
executeCommand(command);
// Verify the package was created
if (!(await fs.pathExists(outputPath))) {
throw new MSIXError('MSIX package was not created successfully');
}
const stats = await fs.stat(outputPath);
console.log(`MSIX package created: ${outputPath} (${formatFileSize(stats.size)})`);
return outputPath;
} catch (error) {
throw new MSIXError(`Failed to create MSIX package: ${error.message}`);
}
}
/**
* Signs an MSIX package
* @param {string} packagePath - Path to MSIX package to sign
* @param {Object} signingConfig - Signing configuration
* @returns {boolean} True if signing was successful
*/
async function signMsixPackage(packagePath, signingConfig) {
try {
const signingMethod = await determineSigningMethod(signingConfig);
if (!signingMethod) {
console.log('Signing disabled, skipping...');
return false;
}
console.log(`Signing package using ${signingMethod.method} method...`);
const tools = findWindowsSDKTools();
let signCommand;
if (signingMethod.method === 'pfx') {
// Sign with PFX file
const { pfxPath, password, timestampUrl } = signingMethod.parameters;
signCommand = `"${tools.signtool}" sign /f "${pfxPath}" /p "${password}" /tr "${timestampUrl}" /td SHA256 /fd SHA256 "${packagePath}"`;
} else if (signingMethod.method === 'store') {
// Sign with certificate from store
const { thumbprint, store, timestampUrl } = signingMethod.parameters;
const storeLocation = store.toLowerCase() === 'localmachine' ? '/sm' : '';
console.log(`Using certificate from ${store} store with thumbprint: ${thumbprint}`);
// For test certificates, try the most basic signing approach first
if (timestampUrl && timestampUrl.length > 0) {
try {
signCommand = `"${tools.signtool}" sign /sha1 "${thumbprint}" /s "My" ${storeLocation} /fd SHA256 "${packagePath}"`;
console.log('Attempting basic signing without timestamp first...');
executeCommand(signCommand);
console.log('Basic signing successful, package signed without timestamp');
return true;
} catch (basicError) {
console.log('Basic signing failed, trying with timestamp...');
signCommand = `"${tools.signtool}" sign /sha1 "${thumbprint}" /s "My" ${storeLocation} /tr "${timestampUrl}" /td SHA256 /fd SHA256 "${packagePath}"`;
}
} else {
console.log('Signing without timestamp server...');
signCommand = `"${tools.signtool}" sign /sha1 "${thumbprint}" /s "My" ${storeLocation} /fd SHA256 "${packagePath}"`;
}
} else {
throw new SigningError(`Unknown signing method: ${signingMethod.method}`);
}
console.log('Running signtool...');
executeCommand(signCommand);
console.log('Package signed successfully');
return true;
} catch (error) {
throw new SigningError(`Failed to sign MSIX package: ${error.message}`);
}
}
/**
* Prepares the package directory structure with enhanced workflow
* This implements the proper Node.js app packaging workflow:
* 1. Copy source code to temp directory
* 2. Build the application (install deps, run build scripts)
* 3. Create executable structure
* 4. Create proper MSIX directory structure
* @param {Object} config - Configuration object
* @param {string} tempDir - Temporary directory for package preparation
* @param {Object} packageJson - package.json content
* @returns {string} Path to prepared package directory
*/
async function preparePackageDirectory(config, tempDir, packageJson) {
try {
console.log('🏗️ Preparing package directory structure...');
// Step 1: Create temporary directories for the enhanced workflow
const sourceDir = path.join(tempDir, 'source');
const buildDir = path.join(tempDir, 'build');
const packageDir = path.join(tempDir, 'package');
const assetsDir = path.join(packageDir, 'Assets');
await fs.ensureDir(sourceDir);
await fs.ensureDir(buildDir);
await fs.ensureDir(packageDir);
await fs.ensureDir(assetsDir);
// Step 2: Copy source code to temporary directory
console.log('📁 Copying source code to temporary directory...');
const { copyFiles } = require('./utils');
await copyFiles(config.inputPath, sourceDir, true);
// Step 3: Build the application (install dependencies and run build scripts)
console.log('🔧 Building the application...');
await buildApplication(sourceDir, buildDir, packageJson, config);
// Step 4: Create executable structure
console.log('⚙️ Creating executable structure...');
await createExecutableStructure(buildDir, packageDir, config, packageJson);
// Step 5: Generate MSIX directory structure
console.log('📦 Setting up MSIX directory structure...');
await setupMsixStructure(packageDir, assetsDir, config, packageJson);
console.log('✅ Package directory prepared successfully');
return packageDir;
} catch (error) {
throw new MSIXError(`Failed to prepare package directory: ${error.message}`);
}
}
/**
* Builds the Node.js application in the build directory
* @param {string} sourceDir - Source directory path
* @param {string} buildDir - Build directory path
* @param {Object} packageJson - package.json content
* @param {Object} config - Configuration object
*/
async function buildApplication(sourceDir, buildDir, packageJson, config) {
try {
// Copy source files to build directory with filtering
console.log('📋 Copying source files for build...');
await fs.copy(sourceDir, buildDir, {
overwrite: true,
errorOnExist: false,
filter: (src) => {
// Skip node_modules, build artifacts, and temporary files
const relativePath = path.relative(sourceDir, src);
const excludePatterns = [
'node_modules',
'.git',
'dist',
'build',
'.nyc_output',
'coverage',
'.vscode',
'.idea',
'*.log',
'.env'
];
return !excludePatterns.some(pattern =>
relativePath.includes(pattern) || relativePath.endsWith(pattern.replace('*', ''))
);
}
});
// Install dependencies (include dev dependencies if build script exists or configured)
const packageJsonPath = path.join(buildDir, 'package.json');
if (await fs.pathExists(packageJsonPath)) {
const needsDevDeps = config.installDevDeps && ((packageJson.scripts && packageJson.scripts.build) || config.installDevDeps === true);
if (needsDevDeps) {
console.log('📦 Installing all dependencies (including dev dependencies for build)...');
await installNodeDependencies(buildDir, false); // false = don't production-only
} else {
console.log('📦 Installing production dependencies...');
await installNodeDependencies(buildDir, true); // true = production-only
}
}
// Run build script if defined in package.json and not skipped
if (!config.skipBuild && packageJson.scripts && packageJson.scripts.build) {
console.log('🔨 Running build script...');
const originalCwd = process.cwd();
try {
process.chdir(buildDir);
executeCommand('npm run build');
} catch (buildError) {
console.warn(`⚠️ Build script failed: ${buildError.message}`);
// Check if this is a TypeScript build issue
if (buildError.message.includes('tsc') || buildError.message.includes('TypeScript')) {
console.log('🔧 Detected TypeScript build failure. Attempting to install TypeScript...');
try {
// Try to install TypeScript locally
executeCommand('npm install typescript --save-dev --silent');
console.log('📦 TypeScript installed. Retrying build...');
executeCommand('npm run build');
console.log('✅ Build succeeded after installing TypeScript');
} catch (retryError) {
console.warn(`⚠️ Build still failed after installing TypeScript: ${retryError.message}`);
console.log('📝 Continuing without build step. You may need to manually build your application before packaging.');
}
} else {
console.warn('📝 Continuing without build step. Ensure your application is pre-built if needed.');
}
} finally {
process.chdir(originalCwd);
}
} else if (config.skipBuild) {
console.log('⏭️ Skipping build step as requested in configuration');
}
console.log('✅ Application built successfully');
} catch (error) {
throw new MSIXError(`Failed to build application: ${error.message}`);
}
}
/**
* Creates the executable structure for the MSIX package
* @param {string} buildDir - Build directory path
* @param {string} packageDir - Package directory path
* @param {Object} config - Configuration object
* @param {Object} packageJson - package.json content
*/
async function createExecutableStructure(buildDir, packageDir, config, packageJson) {
try {
// Copy built application files to package directory
console.log('📁 Copying built application files...');
await fs.copy(buildDir, packageDir, {
overwrite: true,
errorOnExist: false,
filter: (src) => {
// Skip development files
const basename = path.basename(src);
const excludeFiles = [
'.gitignore',
'.env.example',
'README.md',
'CHANGELOG.md',
'LICENSE',
'tsconfig.json',
'jest.config.js',
'webpack.config.js'
];
return !excludeFiles.includes(basename);
}
});
// Ensure Node.js runtime is available
console.log('🟢 Ensuring Node.js runtime...');
await ensureNodeRuntime(packageDir, config);
// Create startup script if the main executable is node.exe
if (config.executable === 'node.exe' || config.executable.endsWith('node.exe')) {
await createNodeStartupScript(packageDir, packageJson, config);
}
// Try to create SEA (Single Executable Application) first
const { createSingleExecutableApp, createFallbackLauncher } = require('./sea-handler');
const seaSuccess = await createSingleExecutableApp(packageDir, config, packageJson);
// Create fallback launcher if SEA creation failed
if (!seaSuccess) {
console.log('📄 Creating fallback Node.js launcher...');
await createFallbackLauncher(packageDir, config, packageJson);
}
console.log('✅ Executable structure created successfully');
} catch (error) {
throw new MSIXError(`Failed to create executable structure: ${error.message}`);
}
}
/**
* Sets up the MSIX directory structure with manifests and assets
* @param {string} packageDir - Package directory path
* @param {string} assetsDir - Assets directory path
* @param {Object} config - Configuration object
* @param {Object} packageJson - package.json content
*/
async function setupMsixStructure(packageDir, assetsDir, config, packageJson) {
try {
// Generate AppxManifest.xml
console.log('📄 Generating AppxManifest.xml...');
const manifestContent = generateManifest(config, packageJson);
await fs.writeFile(path.join(packageDir, 'AppxManifest.xml'), manifestContent);
// Create package assets
console.log('🎨 Creating package assets...');
await createDefaultAssets(assetsDir, config.icon);
// Create MCP Server configuration
console.log('🔧 Creating MCP Server configuration...');
await createMcpServerConfig(assetsDir, config);
// Generate resource configuration
console.log('⚙️ Generating resource configuration...');
const resourceConfig = generateResourceConfig(config);
await fs.writeFile(path.join(packageDir, 'msix-resource-config.json'), resourceConfig);
// Create registry entries if needed
await createRegistryEntries(packageDir, config, packageJson);
console.log('✅ MSIX structure setup completed');
} catch (error) {
throw new MSIXError(`Failed to setup MSIX structure: ${error.message}`);
}
}
/**
* Creates a Node.js startup script
* @param {string} packageDir - Package directory path
* @param {Object} packageJson - package.json content
* @param {Object} config - Configuration object
*/
async function createNodeStartupScript(packageDir, packageJson, config) {
try {
const mainScript = packageJson.main || 'index.js';
const startScript = packageJson.scripts && packageJson.scripts.start;
// Create batch file to start the Node.js application
let batchContent;
if (startScript && !startScript.includes('node ')) {
// Use the start script if it doesn't already include 'node'
batchContent = `@echo off
title ${config.displayName || config.appName}
cd /d "%~dp0"
echo Starting ${config.displayName || config.appName}...
npm start
if errorlevel 1 (
echo.
echo Failed to start the application.
echo Make sure Node.js is installed on this system.
echo.
pause
exit /b 1
)
`;
} else {
// Create direct node execution with configurable args
const nodeArgs = config.executableArgs || mainScript;
batchContent = `@echo off
title ${config.displayName || config.appName}
cd /d "%~dp0"
echo Starting ${config.displayName || config.appName}...
node ${nodeArgs} %*
if errorlevel 1 (
echo.
echo Failed to start the application.
echo Make sure Node.js is installed on this system.
echo.
pause
exit /b 1
)
`;
}
const batchPath = path.join(packageDir, 'start.bat');
await fs.writeFile(batchPath, batchContent);
// Also create a silent launcher for background services
const silentBatchContent = `@echo off
cd /d "%~dp0"
start "" /min cmd /c "node "${mainScript}" %*"
`;
const silentBatchPath = path.join(packageDir, 'start-silent.bat');
await fs.writeFile(silentBatchPath, silentBatchContent);
console.log('📄 Created Node.js startup scripts (interactive and silent)');
} catch (error) {
console.warn(`Warning: Could not create startup script: ${error.message}`);
}
}
/**
* Creates registry entries for the application if needed
* @param {string} packageDir - Package directory path
* @param {Object} config - Configuration object
* @param {Object} packageJson - package.json content
*/
async function createRegistryEntries(packageDir, config, packageJson) {
try {
// Create a registry file for application registration
const regContent = `Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\\Software\\Classes\\Applications\\${config.executable}]
"FriendlyAppName"="${config.displayName || config.appName}"
[HKEY_CURRENT_USER\\Software\\Classes\\Applications\\${config.executable}\\shell]
[HKEY_CURRENT_USER\\Software\\Classes\\Applications\\${config.executable}\\shell\\open]
[HKEY_CURRENT_USER\\Software\\Classes\\Applications\\${config.executable}\\shell\\open\\command]
@="\\"${config.executable}\\" \\"%1\\""
`;
const regPath = path.join(packageDir, 'app-registration.reg');
await fs.writeFile(regPath, regContent);
console.log('📋 Created registry entries');
} catch (error) {
console.warn(`Warning: Could not create registry entries: ${error.message}`);
}
}
/**
* Creates MCP Server configuration file in the Assets directory
* @param {string} assetsDir - Assets directory path
* @param {Object} config - Configuration object
*/
async function createMcpServerConfig(assetsDir, config) {
try {
// Generate the executable name from config
let executableName;
if (config.executable && config.executable.endsWith('.exe')) {
// Use the configured executable name
executableName = config.executable;
} else {
// Generate executable name from app name
const appName = config.appName || config.displayName || 'app';
executableName = `${appName.replace(/[^a-zA-Z0-9.-]/g, '_')}.exe`;
}
// Create MCP server configuration
const mcpConfig = {
"version": 1,
"mcpServers": [
{
"name": `${config.packageName || config.appName}MCPServer`,
"type": "stdio",
"command": executableName
}
]
};
const configPath = path.join(assetsDir, 'mcpServerConfig.json');
await fs.writeFile(configPath, JSON.stringify(mcpConfig, null, 2));
console.log('📄 Created MCP server configuration: mcpServerConfig.json');
} catch (error) {
console.warn(`Warning: Could not create MCP server configuration: ${error.message}`);
}
}
/**
* Installs Node.js dependencies in the package directory
* @param {string} packageDir - Package directory path
* @param {boolean} productionOnly - Whether to install only production dependencies
*/
async function installNodeDependencies(packageDir, productionOnly = true) {
try {
const originalCwd = process.cwd();
process.chdir(packageDir);
try {
// Use npm ci for faster, reliable, reproducible builds
const productionFlag = productionOnly ? '--production' : '';
executeCommand(`npm ci ${productionFlag} --silent`);
} catch (error) {
// Fallback to npm install
console.log('npm ci failed, falling back to npm install...');
const productionFlag = productionOnly ? '--production' : '';
executeCommand(`npm install ${productionFlag} --silent`);
}
process.chdir(originalCwd);
console.log('Dependencies installed successfully');
} catch (error) {
throw new MSIXError(`Failed to install Node.js dependencies: ${error.message}`);
}
}
/**
* Ensures Node.js runtime is available in the package
* @param {string} packageDir - Package directory path
* @param {Object} config - Configuration object
*/
async function ensureNodeRuntime(packageDir, config) {
try {
const nodeExecutablePath = path.join(packageDir, 'node.exe');
// Always remove existing node.exe to ensure fresh copy
if (await fs.pathExists(nodeExecutablePath)) {
console.log('🗑️ Removing existing Node.js runtime to ensure fresh copy...');
await fs.remove(nodeExecutablePath);
}
// Check if we have a bundled Node.js runtime
const bundledNodePath = path.join(__dirname, '..', 'runtime', 'nodejs', 'node.exe');
if (await fs.pathExists(bundledNodePath)) {
console.log('Copying bundled Node.js runtime...');
await fs.copy(bundledNodePath, nodeExecutablePath);
return;
}
// Try to copy system Node.js executable using fresh binary detection
try {
console.log('🔍 Attempting to get fresh Node.js binary...');
const { getFreshNodeBinary } = require('./sea-handler');
// Use the enhanced fresh binary detection that avoids SEA markers
const freshNodePath = await getFreshNodeBinary();
console.log(`🔍 Fresh binary search result: ${freshNodePath}`);
if (freshNodePath && await fs.pathExists(freshNodePath)) {
console.log(`Copying fresh Node.js runtime from: ${freshNodePath}`);
// Only copy the node.exe file, not entire directories
await fs.copyFile(freshNodePath, nodeExecutablePath);
// Verify the copy worked
if (await fs.pathExists(nodeExecutablePath)) {
console.log('✅ Fresh Node.js runtime copied successfully');
// If config is using node.exe, also create a startup script
if (config.executable === 'node.exe') {
await createNodeStartupWrapper(packageDir, config);
}
return;
}
}
} catch (error) {
console.warn(`Warning: Could not copy fresh Node.js: ${error.message}`);
console.log(`Error details: ${error.stack}`);
// Fallback to basic system node lookup
try {
console.log('🔙 Falling back to basic system Node.js lookup...');
const { executeCommand } = require('./utils');
const wherePath = process.platform === 'win32' ? 'where' : 'which';
const systemNodePath = executeCommand(`${wherePath} node`, { silent: true }).trim().split('\n')[0];
if (systemNodePath && await fs.pathExists(systemNodePath)) {
console.log(`Copying system Node.js runtime from: ${systemNodePath}`);
// Only copy the node.exe file, not entire directories
await fs.copyFile(systemNodePath, nodeExecutablePath);
// Verify the copy worked
if (await fs.pathExists(nodeExecutablePath)) {
console.log('✅ Node.js runtime copied successfully');
// If config is using node.exe, also create a startup script
if (config.executable === 'node.exe') {
await createNodeStartupWrapper(packageDir, config);
}
return;
}
}
} catch (fallbackError) {
console.warn(`Warning: Fallback Node.js copy also failed: ${fallbackError.message}`);
}
}
// If we can't find Node.js, try to download it
await downloadNodejs(packageDir, config.architecture || 'x64');
} catch (error) {
console.warn(`Warning: Could not ensure Node.js runtime: ${error.message}`);
console.warn('📝 The application may not work without Node.js installed on the target system.');
}
}
/**
* Creates a Node.js startup wrapper when using node.exe directly
* @param {string} packageDir - Package directory path
* @param {Object} config - Configuration object
*/
async function createNodeStartupWrapper(packageDir, config) {
try {
// Create a launcher.js that handles the startup logic
const launcherContent = `const { spawn } = require('child_process');
const path = require('path');
// Determine the main script to run
const mainScript = 'app.js'; // Default for our sample app
console.log('Starting ${config.displayName || config.appName}...');
// Launch the main application
const child = spawn(process.execPath, [mainScript], {
stdio: 'inherit',
cwd: __dirname
});
child.on('error', (err) => {
console.error('Failed to start application:', err.message);
console.log('Press any key to exit...');
process.stdin.once('data', () => process.exit(1));
});
child.on('exit', (code) => {
if (code !== 0) {
console.log('\\nApplication exited with code:', code);
console.log('Press any key to exit...');
process.stdin.once('data', () => process.exit(code));
} else {
process.exit(0);
}
});
// Handle termination gracefully
process.on('SIGINT', () => {
child.kill('SIGINT');
});
process.on('SIGTERM', () => {
child.kill('SIGTERM');
});
`;
const launcherPath = path.join(packageDir, 'launcher.js');
await fs.writeFile(launcherPath, launcherContent);
console.log('📄 Created Node.js startup wrapper');
} catch (error) {
console.warn(`Warning: Could not create Node.js startup wrapper: ${error.message}`);
}
}
/**
* Downloads Node.js runtime for the specified architecture
* @param {string} packageDir - Package directory path
* @param {string} architecture - Target architecture (x64, x86)
*/
async function downloadNodejs(packageDir, architecture) {
try {
console.log(`📥 Attempting to download Node.js runtime for ${architecture}...`);
// Get the latest Node.js version dynamically
const nodeVersion = await getLatestNodeVersion();
console.log(`🎯 Using Node.js version: ${nodeVersion}`);
const https = require('https');
const archMap = { x64: 'x64', x86: 'x86', arm64: 'arm64' };
const nodeArch = archMap[architecture] || 'x64';
const downloadUrl = `https://nodejs.org/dist/${nodeVersion}/win-${nodeArch}/node.exe`;
const nodeExecutablePath = path.join(packageDir, 'node.exe');
await new Promise((resolve, reject) => {
const file = fs.createWriteStream(nodeExecutablePath);
https.get(downloadUrl, (response) => {
if (response.statusCode === 200) {
response.pipe(file);
file.on('finish', () => {
file.close();
console.log('✅ Node.js runtime downloaded successfully');
resolve();
});
} else {
reject(new Error(`Failed to download Node.js: HTTP ${response.statusCode}`));
}
}).on('error', (err) => {
fs.unlink(nodeExecutablePath, () => {}); // Clean up on error
reject(err);
});
});
} catch (error) {
console.warn(`Warning: Could not download Node.js runtime: ${error.message}`);
console.warn('💡 Consider manually placing node.exe in your input directory.');
}
}
/**
* Validates the prepared package
* @param {string} packageDir - Package directory path
* @returns {Object} Validation results
*/
async function validatePackage(packageDir) {
const issues = [];
const warnings = [];
try {
// Check for required files
const requiredFiles = ['AppxManifest.xml'];
for (const file of requiredFiles) {
const filePath = path.join(packageDir, file);
if (!(await fs.pathExists(filePath))) {
issues.push(`Missing required file: ${file}`);
}
}
// Check for executable
const manifestPath = path.join(packageDir, 'AppxManifest.xml');
if (await fs.pathExists(manifestPath)) {
const manifestContent = await fs.readFile(manifestPath, 'utf8');
const executableMatch = manifestContent.match(/Executable="([^"]+)"/);
if (executableMatch) {
const executablePath = path.join(packageDir, executableMatch[1]);
if (!(await fs.pathExists(executablePath))) {
issues.push(`Executable not found: ${executableMatch[1]}`);
}
}
}
// Check for assets
const assetsDir = path.join(packageDir, 'Assets');
const requiredAssets = [
'Square44x44Logo.png',
'Square150x150Logo.png',
'StoreLogo.png'
];
for (const asset of requiredAssets) {
const assetPath = path.join(assetsDir, asset);
if (!(await fs.pathExists(assetPath))) {
warnings.push(`Missing asset: ${asset}`);
}
}
// Check package size
const packageSize = await getDirectorySize(packageDir);
if (packageSize > CONSTANTS.MAX_PACKAGE_SIZE) {
warnings.push(`Package size (${formatFileSize(packageSize)}) exceeds recommended maximum (${formatFileSize(CONSTANTS.MAX_PACKAGE_SIZE)})`);
}
return {
isValid: issues.length === 0,
issues,
warnings,
packageSize: formatFileSize(packageSize)
};
} catch (error) {
return {
isValid: false,
issues: [`Validation failed: ${error.message}`],
warnings: [],
packageSize: 'Unknown'
};
}
}
/**
* Gets the total size of a directory
* @param {string} dirPath - Directory path
* @returns {number} Size in bytes
*/
async function getDirectorySize(dirPath) {
let totalSize = 0;
async function calculateSize(currentPath) {
const stats = await fs.stat(currentPath);
if (stats.isFile()) {
totalSize += stats.size;
} else if (stats.isDirectory()) {
const items = await fs.readdir(currentPath);
for (const item of items) {
await calculateSize(path.join(currentPath, item));
}
}
}
await calculateSize(dirPath);
return totalSize;
}
module.exports = {
createMsixPackage,
signMsixPackage,
preparePackageDirectory,
validatePackage,
installNodeDependencies,
ensureNodeRuntime
};