@interopio/desktop-cli
Version:
io.Connect Desktop Seed Repository CLI Tools
538 lines (534 loc) • 27.2 kB
JavaScript
;
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