UNPKG

@interopio/desktop-cli

Version:

io.Connect Desktop Seed Repository CLI Tools

538 lines (534 loc) 27.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.packageCommand = void 0; const commander_1 = require("commander"); const utils_1 = require("../utils"); const modification_manager_1 = require("../services/modification-manager"); const fs_extra_1 = __importDefault(require("fs-extra")); const path_1 = __importDefault(require("path")); const json_merger_1 = require("json-merger"); exports.packageCommand = new commander_1.Command('package') .description('Package the standalone iocd Electron application and resources for distribution') .option('--output <path>', 'Output directory for packages', 'dist/packages') .option('--dir', 'Build unpacked directory only (useful for testing)') .action(async (options) => { try { utils_1.Logger.info('Starting application packaging with electron-builder...'); // Check prerequisites await validatePrerequisites(); // Get build configuration let buildConfig = await getBuildConfig(); // Prepare build environment await preparePrepackBuild(); // Create package using electron-builder await createPackageWithElectronBuilder(options, buildConfig); utils_1.Logger.success('Application packaged successfully!'); } catch (error) { utils_1.Logger.error(`Packaging failed: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); } }); async function validatePrerequisites() { // Check if electron-builder is available try { await Promise.resolve().then(() => __importStar(require('electron-builder'))); utils_1.Logger.info('electron-builder found'); // Check if it's properly installed as devDependency await validateElectronBuilderInstallation(); } catch (error) { throw new Error('electron-builder is required but not found. Please install it as a dev dependency:\n' + 'npm install --save-dev electron-builder\n' + 'or\n' + 'yarn add --dev electron-builder\n\n' + 'Note: electron-builder should be installed as devDependency to avoid being bundled in the final package.'); } // Check if components are installed const componentsDir = utils_1.PathUtils.getComponentsDir(); if (!(await utils_1.FileUtils.exists(componentsDir))) { throw new Error('Components not found. Please run: npm run setup'); } // Check for core component const iocdDir = utils_1.PathUtils.getComponentDir('iocd'); if (!(await utils_1.FileUtils.exists(iocdDir))) { throw new Error('Core iocd component not found. Please run: npm run setup'); } // Validate license const hasValidLicense = await utils_1.ConfigManager.hasValidLicense(); if (!hasValidLicense) { throw new Error('Valid license required for packaging'); } utils_1.Logger.success('Prerequisites validated'); } async function validateElectronBuilderInstallation() { try { // Read the current package.json to check dependency installation const packageJson = await utils_1.ConfigManager.getPackageJson(); // Also check iocd-cli's own package.json for proper configuration utils_1.Logger.debug('Validating iocd-cli configuration...'); await validateIocdCliConfiguration(); // Check if iocd-cli is incorrectly installed as regular dependency await validateIocdCliInstallation(packageJson); // Check if electron-builder is in dependencies (bad) if (packageJson.dependencies && packageJson.dependencies['electron-builder']) { utils_1.Logger.warning('electron-builder is installed as a regular dependency. ' + 'It should be installed as devDependency to avoid being bundled in the final package.\n' + 'Please move it to devDependencies:\n' + '1. Remove: npm uninstall electron-builder\n' + '2. Reinstall: npm install --save-dev electron-builder'); } // Check if electron-builder is in devDependencies (good) or peerDependencies (also acceptable) const hasInDevDeps = packageJson.devDependencies && packageJson.devDependencies['electron-builder']; const hasInPeerDeps = packageJson.peerDependencies && packageJson.peerDependencies['electron-builder']; if (hasInDevDeps) { utils_1.Logger.info('electron-builder correctly installed as devDependency'); } else if (hasInPeerDeps) { utils_1.Logger.info('electron-builder specified as peerDependency (user must install separately)'); } else { // electron-builder is available (import worked) but not in package.json // This could happen if it's installed globally or in a parent node_modules utils_1.Logger.warning('electron-builder is available but not found in package.json dependencies. ' + 'For best practices, install it as devDependency:\n' + 'npm install --save-dev electron-builder'); } } catch (error) { // If we can't read package.json, just log a debug message utils_1.Logger.debug(`Could not validate electron-builder installation: ${error instanceof Error ? error.message : String(error)}`); } } async function validateIocdCliConfiguration() { try { // Read iocd-cli's own package.json const iocdCliPackageJsonPath = path_1.default.resolve(__dirname, '../../package.json'); if (!(await utils_1.FileUtils.exists(iocdCliPackageJsonPath))) { utils_1.Logger.debug('iocd-cli package.json not found, skipping self-validation'); return; } const iocdCliPackageJson = await fs_extra_1.default.readJson(iocdCliPackageJsonPath); // Check iocd-cli's electron-builder configuration if (iocdCliPackageJson.dependencies && iocdCliPackageJson.dependencies['electron-builder']) { utils_1.Logger.error('CONFIGURATION ERROR: iocd-cli has electron-builder in dependencies! ' + 'This will cause it to be bundled in the final package. ' + 'iocd-cli should have electron-builder as peerDependency only.'); } else if (iocdCliPackageJson.peerDependencies && iocdCliPackageJson.peerDependencies['electron-builder']) { utils_1.Logger.debug('✓ iocd-cli correctly configured with electron-builder as peerDependency'); } else { utils_1.Logger.warning('iocd-cli configuration: electron-builder not found in peerDependencies. ' + 'This may indicate a configuration issue with the CLI tool itself.'); } // Check if iocd-cli has electron-builder in devDependencies (also problematic for distribution) if (iocdCliPackageJson.devDependencies && iocdCliPackageJson.devDependencies['electron-builder']) { utils_1.Logger.warning('iocd-cli has electron-builder in devDependencies. ' + 'For CLI distribution, it should be peerDependency to let users choose their version.'); } } catch (error) { utils_1.Logger.debug(`Could not validate iocd-cli configuration: ${error instanceof Error ? error.message : String(error)}`); } } async function validateIocdCliInstallation(packageJson) { try { // Check if iocd-cli is installed as regular dependency (problematic) if (packageJson.dependencies && packageJson.dependencies['iocd-cli']) { throw new Error('INSTALLATION ERROR: iocd-cli is installed as a regular dependency!\n\n' + 'iocd-cli should NOT be installed as a regular dependency because:\n' + '• It would be bundled in your production application\n' + '• It\'s a development/build tool, not a runtime dependency\n' + '• It significantly increases your bundle size unnecessarily\n\n' + 'Please fix this by choosing one of these options:\n\n' + '1. Install as devDependency (recommended for project-specific use):\n' + ' npm uninstall iocd-cli\n' + ' npm install --save-dev iocd-cli\n\n' + '2. Install globally (recommended for system-wide use):\n' + ' npm uninstall iocd-cli\n' + ' npm install -g iocd-cli\n\n' + '3. Use npx (no installation required):\n' + ' npm uninstall iocd-cli\n' + ' npx iocd-cli package [options]'); } // Check installation location and provide guidance const hasInDevDeps = packageJson.devDependencies && packageJson.devDependencies['iocd-cli']; const hasInPeerDeps = packageJson.peerDependencies && packageJson.peerDependencies['iocd-cli']; if (hasInDevDeps) { utils_1.Logger.debug('✓ iocd-cli correctly installed as devDependency'); } else if (hasInPeerDeps) { utils_1.Logger.info('iocd-cli specified as peerDependency (users must install separately)'); } else { // iocd-cli is available (we're running) but not in package.json // This means it's either installed globally or via npx utils_1.Logger.debug('iocd-cli running from global installation or npx (recommended)'); } } catch (error) { // Re-throw configuration errors, but catch other validation errors if (error instanceof Error && error.message.includes('INSTALLATION ERROR')) { throw error; } utils_1.Logger.debug(`Could not validate iocd-cli installation: ${error instanceof Error ? error.message : String(error)}`); } } async function getElectronVersionFromComponent() { try { const iocdDir = utils_1.PathUtils.getComponentDir('iocd'); const metadataPath = path_1.default.join(iocdDir, 'metadata.json'); if (!(await utils_1.FileUtils.exists(metadataPath))) { utils_1.Logger.debug('No iocd component metadata found, skipping electron version detection'); return null; } const metadata = await fs_extra_1.default.readJson(metadataPath); if (metadata && metadata.electronVersion) { return metadata.electronVersion; } utils_1.Logger.debug('No electron version found in iocd component metadata'); return null; } catch (error) { utils_1.Logger.debug(`Failed to read electron version from component: ${error instanceof Error ? error.message : String(error)}`); return null; } } async function getBuildConfig() { const packageJson = await utils_1.ConfigManager.getPackageJson(); // Load base configuration required for iocd component const baseConfigPath = path_1.default.resolve(__dirname, '../config/electron-builder-base.json'); const baseConfig = await fs_extra_1.default.readJson(baseConfigPath); // Get client configuration from multiple possible sources (in order of precedence) const clientConfig = await getClientElectronBuilderConfig(packageJson); // Get electron version from iocd component metadata const electronVersion = await getElectronVersionFromComponent(); if (electronVersion) { // Add electron version to base config if not already specified in client config if (!clientConfig.electronVersion) { baseConfig.electronVersion = electronVersion; utils_1.Logger.info(`Using electron version ${electronVersion} from iocd component`); } } // Merge configurations with client config taking precedence const mergedConfig = mergeConfigurations(baseConfig, clientConfig); utils_1.Logger.info('Using merged electron-builder configuration'); utils_1.Logger.debug(`Base config: ${baseConfigPath}`); if (Object.keys(clientConfig).length > 0) { utils_1.Logger.debug('Client configuration found'); } return mergedConfig; } async function getClientElectronBuilderConfig(packageJson) { // Check multiple configuration sources in order of precedence const configSources = [ // { path: 'electron-builder.json', type: 'json' }, // { path: '.electron-builder.json', type: 'json' }, // { path: 'electron-builder.yml', type: 'yaml' }, // { path: 'electron-builder.yaml', type: 'yaml' }, { path: 'package.json', type: 'package-build' } ]; for (const source of configSources) { try { if (source.type === 'package-build') { // Handle package.json build field if (packageJson.build && Object.keys(packageJson.build).length > 0) { utils_1.Logger.debug('Client configuration found in package.json build field'); return packageJson.build; } } else if (source.type === 'json') { // Handle JSON config files if (await utils_1.FileUtils.exists(source.path)) { const config = await fs_extra_1.default.readJson(source.path); if (config && Object.keys(config).length > 0) { utils_1.Logger.info(`Client configuration found in ${source.path}`); return config; } } } else if (source.type === 'yaml') { // Handle YAML config files (would need yaml parser, but show intent) if (await utils_1.FileUtils.exists(source.path)) { utils_1.Logger.info(`YAML configuration file found: ${source.path}`); utils_1.Logger.warning('YAML configuration files are not yet supported. Please use JSON or package.json build field.'); // TODO: Add yaml parser support if needed // const yaml = await import('yaml'); // const content = await fs.readFile(source.path, 'utf8'); // return yaml.parse(content); } } } catch (error) { utils_1.Logger.debug(`Failed to read configuration from ${source.path}: ${error instanceof Error ? error.message : String(error)}`); } } // No client configuration found return {}; } function mergeConfigurations(baseConfig, clientConfig) { try { // Use simple object spread for reliable merging const mergedConfig = { ...baseConfig, ...clientConfig }; utils_1.Logger.debug('Configuration merge completed'); utils_1.Logger.debug(`Base config keys: ${Object.keys(baseConfig).join(', ')}`); utils_1.Logger.debug(`Client config keys: ${Object.keys(clientConfig).join(', ')}`); utils_1.Logger.debug(`Merged keys: ${Object.keys(mergedConfig).join(', ')}`); return mergedConfig; } catch (error) { utils_1.Logger.error(`Configuration merge failed: ${error instanceof Error ? error.message : String(error)}`); throw error; } } async function preparePrepackBuild() { utils_1.Logger.info('Preparing build environment with prepackaged app...'); const iocdDir = utils_1.PathUtils.getComponentDir('iocd'); // Ensure the iocd component exists if (!(await utils_1.FileUtils.exists(iocdDir))) { throw new Error('iocd component not found. Please run: npm run setup'); } // Verify or create package.json in the iocd directory const iocdPackageJson = path_1.default.join(iocdDir, 'package.json'); // Always create/update package.json to ensure correct main entry point utils_1.Logger.debug('Creating/updating package.json for prepackaged app...'); // Read client's package.json for metadata const clientPackageJson = await utils_1.ConfigManager.getPackageJson(); const packageJsonContent = { name: clientPackageJson.name || 'io-connect-desktop', version: clientPackageJson.version || '1.0.0', description: clientPackageJson.description || 'io.Connect Desktop Application', main: './bootstrap.js', // Always use bootstrap.js as entry point author: clientPackageJson.author || 'io.Connect', license: clientPackageJson.license || 'Proprietary', private: true }; await fs_extra_1.default.writeJson(iocdPackageJson, packageJsonContent, { spaces: 2 }); utils_1.Logger.debug(`Created/updated package.json with main entry point: ${packageJsonContent.main}`); // Create bootstrap.js as the only file in the asar const bootstrapJsPath = path_1.default.join(iocdDir, 'bootstrap.js'); const bootstrapJsContent = `// Auto-generated bootstrap.js - single file in asar const path = require('path'); const fs = require('fs'); // Bootstrap loads the actual application from resources folder const coreAppPath = path.join(process.resourcesPath, 'core-app.asar'); if (fs.existsSync(coreAppPath)) { console.log('Loading core application from core-app.asar...'); try { require(coreAppPath); } catch (err) { console.error('❌ Failed to load core application:', err); process.exit(1); } } else { console.error(\` ❌ ERROR: Core application not found. Expected at: \${coreAppPath} This bootstrap.js is designed to load the actual application from a separate core-app.asar file in the resources folder. \`); process.exit(1); } `; await fs_extra_1.default.writeFile(bootstrapJsPath, bootstrapJsContent); utils_1.Logger.debug('Created bootstrap.js as single file for asar'); // Validate that bootstrap.js was created successfully if (!(await utils_1.FileUtils.exists(bootstrapJsPath))) { throw new Error('Failed to create bootstrap.js file'); } // Validate that package.json was created successfully if (!(await utils_1.FileUtils.exists(iocdPackageJson))) { throw new Error('Failed to create package.json file'); } // Merge JSON configuration files if merge files exist await mergeJsonConfigurations(iocdDir); utils_1.Logger.success('Build environment prepared for prepackaged app'); } async function mergeJsonConfigurations(iocdDir) { utils_1.Logger.debug('Checking for JSON configuration files to merge...'); // Look for .merge.json files in the config directory const configDir = path_1.default.join(iocdDir, 'config'); if (!(await utils_1.FileUtils.exists(configDir))) { utils_1.Logger.debug('No config directory found, skipping JSON merge'); return; } try { const files = await fs_extra_1.default.readdir(configDir); const mergeFiles = files.filter(file => file.endsWith('.merge.json')); if (mergeFiles.length === 0) { utils_1.Logger.debug('No .merge.json files found'); return; } utils_1.Logger.info(`Merging ${mergeFiles.length} configuration file(s)...`); for (const mergeFile of mergeFiles) { const baseName = mergeFile.replace('.merge.json', '.json'); const mergeFilePath = path_1.default.join(configDir, mergeFile); const baseFilePath = path_1.default.join(configDir, baseName); try { // Read the merge file const mergeData = await fs_extra_1.default.readJson(mergeFilePath); let baseData = {}; // Read the base file if it exists if (await utils_1.FileUtils.exists(baseFilePath)) { baseData = await fs_extra_1.default.readJson(baseFilePath); utils_1.Logger.debug(`Merging ${mergeFile} into existing ${baseName}`); } else { utils_1.Logger.debug(`Creating ${baseName} from ${mergeFile} (base file doesn't exist)`); } // Use json-merger for sophisticated merging const merger = new json_merger_1.Merger({}); const mergedData = merger.mergeObjects([baseData, mergeData]); // Write the merged result back to the base file await fs_extra_1.default.writeJson(baseFilePath, mergedData, { spaces: 2 }); utils_1.Logger.debug(`✓ Successfully merged ${mergeFile}${baseName}`); // Remove the merge file after successful merge await fs_extra_1.default.remove(mergeFilePath); utils_1.Logger.debug(`Removed merge file: ${mergeFile}`); } catch (error) { utils_1.Logger.error(`Failed to merge ${mergeFile}: ${error instanceof Error ? error.message : String(error)}`); // Continue processing other files even if one fails } } utils_1.Logger.success('Configuration files merged successfully'); } catch (error) { utils_1.Logger.error(`Failed to process JSON merge: ${error instanceof Error ? error.message : String(error)}`); // Don't throw - this is not a critical failure } } async function createPackageWithElectronBuilder(options, buildConfig) { // Apply modifications before packaging utils_1.Logger.info('Processing modifications...'); const modManager = new modification_manager_1.ModificationManager(); await modManager.applyAll(); utils_1.Logger.success('Modifications applied successfully'); utils_1.Logger.info('Building package with electron-builder...'); try { // Dynamically import electron-builder from the client's node_modules let electronBuilder; try { electronBuilder = await Promise.resolve().then(() => __importStar(require('electron-builder'))); // Skip duplicate validation here since we already validated in prerequisites } catch (error) { throw new Error('electron-builder not found. Please install it as a dev dependency:\n' + 'npm install --save-dev electron-builder\n' + 'or\n' + 'yarn add --dev electron-builder'); } const { build } = electronBuilder; // Filter configuration to only include valid electron-builder properties const cleanedConfig = buildConfig; // Build configuration - use prepackaged iocd component const iocdDir = utils_1.PathUtils.getComponentDir('iocd'); const builderConfig = { ...cleanedConfig, asar: true, // Create asar with only bootstrap.js directories: { ...cleanedConfig.directories, app: iocdDir, // Point directly to prepackaged iocd component output: options.output || cleanedConfig.directories?.output || 'dist' }, files: [ "bootstrap.js", // Only include bootstrap.js in the asar "package.json" // Include package.json for electron to read main entry point ], extraFiles: [ { "from": "components/iocd/config/", "to": "config/", "filter": ["**/*.json", "!**/*.merge.json"] }, { "from": "components/iocd/assets/", "to": "assets/" } ], // Enable hooks for electron-builder afterSign: async (context) => { utils_1.Logger.debug('Running electron-builder afterSign hook...'); // copy the asar from iocdDir to the output const asarPath = path_1.default.join(iocdDir, 'resources', 'app.asar'); if (await utils_1.FileUtils.exists(asarPath)) { await fs_extra_1.default.copyFile(asarPath, path_1.default.join(context.appOutDir, 'resources', 'app.asar')); utils_1.Logger.debug('Core application ASAR copied to output directory'); } else { utils_1.Logger.warning('Core application ASAR not found, skipping copy'); } return Promise.resolve(); } }; utils_1.Logger.debug('Using electron-builder with prepackaged app and client customizations'); // Copy additional resources to the config builderConfig.extraResources = [ // Include any existing extraResources from config ...(Array.isArray(cleanedConfig.extraResources) ? cleanedConfig.extraResources : cleanedConfig.extraResources ? [cleanedConfig.extraResources] : []) ]; // Configure build options const buildOptions = { config: builderConfig, publish: 'never' // Don't auto-publish }; // Add dir option if specified (for testing) if (options.dir) { buildOptions.dir = true; utils_1.Logger.info('Building unpacked directory only (--dir option)'); } utils_1.Logger.info('Starting electron-builder...'); utils_1.Logger.debug(`Build configuration: ${JSON.stringify(builderConfig, null, 2)}`); // Build the app with electron-builder (will handle signing/notarization) await build(buildOptions); utils_1.Logger.success(`Package created in ${builderConfig.directories?.output}`); } catch (error) { if (error instanceof Error && error.message.includes('electron-builder not found')) { throw error; } throw new Error(`electron-builder failed: ${error instanceof Error ? error.message : String(error)}`); } } //# sourceMappingURL=package.js.map