UNPKG

nodegui-builder

Version:

Tool for packaging NodeGUI applications into standalone executables

335 lines (284 loc) 12.1 kB
const fs = require('fs-extra'); const path = require('path'); const { execSync } = require('child_process'); /** * Package NodeGUI application into standalone application * @param {Object} options Packaging configuration * @param {string} options.appName Application name * @param {string} options.sourceDir Source directory path * @param {string} options.outputDir Output directory path * @param {string} options.mainFile Main file name (default: 'main.js') * @param {string[]} options.additionalModules List of additional npm modules */ async function packageApp(options) { // Set default configuration const config = { appName: options.appName || 'NodeGUIApp', sourceDir: options.sourceDir || process.cwd(), outputDir: options.outputDir || path.join(process.cwd(), 'deploy'), mainFile: options.mainFile || 'main.js', additionalModules: options.additionalModules || [], }; // Remove debug log // Resolve absolute paths to avoid path-related issues const sourceDirAbs = path.resolve(config.sourceDir); const outputDirAbs = path.resolve(config.outputDir); const appDirAbs = path.join(outputDirAbs, config.appName); // Check if output directory is inside source directory, which would cause circular copying const isOutputInSource = appDirAbs.startsWith(sourceDirAbs + path.sep) || appDirAbs === sourceDirAbs; const NODE_MODULES = path.join(sourceDirAbs, 'node_modules'); const NODEGUI_PATH = path.join(NODE_MODULES, '@nodegui', 'nodegui'); const QODE_PATH = path.join(NODE_MODULES, '@nodegui', 'qode', 'binaries', 'qode.exe'); const MINIQT_PATH = path.join(NODEGUI_PATH, 'miniqt'); try { console.log(`Starting packaging process for ${config.appName}...`); console.log('Using qode from:', QODE_PATH); // Create output directory const appDir = appDirAbs; await fs.ensureDir(appDir); await fs.emptyDir(appDir); // Copy project files - improved approach to avoid recursive copying console.log('Copying project files...'); const ignoredDirs = [ 'node_modules', '.git', '.github', 'coverage', '.vscode', '.idea' ]; // Add output directory to ignored dirs if (isOutputInSource) { const relativeOutputPath = path.relative(sourceDirAbs, outputDirAbs); if (relativeOutputPath) { ignoredDirs.push(relativeOutputPath); console.log(`Detected output inside source directory, excluding: ${relativeOutputPath}`); } } // Manual recursive copy implementation to avoid fs-extra's limitation async function copyDirRecursive(src, dest) { // Read source directory contents const entries = await fs.readdir(src, { withFileTypes: true }); // Create the destination directory await fs.ensureDir(dest); // Process each entry for (const entry of entries) { const srcPath = path.join(src, entry.name); const destPath = path.join(dest, entry.name); // Skip ignored directories const relativePath = path.relative(sourceDirAbs, srcPath); const shouldIgnore = ignoredDirs.some(dir => relativePath === dir || (relativePath && relativePath.startsWith(dir + path.sep)) ); if (shouldIgnore) { console.log(` Skipping: ${relativePath}`); continue; } // Handle directories vs files if (entry.isDirectory()) { await copyDirRecursive(srcPath, destPath); } else { await fs.copy(srcPath, destPath); } } } // Start the copying process from source to app directory await copyDirRecursive(sourceDirAbs, appDir); console.log('Project files copied successfully'); // Handle main file path if it's not already copied in the project structure const mainFilePath = path.join(sourceDirAbs, config.mainFile); const mainFileRelativePath = config.mainFile; // Keep relative path for launching // Create node_modules directory (it might already exist from the copy operation) await fs.ensureDir(path.join(appDir, 'node_modules')); // Copy qode.exe console.log('Copying qode executable...'); await fs.copy(QODE_PATH, path.join(appDir, 'qode.exe')); // Copy NodeGUI modules - always needed console.log('Copying NodeGUI modules...'); await fs.copy( path.join(NODE_MODULES, '@nodegui'), path.join(appDir, 'node_modules', '@nodegui') ); // Read package.json to get dependencies let directDependencies = []; try { const packageJsonPath = path.join(config.sourceDir, 'package.json'); console.log(`Reading package.json from: ${packageJsonPath}`); if (fs.existsSync(packageJsonPath)) { const packageJson = require(packageJsonPath); // Get all dependencies from package.json if (packageJson.dependencies) { directDependencies = Object.keys(packageJson.dependencies); console.log(`Found ${directDependencies.length} direct dependencies in package.json`); } } else { console.warn('No package.json found, using default dependencies'); } } catch (error) { console.warn(`Error reading package.json: ${error.message}`); } // Add additional modules specified by user if (config.additionalModules && config.additionalModules.length > 0) { directDependencies = [...new Set([...directDependencies, ...config.additionalModules])]; } // Filter out @nodegui modules as they're already copied directDependencies = directDependencies.filter(dep => !dep.startsWith('@nodegui/')); // Set to track all dependencies to copy (direct and nested) const allDependencies = new Set(directDependencies); // Map to track dependencies that have been processed to avoid circular dependencies const processedDeps = new Map(); // Recursive function to find all nested dependencies async function findNestedDependencies(moduleName) { // Skip if already processed if (processedDeps.has(moduleName)) { return; } processedDeps.set(moduleName, true); const modulePath = path.join(NODE_MODULES, moduleName); // Check for module's package.json const modulePackageJsonPath = path.join(modulePath, 'package.json'); if (fs.existsSync(modulePackageJsonPath)) { try { const modulePackageJson = require(modulePackageJsonPath); // Get module dependencies if (modulePackageJson.dependencies) { const nestedDeps = Object.keys(modulePackageJson.dependencies); // Add all nested dependencies to the set for (const nestedDep of nestedDeps) { // Skip nodegui modules if (!nestedDep.startsWith('@nodegui/')) { allDependencies.add(nestedDep); // Recursively find this dependency's dependencies await findNestedDependencies(nestedDep); } } } } catch (error) { console.warn(`Error reading package.json for ${moduleName}: ${error.message}`); } } } // Find all nested dependencies console.log('Analyzing dependencies tree...'); for (const dep of directDependencies) { await findNestedDependencies(dep); } // Copy all dependencies (direct and nested) if (allDependencies.size > 0) { console.log(`Copying ${allDependencies.size} total dependencies (including nested dependencies):`); for (const dep of allDependencies) { const modulePath = path.join(NODE_MODULES, dep); if (fs.existsSync(modulePath)) { console.log(` - ${dep}`); await fs.copy( modulePath, path.join(appDir, 'node_modules', dep) ); } else { console.warn(` - ${dep} (not found, skipping)`); } } } else { console.log('No dependencies to copy'); } // Copy all Qt DLLs to root folder console.log('Copying Qt DLLs...'); if (fs.existsSync(MINIQT_PATH)) { const qtVersions = fs.readdirSync(MINIQT_PATH); if (qtVersions.length > 0) { const qtVersion = qtVersions[0]; const qtPath = path.join(MINIQT_PATH, qtVersion); const qtBinPath = path.join(qtPath, 'msvc2019_64', 'bin'); // Copy all DLLs from bin folder const files = fs.readdirSync(qtBinPath); for (const file of files) { if (file.endsWith('.dll')) { await fs.copy( path.join(qtBinPath, file), path.join(appDir, file) ); } } // Copy Qt plugins const pluginDirs = ['platforms', 'styles', 'imageformats']; for (const pluginDir of pluginDirs) { const srcDir = path.join(qtPath, 'msvc2019_64', 'plugins', pluginDir); if (fs.existsSync(srcDir)) { await fs.copy(srcDir, path.join(appDir, pluginDir)); } } // Create qt.conf await fs.writeFile( path.join(appDir, 'qt.conf'), `[Paths]\nPlugins=./\nPrefixes=./\n` ); } } // CREATE STARTUP SCRIPT console.log('Creating startup script...'); await fs.writeFile( path.join(appDir, `debug.bat`), `@echo off echo Starting ${config.appName}... qode.exe ${mainFileRelativePath.replace(/\//g, '\\')} ` ); // Create a VBS script to run the batch file hidden console.log('Creating VBS hidden launcher...'); await fs.writeFile( path.join(appDir, 'hidden-run.vbs'), `Set WshShell = CreateObject("WScript.Shell") WshShell.Run chr(34) & WScript.Arguments(0) & chr(34), 0 Set WshShell = Nothing ` ); // Create a simple C launcher instead of C++ console.log('Creating C launcher...'); await fs.writeFile( path.join(appDir, 'NodeGuiLauncher.c'), `#include <windows.h> #include <stdlib.h> int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { // Set Qt environment variables with relative paths SetEnvironmentVariable("QT_PLUGIN_PATH", "."); SetEnvironmentVariable("QT_QPA_PLATFORM_PLUGIN_PATH", "./platforms"); // Run the VBS script which runs debug.bat hidden const char* cmd = "wscript.exe hidden-run.vbs debug.bat"; // Execute the command WinExec(cmd, SW_HIDE); return 0; } ` ); // Update compilation script for C try { console.log('Compiling C launcher to EXE...'); const compilerScript = path.join(appDir, '_compile.bat'); await fs.writeFile(compilerScript, `@echo off echo Compiling C launcher... gcc -o ${config.appName}.exe NodeGuiLauncher.c -mwindows if %errorlevel% neq 0 ( echo Compilation failed! Try installing MinGW. exit /b 1 ) del NodeGuiLauncher.c echo Compilation successful! `); execSync(compilerScript, { cwd: appDir, stdio: 'inherit' }); await fs.remove(compilerScript); console.log('EXE launcher created successfully!'); } catch (error) { console.error('Failed to compile C launcher:', error.message); console.log('You can compile it manually with: gcc -o app.exe NodeGuiLauncher.c -mwindows'); } console.log(`Packaging complete! Your application is available at: ${appDir}`); return appDir; } catch (error) { console.error('Packaging failed:', error); console.error(error.stack); throw error; } } module.exports = { packageApp };