feat: add platform-specific configuration and build system
- Add Android configuration with notification channels and WorkManager - Add iOS configuration with BGTaskScheduler and notification categories - Add platform-specific build scripts and bundle size checking - Add API change detection and type checksum validation - Add release notes generation and chaos testing scripts - Add Vite configuration for TimeSafari-specific builds - Add Android notification channels XML configuration - Update package.json with new build scripts and dependencies Platforms: Android (WorkManager + SQLite), iOS (BGTaskScheduler + Core Data), Electron (Desktop notifications)
This commit is contained in:
299
scripts/build-timesafari.js
Executable file
299
scripts/build-timesafari.js
Executable file
@@ -0,0 +1,299 @@
|
||||
#!/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
|
||||
};
|
||||
Reference in New Issue
Block a user