feat: implement comprehensive PWA force update system
Some checks are pending
CodeQL Analysis / Analyze CodeQL (push) Waiting to run
Deploy Application / deploy (push) Waiting to run
Mirror to Codeberg / mirror (push) Waiting to run
Mirror to PrivacyGuides / mirror (push) Waiting to run

- Add UpdateManager and UpdateChecker for automatic version detection
- Add post-build script for meta.json generation and version injection
- Enhance Service Worker with version-aware caching
- Add .htaccess configuration for proper cache control

This ensures all users receive the latest version after deployment
without manual cache clearing.
This commit is contained in:
lockbitchat
2025-12-29 10:51:07 -04:00
parent 1b6431a36b
commit 91c292a6cf
20 changed files with 1606 additions and 74 deletions

203
scripts/post-build.js Normal file
View File

@@ -0,0 +1,203 @@
/**
* post-build.js - Script for generating meta.json after build
*
* Generates meta.json file with unique build version (timestamp)
* for automatic update detection
*/
const fs = require('fs');
const path = require('path');
// Configuration
const CONFIG = {
// Path to public directory (project root where index.html is located)
publicDir: path.join(__dirname, '..'),
// meta.json filename
metaFileName: 'meta.json',
// Version format: 'timestamp' or 'semver'
versionFormat: 'timestamp'
};
/**
* Generate unique build version
*/
function generateBuildVersion() {
// Use timestamp for uniqueness of each build
const timestamp = Date.now();
// Optional: can add git commit hash
let gitHash = '';
try {
const { execSync } = require('child_process');
gitHash = execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim();
} catch (error) {
// Git not available or not initialized - ignore
}
return {
version: timestamp.toString(),
buildTime: new Date().toISOString(),
gitHash: gitHash || null,
buildId: `${timestamp}${gitHash ? `-${gitHash}` : ''}`
};
}
/**
* Read package.json to get application version
*/
function getAppVersion() {
try {
const packageJsonPath = path.join(__dirname, '..', 'package.json');
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
return packageJson.version || '1.0.0';
}
} catch (error) {
console.warn('⚠️ Failed to read package.json:', error.message);
}
return '1.0.0';
}
/**
* Generate meta.json
*/
function generateMetaJson() {
try {
const buildInfo = generateBuildVersion();
const appVersion = getAppVersion();
const meta = {
// Build version (used for update checking)
version: buildInfo.version,
buildVersion: buildInfo.version,
// Application version from package.json
appVersion: appVersion,
// Additional information
buildTime: buildInfo.buildTime,
buildId: buildInfo.buildId,
gitHash: buildInfo.gitHash,
// Metadata
generated: true,
generatedAt: new Date().toISOString()
};
// Path to meta.json file (in project root where index.html is located)
const metaFilePath = path.join(CONFIG.publicDir, CONFIG.metaFileName);
// Create directory if it doesn't exist
const publicDir = path.dirname(metaFilePath);
if (!fs.existsSync(publicDir)) {
fs.mkdirSync(publicDir, { recursive: true });
console.log(`✅ Created directory: ${publicDir}`);
}
// Write meta.json
fs.writeFileSync(
metaFilePath,
JSON.stringify(meta, null, 2),
'utf-8'
);
console.log('✅ meta.json generated successfully');
console.log(` Version: ${meta.version}`);
console.log(` Build Time: ${meta.buildTime}`);
if (meta.gitHash) {
console.log(` Git Hash: ${meta.gitHash}`);
}
console.log(` File: ${metaFilePath}`);
return meta;
} catch (error) {
console.error('❌ Failed to generate meta.json:', error);
process.exit(1);
}
}
/**
* Update versions in index.html
*/
function updateIndexHtmlVersions(buildVersion) {
try {
const indexHtmlPath = path.join(CONFIG.publicDir, 'index.html');
if (!fs.existsSync(indexHtmlPath)) {
console.warn('⚠️ index.html not found, skipping version update');
return;
}
let indexHtml = fs.readFileSync(indexHtmlPath, 'utf-8');
// Update versions in query parameters for JS files
// Pattern: src="dist/app.js?v=..." or src="dist/app-boot.js?v=..."
// Also replace BUILD_VERSION placeholder
indexHtml = indexHtml.replace(/\?v=BUILD_VERSION/g, `?v=${buildVersion}`);
indexHtml = indexHtml.replace(/\?v=(\d+)/g, `?v=${buildVersion}`);
fs.writeFileSync(indexHtmlPath, indexHtml, 'utf-8');
console.log('✅ index.html versions updated');
} catch (error) {
console.warn('⚠️ Failed to update index.html versions:', error.message);
}
}
/**
* Validate generated meta.json
*/
function validateMetaJson(meta) {
const requiredFields = ['version', 'buildVersion', 'buildTime'];
const missingFields = requiredFields.filter(field => !meta[field]);
if (missingFields.length > 0) {
throw new Error(`Missing required fields: ${missingFields.join(', ')}`);
}
if (!/^\d+$/.test(meta.version)) {
throw new Error(`Invalid version format: ${meta.version} (expected timestamp)`);
}
console.log('✅ meta.json validation passed');
}
// Main function
function main() {
console.log('🔨 Generating meta.json...');
console.log(` Public directory: ${CONFIG.publicDir}`);
// Check if public directory exists
if (!fs.existsSync(CONFIG.publicDir)) {
console.error(`❌ Public directory not found: ${CONFIG.publicDir}`);
process.exit(1);
}
// Generate meta.json
const meta = generateMetaJson();
// Validate
validateMetaJson(meta);
// Update versions in index.html
updateIndexHtmlVersions(meta.version);
console.log('✅ Build metadata generation completed');
}
// Run script
if (require.main === module) {
main();
}
// Export for use in other scripts
module.exports = {
generateMetaJson,
generateBuildVersion,
getAppVersion,
validateMetaJson
};

View File

@@ -0,0 +1,94 @@
/**
* Скрипт для очистки кеша Cloudflare после деплоя
*
* Использование:
* CLOUDFLARE_API_TOKEN=your_token CLOUDFLARE_ZONE_ID=your_zone_id node scripts/purge-cloudflare-cache.js
*/
const https = require('https');
const API_TOKEN = process.env.CLOUDFLARE_API_TOKEN;
const ZONE_ID = process.env.CLOUDFLARE_ZONE_ID;
const DOMAIN = process.env.CLOUDFLARE_DOMAIN || 'securebit.chat';
if (!API_TOKEN || !ZONE_ID) {
console.error('❌ Missing required environment variables:');
console.error(' CLOUDFLARE_API_TOKEN - Cloudflare API Token');
console.error(' CLOUDFLARE_ZONE_ID - Cloudflare Zone ID');
process.exit(1);
}
// Критичные файлы для очистки
const CRITICAL_FILES = [
`https://${DOMAIN}/meta.json`,
`https://${DOMAIN}/index.html`,
`https://${DOMAIN}/sw.js`,
`https://${DOMAIN}/manifest.json`
];
async function purgeCache(files) {
return new Promise((resolve, reject) => {
const data = JSON.stringify({
files: files
});
const options = {
hostname: 'api.cloudflare.com',
port: 443,
path: `/client/v4/zones/${ZONE_ID}/purge_cache`,
method: 'POST',
headers: {
'Authorization': `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json',
'Content-Length': data.length
}
};
const req = https.request(options, (res) => {
let responseData = '';
res.on('data', (chunk) => {
responseData += chunk;
});
res.on('end', () => {
if (res.statusCode === 200) {
const result = JSON.parse(responseData);
if (result.success) {
resolve(result);
} else {
reject(new Error(JSON.stringify(result.errors)));
}
} else {
reject(new Error(`HTTP ${res.statusCode}: ${responseData}`));
}
});
});
req.on('error', (error) => {
reject(error);
});
req.write(data);
req.end();
});
}
async function main() {
console.log('🔄 Purging Cloudflare cache...');
console.log(` Zone ID: ${ZONE_ID}`);
console.log(` Domain: ${DOMAIN}`);
console.log(` Files: ${CRITICAL_FILES.length}`);
try {
const result = await purgeCache(CRITICAL_FILES);
console.log('✅ Cache purged successfully');
console.log(` Purged files: ${result.result.files?.length || 0}`);
} catch (error) {
console.error('❌ Failed to purge cache:', error.message);
process.exit(1);
}
}
main();