@burgan-tech/vnext-cli
Version:
CLI for creating and managing vNext domain projects with modular component sharing
380 lines (327 loc) • 14.2 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
const os = require('os');
const { execSync } = require('child_process');
class SchemaManager {
constructor(options = {}) {
// Load configuration from config file if exists
const configDefaults = this.loadConfigFile();
// Merge configurations with priority: options > environment > config file > defaults
this.options = {
cacheDir: this.getSystemCacheDir(),
schemaPackageName: '@burgan-tech/vnext-schema',
npmRegistry: 'https://registry.npmjs.org',
defaultVersion: 'latest',
...configDefaults,
schemaPackageName: process.env.AMORPHIE_SCHEMA_PACKAGE || configDefaults.schemaPackageName || '@burgan-tech/vnext-schema',
npmRegistry: process.env.AMORPHIE_NPM_REGISTRY || configDefaults.npmRegistry || 'https://registry.npmjs.org',
cacheDir: process.env.AMORPHIE_CACHE_DIR || this.getSystemCacheDir(),
...options
};
this.cacheDir = this.options.cacheDir;
this.schemaCacheDir = path.join(this.cacheDir, 'schemas');
this.currentVersion = null;
// Show cache directory info
console.log(chalk.gray(`📁 Schema cache directory: ${this.schemaCacheDir}`));
}
/**
* Get system-appropriate cache directory
* @returns {string} Cache directory path
*/
getSystemCacheDir() {
const platform = os.platform();
const homedir = os.homedir();
switch (platform) {
case 'darwin': // macOS
return path.join(homedir, 'Library', 'Caches', 'vnext-cli', 'schemas');
case 'win32': // Windows
return path.join(process.env.APPDATA || path.join(homedir, 'AppData', 'Roaming'), 'vnext-cli', 'cache', 'schemas');
case 'linux': // Linux
default: // Other Unix-like systems
return path.join(process.env.XDG_CACHE_HOME || path.join(homedir, '.cache'), 'vnext-cli', 'schemas');
}
}
/**
* Load configuration from config file
* @returns {Object} Configuration object
*/
loadConfigFile() {
try {
const configPath = path.join(__dirname, '..', 'config', 'template.config.js');
if (fs.existsSync(configPath)) {
delete require.cache[require.resolve(configPath)]; // Clear cache
const config = require(configPath);
console.log(chalk.gray('📄 Loaded schema configuration from config file'));
return config.schema || {};
}
} catch (error) {
console.log(chalk.yellow(`⚠️ Could not load schema config file: ${error.message}`));
}
return {};
}
/**
* Ensure schema package is available for a specific runtime version
* @param {string} runtimeVersion - Runtime version (latest, v1.0.0, etc.)
* @returns {Promise<string>} Path to schema directory
*/
async ensureSchemas(runtimeVersion = 'latest') {
try {
console.log(chalk.blue('📋 Checking schema package...'));
// Ensure cache directory exists
await fs.ensureDir(this.schemaCacheDir);
// Resolve version to actual tag/version
const actualVersion = await this.resolveVersion(runtimeVersion);
const versionCacheDir = path.join(this.schemaCacheDir, `${actualVersion}`);
const schemaPath = path.join(versionCacheDir, 'schemas');
// Check if this specific version is cached
if (await fs.pathExists(schemaPath)) {
console.log(chalk.green(`✅ Schema package ${actualVersion} found in cache`));
this.currentVersion = actualVersion;
return schemaPath;
} else {
console.log(chalk.blue(`⬇️ Downloading schema package version ${actualVersion}...`));
await this.downloadSchemaPackage(actualVersion);
this.currentVersion = actualVersion;
return schemaPath;
}
} catch (error) {
throw new Error(`Failed to ensure schema package: ${error.message}`);
}
}
/**
* Download schema package from NPM
* @param {string} version - NPM package version to download
* @returns {Promise<void>}
*/
async downloadSchemaPackage(version) {
const versionCacheDir = path.join(this.schemaCacheDir, `${version}`);
const tempDir = path.join(this.schemaCacheDir, 'temp');
try {
// Test cache directory write permissions first
try {
await fs.ensureDir(this.schemaCacheDir);
await fs.ensureDir(tempDir);
await fs.ensureDir(versionCacheDir);
} catch (error) {
throw new Error(`Cannot write to cache directory '${this.schemaCacheDir}'. Check file permissions. Original error: ${error.message}`);
}
console.log(chalk.gray(`Downloading ${this.options.schemaPackageName}@${version} from ${this.options.npmRegistry}`));
// Build npm pack command with registry and custom cache
const packageSpec = version === 'latest' ? this.options.schemaPackageName : `${this.options.schemaPackageName}@${version}`;
const customCacheDir = path.join(this.schemaCacheDir, 'npm-cache');
await fs.ensureDir(customCacheDir);
const npmPackCmd = `npm pack ${packageSpec} --pack-destination ${tempDir} --registry ${this.options.npmRegistry} --cache ${customCacheDir}`;
// Download package using npm pack
let packOutput;
try {
packOutput = execSync(npmPackCmd, {
encoding: 'utf8',
stdio: 'pipe'
});
} catch (error) {
if (error.message.includes('404')) {
throw new Error(`Schema package '${this.options.schemaPackageName}@${version}' not found in registry '${this.options.npmRegistry}'. Check package name and version.`);
} else if (error.message.includes('ENOTFOUND') || error.message.includes('network')) {
throw new Error(`Network error: Cannot connect to NPM registry '${this.options.npmRegistry}'. Check your internet connection.`);
} else if (error.message.includes('EACCES') || error.message.includes('permission denied')) {
throw new Error(`Permission denied accessing NPM registry '${this.options.npmRegistry}'. For GitHub Package Registry, ensure you have proper authentication. Run: npm config set //npm.pkg.github.com/:_authToken YOUR_GITHUB_TOKEN`);
} else if (error.message.includes('EEXIST') || error.message.includes('File exists')) {
throw new Error(`NPM cache conflict. Try running: npm cache clean --force, then retry the command.`);
} else {
throw new Error(`NPM command failed: ${error.message}. Check NPM configuration and registry access.`);
}
}
const tarFile = packOutput.trim();
const tarPath = path.join(tempDir, tarFile);
// Verify tar file was downloaded
if (!(await fs.pathExists(tarPath))) {
throw new Error(`Downloaded tar file not found at '${tarPath}'. NPM pack command may have failed silently.`);
}
// Extract tar file
const extractCmd = process.platform === 'win32'
? `tar -xzf "${tarPath}" -C "${versionCacheDir}" --strip-components=1`
: `tar -xzf ${tarPath} -C ${versionCacheDir} --strip-components=1`;
try {
execSync(extractCmd, {
stdio: 'pipe'
});
} catch (error) {
throw new Error(`Failed to extract schema package: ${error.message}. Package may be corrupted.`);
}
// Clean up tar file and temp directory
await fs.remove(tarPath);
// Verify schema directory exists in the package
const schemaPath = path.join(versionCacheDir, 'schemas');
if (!(await fs.pathExists(schemaPath))) {
throw new Error(`Downloaded package '${this.options.schemaPackageName}@${version}' does not contain 'schemas' directory. Package structure is invalid.`);
}
// Verify schema files exist
const schemaFiles = await fs.readdir(schemaPath);
const jsonSchemas = schemaFiles.filter(file => file.endsWith('.json'));
if (jsonSchemas.length === 0) {
throw new Error(`No schema files found in '${this.options.schemaPackageName}@${version}'. Package may be empty or invalid.`);
}
console.log(chalk.green(`✅ Schema package ${version} downloaded successfully (${jsonSchemas.length} schema files)`));
} catch (error) {
// Clean up on failure
if (await fs.pathExists(versionCacheDir)) {
await fs.remove(versionCacheDir);
}
// Re-throw with more context
throw error;
} finally {
// Clean up temp directory
if (await fs.pathExists(tempDir)) {
await fs.remove(tempDir);
}
}
}
/**
* Resolve version string to actual npm version
* @param {string} version - Version string ('latest', 'v1.0.0', etc.)
* @returns {Promise<string>} Actual npm version
*/
async resolveVersion(version) {
if (version === 'latest') {
try {
const latestVersion = await this.getLatestVersion();
console.log(chalk.gray(`Latest schema version resolved to: ${latestVersion}`));
return latestVersion;
} catch (error) {
console.log(chalk.yellow(`⚠️ Could not fetch latest version, using 'latest': ${error.message}`));
return 'latest';
}
}
return version;
}
/**
* Get latest version from NPM registry
* @returns {Promise<string>} Latest version tag
*/
async getLatestVersion() {
try {
const npmViewCmd = `npm view ${this.options.schemaPackageName} version --registry ${this.options.npmRegistry}`;
const latestVersion = execSync(npmViewCmd, {
encoding: 'utf8',
stdio: 'pipe'
}).trim();
return latestVersion;
} catch (error) {
throw new Error(`Failed to get latest version: ${error.message}`);
}
}
/**
* List all available versions from NPM registry
* @returns {Promise<Array<string>>} Array of version tags
*/
async listAvailableVersions() {
try {
const npmViewCmd = `npm view ${this.options.schemaPackageName} versions --json --registry ${this.options.npmRegistry}`;
const versionsOutput = execSync(npmViewCmd, {
encoding: 'utf8',
stdio: 'pipe'
}).trim();
const versions = JSON.parse(versionsOutput);
// Return sorted versions (newest first)
return Array.isArray(versions) ? versions.reverse() : [versions];
} catch (error) {
throw new Error(`Failed to list versions: ${error.message}`);
}
}
/**
* Update schema cache (remove all cached versions)
* @returns {Promise<void>}
*/
async updateSchemas() {
try {
console.log(chalk.blue('🔄 Clearing schema cache...'));
if (await fs.pathExists(this.schemaCacheDir)) {
await fs.remove(this.schemaCacheDir);
}
console.log(chalk.green('✅ Schema cache cleared'));
} catch (error) {
throw new Error(`Failed to update schemas: ${error.message}`);
}
}
/**
* Get schema package information
* @param {string} version - Schema version to get info for
* @returns {Promise<Object>} Schema package information
*/
async getSchemaInfo(version = 'latest') {
try {
const schemaPath = await this.ensureSchemas(version);
const packageJsonPath = path.join(path.dirname(schemaPath), 'package.json');
const info = {
packageName: this.options.schemaPackageName,
version: this.currentVersion,
path: schemaPath,
exists: await fs.pathExists(schemaPath),
packageJson: null,
availableVersions: []
};
// Get available versions
try {
info.availableVersions = await this.listAvailableVersions();
} catch (error) {
console.log(chalk.yellow(`⚠️ Could not fetch available versions: ${error.message}`));
}
if (await fs.pathExists(packageJsonPath)) {
info.packageJson = await fs.readJSON(packageJsonPath);
}
// List schema files
if (await fs.pathExists(schemaPath)) {
const schemaFiles = await fs.readdir(schemaPath);
info.schemaFiles = schemaFiles.filter(file => file.endsWith('.json'));
}
return info;
} catch (error) {
throw new Error(`Failed to get schema info: ${error.message}`);
}
}
/**
* Clear schema cache
* @returns {Promise<void>}
*/
async clearCache() {
if (await fs.pathExists(this.schemaCacheDir)) {
await fs.remove(this.schemaCacheDir);
console.log(chalk.green('✅ Schema cache cleared'));
}
}
/**
* Check if runtime version has changed and update if necessary
* @param {string} configPath - Path to vnext.config.json
* @returns {Promise<string>} Schema path for the runtime version
*/
async ensureSchemasForConfig(configPath) {
try {
// Load vnext.config.json
if (!(await fs.pathExists(configPath))) {
throw new Error('vnext.config.json not found');
}
const config = await fs.readJSON(configPath);
const runtimeVersion = config.runtimeVersion || 'latest';
const schemaVersion = config.schemaVersion; // Can be undefined for backward compatibility
// Determine which version to use for schema validation
// Priority: schemaVersion (if exists) > runtimeVersion
const versionToUse = schemaVersion || runtimeVersion;
// Log both versions for user visibility
console.log(chalk.blue(`🔖 Runtime version: ${runtimeVersion}`));
if (schemaVersion) {
console.log(chalk.blue(`📋 Schema version: ${schemaVersion}`));
if (schemaVersion !== runtimeVersion) {
console.log(chalk.yellow(`⚠️ Schema version differs from runtime version - using schema version for validation`));
}
} else {
console.log(chalk.gray(`📋 Schema version: not specified, using runtime version`));
}
// Ensure schema package for the determined version
return await this.ensureSchemas(versionToUse);
} catch (error) {
throw new Error(`Failed to ensure schemas for config: ${error.message}`);
}
}
}
module.exports = SchemaManager;