Spaces:
Build error
Build error
| /** | |
| * Build and Minification Script for Spotify Web Player | |
| * Optimizes JavaScript and CSS for production deployment | |
| */ | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const { promisify } = require('util'); | |
| // Promisify file system operations | |
| const readFile = promisify(fs.readFile); | |
| const writeFile = promisify(fs.writeFile); | |
| const readdir = promisify(fs.readdir); | |
| const stat = promisify(fs.stat); | |
| const mkdir = promisify(fs.mkdir); | |
| class BuildMinifier { | |
| constructor() { | |
| this.sourceDir = path.join(__dirname, '..', 'public'); | |
| this.outputDir = path.join(__dirname, '..', 'public', 'dist'); | |
| this.stats = { | |
| filesProcessed: 0, | |
| originalSize: 0, | |
| compressedSize: 0, | |
| compressionRatio: 0 | |
| }; | |
| } | |
| /** | |
| * Run the complete build process | |
| */ | |
| async build() { | |
| console.log('🚀 Starting build process...\n'); | |
| try { | |
| // Ensure output directory exists | |
| await this.ensureOutputDir(); | |
| // Build JavaScript bundle | |
| await this.buildJavaScript(); | |
| // Build CSS bundle | |
| await this.buildCSS(); | |
| // Copy critical assets | |
| await this.copyAssets(); | |
| // Generate manifest | |
| await this.generateManifest(); | |
| // Display build statistics | |
| this.displayStats(); | |
| console.log('\n✅ Build completed successfully!'); | |
| } catch (error) { | |
| console.error('\n❌ Build failed:', error.message); | |
| process.exit(1); | |
| } | |
| } | |
| /** | |
| * Ensure output directory exists | |
| */ | |
| async ensureOutputDir() { | |
| try { | |
| await mkdir(this.outputDir, { recursive: true }); | |
| console.log('📁 Output directory created'); | |
| } catch (error) { | |
| if (error.code !== 'EEXIST') { | |
| throw error; | |
| } | |
| } | |
| } | |
| /** | |
| * Build and minify JavaScript files | |
| */ | |
| async buildJavaScript() { | |
| console.log('📦 Building JavaScript bundle...'); | |
| // Define the order of JavaScript files for proper dependency loading | |
| const jsFiles = [ | |
| // Core services first | |
| 'js/services/StateManager.js', | |
| 'js/services/DebounceService.js', | |
| 'js/services/CacheService.js', | |
| 'js/services/SecurityService.js', | |
| 'js/services/DOMSanitizer.js', | |
| // Enhanced services | |
| 'js/services/AccessibilityService.js', | |
| 'js/services/SkeletonService.js', | |
| 'js/services/OfflineService.js', | |
| 'js/services/TouchGestureService.js', | |
| 'js/services/ErrorRecoveryService.js', | |
| 'js/services/ErrorBoundaryService.js', | |
| 'js/services/SpotifyAPIService.js', | |
| 'js/services/StateChangeDetectionService.js', | |
| // Components | |
| 'js/components/VirtualScrollComponent.js', | |
| 'js/components/PlaylistComponent.js', | |
| 'js/components/PlayerComponent.js', | |
| 'js/components/DeviceComponent.js', | |
| 'js/components/NavigationComponent.js', | |
| // Main application | |
| 'js/SpotifyWebPlayer.js' | |
| ]; | |
| let combinedJS = ''; | |
| let originalSize = 0; | |
| // Add header comment | |
| combinedJS += `/*!\n * Spotify Web Player - Production Bundle\n * Built on ${new Date().toISOString()}\n */\n\n`; | |
| // Combine all JavaScript files | |
| for (const file of jsFiles) { | |
| const filePath = path.join(this.sourceDir, file); | |
| try { | |
| const content = await readFile(filePath, 'utf8'); | |
| const fileSize = Buffer.byteLength(content, 'utf8'); | |
| originalSize += fileSize; | |
| // Add file separator comment | |
| combinedJS += `\n/* === ${file} === */\n`; | |
| combinedJS += content; | |
| combinedJS += '\n'; | |
| console.log(` ✓ ${file} (${this.formatBytes(fileSize)})`); | |
| } catch (error) { | |
| console.warn(` ⚠️ Warning: Could not read ${file}: ${error.message}`); | |
| } | |
| } | |
| // Simple minification (remove comments and extra whitespace) | |
| const minifiedJS = this.minifyJavaScript(combinedJS); | |
| const compressedSize = Buffer.byteLength(minifiedJS, 'utf8'); | |
| // Write minified bundle | |
| const outputPath = path.join(this.outputDir, 'app.min.js'); | |
| await writeFile(outputPath, minifiedJS, 'utf8'); | |
| // Update stats | |
| this.stats.originalSize += originalSize; | |
| this.stats.compressedSize += compressedSize; | |
| this.stats.filesProcessed += jsFiles.length; | |
| console.log(` 📄 Bundle created: app.min.js (${this.formatBytes(compressedSize)})`); | |
| console.log(` 💾 Compression: ${this.formatBytes(originalSize)} → ${this.formatBytes(compressedSize)} (${Math.round((1 - compressedSize/originalSize) * 100)}% reduction)\n`); | |
| } | |
| /** | |
| * Build and minify CSS files | |
| */ | |
| async buildCSS() { | |
| console.log('🎨 Building CSS bundle...'); | |
| const cssFiles = [ | |
| 'css/styles.css' | |
| ]; | |
| let combinedCSS = ''; | |
| let originalSize = 0; | |
| // Add header comment | |
| combinedCSS += `/*!\n * Spotify Web Player - Styles\n * Built on ${new Date().toISOString()}\n */\n\n`; | |
| // Combine all CSS files | |
| for (const file of cssFiles) { | |
| const filePath = path.join(this.sourceDir, file); | |
| try { | |
| const content = await readFile(filePath, 'utf8'); | |
| const fileSize = Buffer.byteLength(content, 'utf8'); | |
| originalSize += fileSize; | |
| combinedCSS += content; | |
| combinedCSS += '\n'; | |
| console.log(` ✓ ${file} (${this.formatBytes(fileSize)})`); | |
| } catch (error) { | |
| console.warn(` ⚠️ Warning: Could not read ${file}: ${error.message}`); | |
| } | |
| } | |
| // Simple CSS minification | |
| const minifiedCSS = this.minifyCSS(combinedCSS); | |
| const compressedSize = Buffer.byteLength(minifiedCSS, 'utf8'); | |
| // Write minified bundle | |
| const outputPath = path.join(this.outputDir, 'styles.min.css'); | |
| await writeFile(outputPath, minifiedCSS, 'utf8'); | |
| // Update stats | |
| this.stats.originalSize += originalSize; | |
| this.stats.compressedSize += compressedSize; | |
| this.stats.filesProcessed += cssFiles.length; | |
| console.log(` 📄 Bundle created: styles.min.css (${this.formatBytes(compressedSize)})`); | |
| console.log(` 💾 Compression: ${this.formatBytes(originalSize)} → ${this.formatBytes(compressedSize)} (${Math.round((1 - compressedSize/originalSize) * 100)}% reduction)\n`); | |
| } | |
| /** | |
| * Copy critical assets | |
| */ | |
| async copyAssets() { | |
| console.log('📋 Copying critical assets...'); | |
| // Copy service worker (no minification needed) | |
| const swSource = path.join(this.sourceDir, 'sw.js'); | |
| const swDest = path.join(this.outputDir, 'sw.js'); | |
| try { | |
| const swContent = await readFile(swSource, 'utf8'); | |
| await writeFile(swDest, swContent, 'utf8'); | |
| console.log(' ✓ Service Worker copied'); | |
| } catch (error) { | |
| console.warn(' ⚠️ Warning: Could not copy service worker:', error.message); | |
| } | |
| // Create production HTML file | |
| await this.createProductionHTML(); | |
| } | |
| /** | |
| * Create production HTML file with minified assets | |
| */ | |
| async createProductionHTML() { | |
| const htmlTemplate = `<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> | |
| <meta name="mobile-web-app-capable" content="yes"> | |
| <meta name="apple-mobile-web-app-capable" content="yes"> | |
| <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> | |
| <!-- Security Headers --> | |
| <meta http-equiv="X-Content-Type-Options" content="nosniff"> | |
| <meta http-equiv="X-Frame-Options" content="DENY"> | |
| <meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin"> | |
| <meta name="robots" content="noindex, nofollow"> | |
| <title>Spotify Web Player</title> | |
| <link rel="stylesheet" href="dist/styles.min.css"> | |
| </head> | |
| <body> | |
| <!-- Skip link for accessibility --> | |
| <a href="#main-content" class="skip-link" style="position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden;">Skip to main content</a> | |
| <div class="nav-header"> | |
| <div class="nav-title">Spotify Web Player</div> | |
| <div class="nav-buttons"> | |
| <button class="nav-btn" title="Back">Back</button> | |
| <button class="nav-btn" title="Logout">Logout</button> | |
| </div> | |
| </div> | |
| <div class="container"> | |
| <div class="header"> | |
| <div class="filter-section" id="filter-section"> | |
| <div class="filter-buttons"> | |
| <div class="filter-left"> | |
| <button class="filter-btn active" data-filter="mine">My Playlists</button> | |
| <button class="filter-btn" data-filter="others">Others</button> | |
| </div> | |
| <div class="filter-right"> | |
| <button class="nav-btn" title="Back">◀</button> | |
| <button class="nav-btn" title="Logout">↗</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="player-section" id="player-section"> | |
| <div class="current-track"> | |
| <div class="track-info"> | |
| <div class="track-title" id="current-title">Track</div> | |
| <div class="track-artist" id="current-artist">Artist</div> | |
| </div> | |
| </div> | |
| <div class="controls"> | |
| <button class="control-btn" id="prev-btn" disabled title="Previous">⏮</button> | |
| <button class="control-btn" id="play-btn" disabled title="Play/Pause">⏯</button> | |
| <button class="control-btn" id="next-btn" disabled title="Next">⏭</button> | |
| <button class="control-btn" id="devices-btn" disabled title="Available devices">📱</button> | |
| </div> | |
| <div id="device-list" style="display: none;"> | |
| <div class="device-header">Available Devices</div> | |
| <div id="devices-container"></div> | |
| </div> | |
| </div> | |
| <div class="playlist-section" id="main-content" role="main" aria-label="Music playlists and tracks"> | |
| <div id="loading" class="loading">Loading your playlists...</div> | |
| <div id="error" class="error" style="display: none;" role="alert" aria-live="assertive"></div> | |
| <div id="playlist-list" role="list" aria-label="Music playlists"></div> | |
| <div id="track-list" style="display: none;" role="list" aria-label="Playlist tracks"></div> | |
| </div> | |
| </div> | |
| <!-- Load minified application bundle --> | |
| <script src="dist/app.min.js"></script> | |
| <!-- Load Spotify SDK after our code is defined --> | |
| <script src="https://sdk.scdn.co/spotify-player.js"></script> | |
| </body> | |
| </html>`; | |
| const outputPath = path.join(this.outputDir, 'index.html'); | |
| await writeFile(outputPath, htmlTemplate, 'utf8'); | |
| console.log(' ✓ Production HTML created'); | |
| } | |
| /** | |
| * Generate build manifest | |
| */ | |
| async generateManifest() { | |
| console.log('📋 Generating build manifest...'); | |
| const manifest = { | |
| buildTime: new Date().toISOString(), | |
| version: '1.0.0', | |
| files: { | |
| 'app.min.js': await this.getFileHash(path.join(this.outputDir, 'app.min.js')), | |
| 'styles.min.css': await this.getFileHash(path.join(this.outputDir, 'styles.min.css')), | |
| 'index.html': await this.getFileHash(path.join(this.outputDir, 'index.html')) | |
| }, | |
| stats: this.stats | |
| }; | |
| const manifestPath = path.join(this.outputDir, 'manifest.json'); | |
| await writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf8'); | |
| console.log(' ✓ Build manifest created'); | |
| } | |
| /** | |
| * Simple JavaScript minification | |
| */ | |
| minifyJavaScript(code) { | |
| return code | |
| // Remove single-line comments (but preserve URLs) | |
| .replace(/\/\/.*$/gm, '') | |
| // Remove multi-line comments | |
| .replace(/\/\*[\s\S]*?\*\//g, '') | |
| // Remove extra whitespace | |
| .replace(/\s+/g, ' ') | |
| // Remove whitespace around operators | |
| .replace(/\s*([{}();,])\s*/g, '$1') | |
| // Remove leading/trailing whitespace | |
| .trim(); | |
| } | |
| /** | |
| * Simple CSS minification | |
| */ | |
| minifyCSS(code) { | |
| return code | |
| // Remove comments | |
| .replace(/\/\*[\s\S]*?\*\//g, '') | |
| // Remove extra whitespace | |
| .replace(/\s+/g, ' ') | |
| // Remove whitespace around CSS syntax | |
| .replace(/\s*([{}:;,>+~])\s*/g, '$1') | |
| // Remove trailing semicolons | |
| .replace(/;}/g, '}') | |
| // Remove leading/trailing whitespace | |
| .trim(); | |
| } | |
| /** | |
| * Get file hash for cache busting | |
| */ | |
| async getFileHash(filePath) { | |
| try { | |
| const content = await readFile(filePath, 'utf8'); | |
| const crypto = require('crypto'); | |
| return crypto.createHash('md5').update(content).digest('hex').substring(0, 8); | |
| } catch (error) { | |
| return 'unknown'; | |
| } | |
| } | |
| /** | |
| * Format bytes for display | |
| */ | |
| formatBytes(bytes) { | |
| if (bytes === 0) return '0 B'; | |
| const k = 1024; | |
| const sizes = ['B', 'KB', 'MB', 'GB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; | |
| } | |
| /** | |
| * Display build statistics | |
| */ | |
| displayStats() { | |
| console.log('\n📊 BUILD STATISTICS'); | |
| console.log('==================='); | |
| console.log(`Files processed: ${this.stats.filesProcessed}`); | |
| console.log(`Original size: ${this.formatBytes(this.stats.originalSize)}`); | |
| console.log(`Compressed size: ${this.formatBytes(this.stats.compressedSize)}`); | |
| if (this.stats.originalSize > 0) { | |
| const ratio = Math.round((1 - this.stats.compressedSize / this.stats.originalSize) * 100); | |
| console.log(`Compression ratio: ${ratio}%`); | |
| } | |
| } | |
| } | |
| // Run the build process if this script is executed directly | |
| if (require.main === module) { | |
| const builder = new BuildMinifier(); | |
| builder.build().catch(error => { | |
| console.error('Build failed:', error); | |
| process.exit(1); | |
| }); | |
| } | |
| module.exports = BuildMinifier; |