#!/usr/bin/env node /** * TimeSafari Build Script * * Integrates the Daily Notification Plugin with TimeSafari PWA's build system. * Handles platform-specific builds, tree-shaking, and SSR safety validation. * * @author Matthew Raymer * @version 1.0.0 */ const { execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); // Configuration const CONFIG = { platforms: ['android', 'ios', 'electron'], bundleSizeBudget: 35, // KB ssrSafe: true, treeShaking: true }; /** * Log with timestamp */ function log(message, level = 'INFO') { const timestamp = new Date().toISOString(); console.log(`[${timestamp}] [${level}] ${message}`); } /** * Check if file exists */ function fileExists(filePath) { return fs.existsSync(filePath); } /** * Get file size in KB (gzipped) */ function getFileSizeKB(filePath) { if (!fileExists(filePath)) return 0; try { // Use gzip to get compressed size const content = fs.readFileSync(filePath); const zlib = require('zlib'); const gzipped = zlib.gzipSync(content); return Math.round(gzipped.length / 1024 * 100) / 100; } catch (error) { // Fallback to uncompressed size const stats = fs.statSync(filePath); return Math.round(stats.size / 1024 * 100) / 100; } } /** * Validate SSR safety */ function validateSSRSafety() { log('Validating SSR safety...'); // Only validate core files, not platform-specific implementations const coreFiles = [ 'src/index.ts', 'src/timesafari-integration.ts', 'src/timesafari-community-integration.ts' ]; const ssrUnsafePatterns = [ /window\./g, /document\./g, /navigator\./g ]; let hasUnsafeCode = false; coreFiles.forEach(filePath => { if (!fileExists(filePath)) return; const content = fs.readFileSync(filePath, 'utf8'); ssrUnsafePatterns.forEach(pattern => { if (pattern.test(content)) { log(`SSR-unsafe code found in ${filePath}`, 'WARN'); hasUnsafeCode = true; } }); }); if (hasUnsafeCode) { log('SSR safety validation failed for core files', 'ERROR'); return false; } // Check platform-specific files for proper guards const platformFiles = [ 'src/web/index.ts', 'src/timesafari-storage-adapter.ts' ]; platformFiles.forEach(filePath => { if (!fileExists(filePath)) return; const content = fs.readFileSync(filePath, 'utf8'); // Check if file has proper platform guards const hasPlatformGuards = content.includes('typeof window') || content.includes('typeof document') || content.includes('platform check'); if (!hasPlatformGuards && (content.includes('window.') || content.includes('document.'))) { log(`Platform-specific file ${filePath} should have platform guards`, 'WARN'); } }); log('SSR safety validation passed'); return true; } /** * Validate tree-shaking */ function validateTreeShaking() { log('Validating tree-shaking...'); // Check if sideEffects is set to false in package.json const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); if (packageJson.sideEffects !== false) { log('sideEffects not set to false in package.json', 'WARN'); return false; } // Check exports map if (!packageJson.exports) { log('exports map not defined in package.json', 'WARN'); return false; } log('Tree-shaking validation passed'); return true; } /** * Build for specific platform */ function buildForPlatform(platform) { log(`Building for platform: ${platform}`); try { // Set platform-specific environment variables process.env.TIMESAFARI_PLATFORM = platform; // Run build command execSync('npm run build', { stdio: 'inherit' }); // Validate build output const expectedFiles = [ 'dist/plugin.js', 'dist/esm/index.js', 'dist/esm/index.d.ts' ]; expectedFiles.forEach(file => { if (!fileExists(file)) { throw new Error(`Expected file not found: ${file}`); } }); log(`Build completed for platform: ${platform}`); return true; } catch (error) { log(`Build failed for platform: ${platform} - ${error.message}`, 'ERROR'); return false; } } /** * Validate bundle size */ function validateBundleSize() { log('Validating bundle size...'); const bundleFiles = [ 'dist/plugin.js', 'dist/esm/index.js' ]; let totalSize = 0; bundleFiles.forEach(file => { const size = getFileSizeKB(file); totalSize += size; log(` ${file}: ${size}KB`); }); log(`Total bundle size: ${totalSize}KB`); if (totalSize > CONFIG.bundleSizeBudget) { log(`Bundle size (${totalSize}KB) exceeds budget (${CONFIG.bundleSizeBudget}KB)`, 'ERROR'); return false; } log('Bundle size validation passed'); return true; } /** * Generate build report */ function generateBuildReport() { log('Generating build report...'); const report = { timestamp: new Date().toISOString(), platform: process.env.TIMESAFARI_PLATFORM || 'all', bundleSize: { plugin: getFileSizeKB('dist/plugin.js'), esm: getFileSizeKB('dist/esm/index.js'), total: getFileSizeKB('dist/plugin.js') + getFileSizeKB('dist/esm/index.js') }, validation: { ssrSafe: CONFIG.ssrSafe, treeShaking: CONFIG.treeShaking, bundleSizeBudget: CONFIG.bundleSizeBudget }, files: { plugin: fileExists('dist/plugin.js'), esm: fileExists('dist/esm/index.js'), types: fileExists('dist/esm/index.d.ts') } }; // Write report to file fs.writeFileSync('dist/build-report.json', JSON.stringify(report, null, 2)); log('Build report generated: dist/build-report.json'); return report; } /** * Main build function */ function main() { log('Starting TimeSafari build process...'); // Validate configuration if (!validateSSRSafety()) { process.exit(1); } if (!validateTreeShaking()) { process.exit(1); } // Build for all platforms const platform = process.env.TIMESAFARI_PLATFORM || 'all'; if (platform === 'all') { CONFIG.platforms.forEach(p => { if (!buildForPlatform(p)) { process.exit(1); } }); } else { if (!buildForPlatform(platform)) { process.exit(1); } } // Validate bundle size if (!validateBundleSize()) { process.exit(1); } // Generate build report const report = generateBuildReport(); log('TimeSafari build process completed successfully'); log(`Bundle size: ${report.bundleSize.total}KB`); log(`Platform: ${report.platform}`); } // Run if called directly if (require.main === module) { main(); } module.exports = { buildForPlatform, validateSSRSafety, validateTreeShaking, validateBundleSize, generateBuildReport };