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
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
|
|
};
|
|
|