Files
securebit-chat/scripts/post-build.js
T
lockbitchat 98d42ac2fb
CodeQL Analysis / Analyze CodeQL (push) Canceled after 0s
Deploy Application / deploy (push) Canceled after 0s
Mirror to Codeberg / mirror (push) Canceled after 0s
Mirror to PrivacyGuides / mirror (push) Canceled after 0s
feat(i18n): nine languages, each at its own address; release v6.3.0
The site now speaks German, French, Spanish, Ukrainian, Russian, Chinese,
Korean and Hindi alongside English — 623 strings per language, 5,607
translations, covering the landing page, the key exchange, the chat, group
calls and every error along the way.

Each locale is a real page at a real URL (/de/, /fr/, …), generated at build
time from locales/*.json. That is the whole point: the app is client-rendered,
so a language that only swaps strings at runtime has no address for a crawler
to index and no link anyone can share. Each page carries its own canonical, a
reciprocal hreflang cluster and translated schema.org.

The URL decides the language, always. A stored preference only applies where
the address does not say, and nothing ever redirects on Accept-Language —
that is how sites become invisible to search engines outside one country.

robots.txt and sitemap.xml did not exist before; both are generated now.

Bugs found on the way, each with a test that would have caught it:
  - partner logos used page-relative paths and 404'd from any /xx/ page
  - post-build stamped ?v= only into the root shell, leaving locales behind
  - "Back online" appeared on every page load, not just after being offline
  - update timestamps were hard-coded to US format for every reader
  - t() threw on a partial window, taking whole components down with it
2026-08-29 12:46:15 -04:00

258 lines
8.1 KiB
JavaScript

/**
* 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);
}
}
/**
* Every generated app shell: the root one plus one per locale subdirectory.
*
* scripts/build-i18n.js runs before this script and stamps the version it reads from
* meta.json — which is still the *previous* build at that point, since meta.json is
* regenerated below. The root page used to be the only one re-stamped here, so the
* localized pages shipped pointing at an older ?v= than the root did.
*/
function appShellPaths() {
const shells = [path.join(CONFIG.publicDir, 'index.html')];
try {
const sitePath = path.join(CONFIG.publicDir, 'locales', 'site.json');
if (fs.existsSync(sitePath)) {
const site = JSON.parse(fs.readFileSync(sitePath, 'utf-8'));
for (const code of site.locales || []) {
if (code === site.defaultLocale) continue;
shells.push(path.join(CONFIG.publicDir, code, 'index.html'));
}
}
} catch (error) {
console.warn('⚠️ Could not read locales/site.json:', error.message);
}
return shells;
}
/**
* Update versions in every index.html
*/
function updateIndexHtmlVersions(buildVersion) {
for (const indexHtmlPath of appShellPaths()) {
const label = path.relative(CONFIG.publicDir, indexHtmlPath);
try {
if (!fs.existsSync(indexHtmlPath)) {
console.warn(`⚠️ ${label} not found, skipping version update`);
continue;
}
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(`✅ ${label} versions updated`);
} catch (error) {
console.warn(`⚠️ Failed to update ${label} versions:`, error.message);
}
}
}
/**
* Stamp the build version into sw.js so the file's bytes change every deploy. The browser
* only reinstalls a Service Worker when sw.js differs byte-for-byte; without this the SW
* (and its caches) stay frozen and clients never pick up new releases automatically.
*/
function updateServiceWorkerVersion(buildVersion) {
try {
const swPath = path.join(CONFIG.publicDir, 'sw.js');
if (!fs.existsSync(swPath)) {
console.warn('⚠️ sw.js not found, skipping SW version stamp');
return;
}
let sw = fs.readFileSync(swPath, 'utf-8');
if (/const SW_BUILD_VERSION = '[^']*';/.test(sw)) {
sw = sw.replace(/const SW_BUILD_VERSION = '[^']*';/, `const SW_BUILD_VERSION = '${buildVersion}';`);
fs.writeFileSync(swPath, sw, 'utf-8');
console.log(`✅ sw.js build stamp updated → ${buildVersion}`);
} else {
console.warn('⚠️ SW_BUILD_VERSION marker not found in sw.js');
}
} catch (error) {
console.warn('⚠️ Failed to stamp sw.js version:', 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);
// Stamp the build version into sw.js so the Service Worker reinstalls each deploy.
updateServiceWorkerVersion(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
};