UNPKG

@goldenpath-cloud/frontend-standards

Version:

Golden Path frontend development standards for Svelte 5 with TypeScript, testing, and validation

656 lines (542 loc) 20 kB
#!/usr/bin/env node const fs = require('fs') const path = require('path') const { execSync } = require('child_process') /** * Golden Path Frontend Standards Installation Script * NPX entry point with cross-platform compatibility */ class FrontendStandardsInstaller { constructor() { this.projectRoot = process.cwd() this.packageRoot = path.dirname(path.dirname(__filename)) this.backupDir = path.join(this.projectRoot, '.goldenpath-backup') this.options = { dryRun: false, verbose: false, rollback: false, help: false, version: false } } async install() { try { this.parseArguments() if (this.options.help) { this.showHelp() return } if (this.options.version) { this.showVersion() return } if (this.options.rollback) { await this.rollback() return } console.log('🚀 Installing Golden Path Frontend Standards...') console.log('') if (this.options.dryRun) { console.log('🔍 DRY RUN MODE - No files will be modified') console.log('') } // Installation steps await this.validateEnvironment() await this.detectProjectType() await this.createBackup() await this.installCoreStandards() await this.installConfigurations() await this.installDependencies() await this.validateInstallation() await this.cleanupBackup() console.log('') console.log('✅ Golden Path Frontend Standards installed successfully!') console.log('') this.showNextSteps() } catch (error) { console.error('❌ Installation failed:', error.message) if (this.options.verbose) { console.error('Stack trace:', error.stack) } console.log('') console.log('🔄 To rollback changes, run:') console.log(' npx @goldenpath/frontend-standards --rollback') process.exit(1) } } parseArguments() { const args = process.argv.slice(2) for (const arg of args) { switch (arg) { case '--dry-run': this.options.dryRun = true break case '--verbose': this.options.verbose = true break case '--rollback': this.options.rollback = true break case '--help': case '-h': this.options.help = true break case '--version': case '-v': this.options.version = true break default: if (arg.startsWith('--')) { throw new Error(`Unknown option: ${arg}`) } } } } showHelp() { console.log(` Golden Path Frontend Standards Installer USAGE: npx @goldenpath/frontend-standards [OPTIONS] OPTIONS: --dry-run Preview installation without making changes --verbose Show detailed installation progress --rollback Restore project to pre-installation state --help, -h Show this help message --version, -v Show version information EXAMPLES: npx @goldenpath/frontend-standards npx @goldenpath/frontend-standards --dry-run npx @goldenpath/frontend-standards --verbose npx @goldenpath/frontend-standards --rollback CORE STANDARDS INSTALLED: • TypeScript - ESLint rules and configuration • Svelte 5 - Component rules and IDE settings • Testing - Vitest, Playwright, Testing Library • Zod - Validation patterns and form handling For more information, visit: https://github.com/goldenpath/frontend-standards `) } showVersion() { const packageJson = JSON.parse(fs.readFileSync(path.join(this.packageRoot, 'package.json'), 'utf8')) console.log(`Golden Path Frontend Standards v${packageJson.version}`) } async validateEnvironment() { this.log('🔍 Validating environment...') // Check Node.js version const nodeVersion = process.version const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0]) if (majorVersion < 18) { throw new Error(`Node.js 18+ required, found ${nodeVersion}`) } // Check if we're in a project directory if (!fs.existsSync(path.join(this.projectRoot, 'package.json'))) { throw new Error('No package.json found. Please run this command in a project directory.') } this.log('✅ Environment validation passed') } async detectProjectType() { this.log('🔍 Detecting project type...') const packageJson = JSON.parse(fs.readFileSync(path.join(this.projectRoot, 'package.json'), 'utf8')) // Check for SvelteKit if (packageJson.devDependencies?.['@sveltejs/kit'] || packageJson.dependencies?.['@sveltejs/kit']) { this.projectType = 'sveltekit' this.log('📦 Detected SvelteKit project') return } // Check for Svelte + Vite if ((packageJson.devDependencies?.svelte || packageJson.dependencies?.svelte) && (packageJson.devDependencies?.vite || packageJson.dependencies?.vite)) { this.projectType = 'svelte-vite' this.log('📦 Detected Svelte + Vite project') return } // Check for general Svelte if (packageJson.devDependencies?.svelte || packageJson.dependencies?.svelte) { this.projectType = 'svelte' this.log('📦 Detected Svelte project') return } // Default to generic frontend this.projectType = 'frontend' this.log('📦 Detected frontend project (generic)') } async createBackup() { this.log('💾 Creating backup...') if (this.options.dryRun) { this.log('📋 Would create backup of existing configuration files') return } // Create backup directory if (fs.existsSync(this.backupDir)) { fs.rmSync(this.backupDir, { recursive: true, force: true }) } fs.mkdirSync(this.backupDir, { recursive: true }) // Backup existing configuration files const configFiles = [ '.eslintrc.js', '.eslintrc.cjs', '.eslintrc.json', '.eslintrc.yaml', '.eslintrc.yml', 'eslint.config.js', '.amazonq.json', 'tsconfig.json', 'vitest.config.ts', 'vitest.config.js', 'playwright.config.ts', 'playwright.config.js', '.vscode/settings.json' ] let backedUpFiles = 0 for (const configFile of configFiles) { const filePath = path.join(this.projectRoot, configFile) if (fs.existsSync(filePath)) { const backupPath = path.join(this.backupDir, configFile) const backupDir = path.dirname(backupPath) if (!fs.existsSync(backupDir)) { fs.mkdirSync(backupDir, { recursive: true }) } fs.copyFileSync(filePath, backupPath) backedUpFiles++ this.log(`📋 Backed up: ${configFile}`) } } // Create backup manifest const backupManifest = { timestamp: new Date().toISOString(), projectType: this.projectType, backedUpFiles: backedUpFiles, nodeVersion: process.version, installerVersion: this.getInstallerVersion() } fs.writeFileSync( path.join(this.backupDir, 'manifest.json'), JSON.stringify(backupManifest, null, 2) ) this.log(`✅ Backup created (${backedUpFiles} files)`) } async installCoreStandards() { this.log('📦 Installing core standards...') const coreStandardsSource = path.join(this.packageRoot, 'core-standards') const coreStandardsTarget = path.join(this.projectRoot, '.goldenpath', 'core-standards') if (this.options.dryRun) { this.log('📋 Would copy core standards to .goldenpath/core-standards/') return } // Create target directory if (fs.existsSync(coreStandardsTarget)) { fs.rmSync(coreStandardsTarget, { recursive: true, force: true }) } fs.mkdirSync(coreStandardsTarget, { recursive: true }) // Copy core standards this.copyDirectory(coreStandardsSource, coreStandardsTarget) this.log('✅ Core standards installed') } async installConfigurations() { this.log('⚙️ Installing configuration files...') if (this.options.dryRun) { this.log('📋 Would install configuration files that reference core standards') return } // Install main ESLint configuration await this.installEslintConfig() // Install Amazon Q configuration await this.installAmazonQConfig() // Install other configurations await this.installOtherConfigs() this.log('✅ Configuration files installed') } async installEslintConfig() { // Check if project uses ESM const packageJsonPath = path.join(this.projectRoot, 'package.json') const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) const isESM = packageJson.type === 'module' // Use .cjs extension for ESM projects, .js for CommonJS const configFileName = isESM ? '.eslintrc.cjs' : '.eslintrc.js' const eslintConfigPath = path.join(this.projectRoot, configFileName) const eslintConfig = `// Golden Path Frontend Standards - Generated Configuration // This file references core standards - do not edit directly // To customize rules, modify the core standards in .goldenpath/core-standards/ module.exports = { root: true, extends: [ './.goldenpath/core-standards/typescript/.eslintrc.cjs', './.goldenpath/core-standards/svelte5/.eslintrc.cjs', './.goldenpath/core-standards/zod/.eslintrc.cjs', './.goldenpath/core-standards/testing/.eslintrc.cjs' ], parserOptions: { sourceType: 'module', ecmaVersion: 2020, extraFileExtensions: ['.svelte'] }, env: { browser: true, es2017: true, node: true } }` fs.writeFileSync(eslintConfigPath, eslintConfig) this.log(`📝 Installed ${configFileName}`) } async installAmazonQConfig() { const amazonqConfigPath = path.join(this.projectRoot, '.amazonq.json') const sourceConfigPath = path.join(this.packageRoot, 'configs', '.amazonq.json') if (fs.existsSync(sourceConfigPath)) { fs.copyFileSync(sourceConfigPath, amazonqConfigPath) this.log('📝 Installed .amazonq.json') } } async installOtherConfigs() { const configMappings = [ { source: 'configs/tsconfig.json', target: 'tsconfig.json' }, { source: 'configs/vitest.config.ts', target: 'vitest.config.ts' }, { source: 'configs/playwright.config.ts', target: 'playwright.config.ts' }, { source: 'configs/.vscode/settings.json', target: '.vscode/settings.json' } ] for (const mapping of configMappings) { const sourcePath = path.join(this.packageRoot, mapping.source) const targetPath = path.join(this.projectRoot, mapping.target) if (fs.existsSync(sourcePath)) { // Create target directory if needed const targetDir = path.dirname(targetPath) if (!fs.existsSync(targetDir)) { fs.mkdirSync(targetDir, { recursive: true }) } fs.copyFileSync(sourcePath, targetPath) this.log(`📝 Installed ${mapping.target}`) } } } async installDependencies() { this.log('📦 Installing dependencies...') if (this.options.dryRun) { this.log('📋 Would install required npm dependencies') return } // Read package.json to check existing dependencies const packageJsonPath = path.join(this.projectRoot, 'package.json') const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) // Required dependencies for Golden Path Standards const requiredDeps = { devDependencies: { 'eslint': '^8.57.0', '@typescript-eslint/eslint-plugin': '^7.0.0', '@typescript-eslint/parser': '^7.0.0', 'eslint-plugin-svelte': '^2.46.0', 'eslint-plugin-playwright': '^2.2.0', 'eslint-plugin-testing-library': '^7.5.3', 'svelte-eslint-parser': '^0.41.0', 'vitest': '^3.2.4', '@playwright/test': '^1.53.2', '@testing-library/svelte': '^5.2.8', '@testing-library/user-event': '^14.6.1', '@testing-library/jest-dom': '^6.6.3' } } // Add missing dependencies let depsToInstall = [] for (const [dep, version] of Object.entries(requiredDeps.devDependencies)) { if (!packageJson.devDependencies?.[dep] && !packageJson.dependencies?.[dep]) { depsToInstall.push(`${dep}@${version}`) } } if (depsToInstall.length > 0) { this.log(`📦 Installing ${depsToInstall.length} missing dependencies...`) try { // Detect package manager const packageManager = this.detectPackageManager() if (packageManager === 'npm') { execSync(`npm install --save-dev ${depsToInstall.join(' ')} --legacy-peer-deps`, { cwd: this.projectRoot, stdio: this.options.verbose ? 'inherit' : 'pipe' }) } else if (packageManager === 'yarn') { execSync(`yarn add --dev ${depsToInstall.join(' ')}`, { cwd: this.projectRoot, stdio: this.options.verbose ? 'inherit' : 'pipe' }) } else if (packageManager === 'pnpm') { execSync(`pnpm add --save-dev ${depsToInstall.join(' ')}`, { cwd: this.projectRoot, stdio: this.options.verbose ? 'inherit' : 'pipe' }) } this.log('✅ Dependencies installed') } catch (error) { throw new Error(`Failed to install dependencies: ${error.message}`) } } else { this.log('✅ All required dependencies already present') } } async validateInstallation() { this.log('🔍 Validating installation...') if (this.options.dryRun) { this.log('📋 Would validate installation (skipped in dry-run mode)') return } // Check that core standards are installed const coreStandardsPath = path.join(this.projectRoot, '.goldenpath', 'core-standards') if (!fs.existsSync(coreStandardsPath)) { throw new Error('Core standards not found after installation') } // Check that configuration files exist const packageJsonPath = path.join(this.projectRoot, 'package.json') const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) const isESM = packageJson.type === 'module' const eslintConfigFile = isESM ? '.eslintrc.cjs' : '.eslintrc.js' const requiredConfigs = [eslintConfigFile, '.amazonq.json'] for (const config of requiredConfigs) { const configPath = path.join(this.projectRoot, config) if (!fs.existsSync(configPath)) { throw new Error(`Configuration file not found: ${config}`) } } // Test ESLint configuration (skip for now to avoid dependency issues) // try { // execSync(`npx eslint --print-config ${eslintConfigFile}`, { // cwd: this.projectRoot, // stdio: 'pipe' // }) // this.log('✅ ESLint configuration valid') // } catch (error) { // throw new Error(`ESLint configuration invalid: ${error.message}`) // } this.log('✅ ESLint configuration installed (validation skipped)') this.log('✅ Installation validation passed') } async cleanupBackup() { if (this.options.dryRun) { return } // Keep backup for rollback capability this.log('💾 Backup preserved for rollback capability') } async rollback() { console.log('🔄 Rolling back Golden Path Frontend Standards...') if (!fs.existsSync(this.backupDir)) { throw new Error('No backup found. Cannot rollback.') } // Read backup manifest const manifestPath = path.join(this.backupDir, 'manifest.json') if (!fs.existsSync(manifestPath)) { throw new Error('Backup manifest not found. Cannot rollback safely.') } const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) console.log(`📋 Restoring backup from ${manifest.timestamp}`) // Remove installed files const installedFiles = [ '.eslintrc.js', '.eslintrc.cjs', '.amazonq.json', 'tsconfig.json', 'vitest.config.ts', 'playwright.config.ts', '.vscode/settings.json', '.goldenpath' ] for (const file of installedFiles) { const filePath = path.join(this.projectRoot, file) if (fs.existsSync(filePath)) { if (fs.statSync(filePath).isDirectory()) { fs.rmSync(filePath, { recursive: true, force: true }) } else { fs.unlinkSync(filePath) } console.log(`🗑️ Removed: ${file}`) } } // Restore backed up files const backupFiles = fs.readdirSync(this.backupDir, { withFileTypes: true }) .filter(dirent => !dirent.name.endsWith('.json')) for (const file of backupFiles) { const backupPath = path.join(this.backupDir, file.name) const restorePath = path.join(this.projectRoot, file.name) if (file.isDirectory()) { this.copyDirectory(backupPath, restorePath) } else { // Create directory if needed const restoreDir = path.dirname(restorePath) if (!fs.existsSync(restoreDir)) { fs.mkdirSync(restoreDir, { recursive: true }) } fs.copyFileSync(backupPath, restorePath) } console.log(`📋 Restored: ${file.name}`) } // Remove backup fs.rmSync(this.backupDir, { recursive: true, force: true }) console.log('') console.log('✅ Rollback completed successfully!') } // Utility methods copyDirectory(src, dest) { if (!fs.existsSync(dest)) { fs.mkdirSync(dest, { recursive: true }) } const entries = fs.readdirSync(src, { withFileTypes: true }) for (const entry of entries) { const srcPath = path.join(src, entry.name) const destPath = path.join(dest, entry.name) if (entry.isDirectory()) { this.copyDirectory(srcPath, destPath) } else { fs.copyFileSync(srcPath, destPath) } } } detectPackageManager() { if (fs.existsSync(path.join(this.projectRoot, 'yarn.lock'))) { return 'yarn' } if (fs.existsSync(path.join(this.projectRoot, 'pnpm-lock.yaml'))) { return 'pnpm' } return 'npm' } getInstallerVersion() { try { const packageJson = JSON.parse(fs.readFileSync(path.join(this.packageRoot, 'package.json'), 'utf8')) return packageJson.version } catch { return 'unknown' } } log(message) { if (this.options.verbose || !message.startsWith('📋')) { console.log(message) } } showNextSteps() { console.log('🎉 Next Steps:') console.log('') console.log('1. Run linting to see the standards in action:') console.log(' npm run lint') console.log('') console.log('2. Run tests to validate your setup:') console.log(' npm run test') console.log('') console.log('3. Check your IDE integration:') console.log(' - VS Code should show real-time linting') console.log(' - Amazon Q should generate compliant code') console.log('') console.log('4. Customize core standards if needed:') console.log(' - Edit files in .goldenpath/core-standards/') console.log(' - Changes will be reflected automatically') console.log('') console.log('📚 Documentation: https://github.com/goldenpath/frontend-standards') } } // CLI interface async function main() { const installer = new FrontendStandardsInstaller() await installer.install() } if (require.main === module) { main().catch(error => { console.error('Fatal error:', error.message) process.exit(1) }) } module.exports = { FrontendStandardsInstaller }