You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

299 lines
6.8 KiB

#!/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
};