Files
hedena_gold/scripts/app-version.mjs
T

88 lines
3.3 KiB
JavaScript
Raw Normal View History

2026-10-15 17:19:10 +03:30
// App version + «what changed» notes, baked into each front-end at build time.
//
// Version: the build number is the commit count of HEAD, so it only ever goes
// up and needs no file to bump. It is compiled into the bundle (__APP_BUILD__)
// and written to dist/version.json; the running app polls that file and, when
// the server's build is newer than its own, shows the update modal.
//
// Notes come from commit messages. Technical text is never shown: only lines
// that start with a Persian tag, written for the person using that part:
//
// اپ: … the customer app
// اپ۱: … user-app (alias of اپ)
// پنل: … the admin panel
//
// A commit may carry several such lines. Each app shows only its own.
import { execFileSync } from 'node:child_process';
const TAGS = {
'user-app': ['اپ', 'اپ۱', 'اپ1'],
'admin-panel': ['پنل'],
};
/** How many released builds with notes the file keeps. */
const HISTORY = 30;
function git(args, cwd) {
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
}
/** Pure, for testing: the note lines in one commit message that belong to `app`. */
export function notesFor(app, message) {
const tags = TAGS[app] ?? [];
const out = [];
for (const raw of message.split(/\r?\n/)) {
const m = /^\s*([^\s::]+)\s*[::]\s*(.+?)\s*$/.exec(raw);
// The note must be Persian: a wrapped English line that happens to begin
// with a tag (e.g. in a commit body explaining the tags) is not a note.
if (m && tags.includes(m[1]) && /[؀-ۿ]/.test(m[2])) out.push(m[2]);
}
return out;
}
export function computeAppVersion(app, cwd = process.cwd()) {
const builtAt = new Date().toISOString();
try {
const build = Number(git(['rev-list', '--count', 'HEAD'], cwd).trim());
const commit = git(['rev-parse', '--short', 'HEAD'], cwd).trim();
// Newest first; a record separator between commits, unit separator between fields.
const log = git(['log', '-n', '400', '--format=%ct%x1f%B%x1e'], cwd);
const history = [];
log.split('\x1e').forEach((rec, i) => {
const [ts, body] = rec.replace(/^\s+/, '').split('\x1f');
if (!body) return;
const notes = notesFor(app, body);
if (!notes.length) return;
history.push({ build: build - i, date: new Date(Number(ts) * 1000).toISOString(), notes });
});
return { app, build, version: `1.${build}`, commit, builtAt, history: history.slice(0, HISTORY) };
} catch {
// Not a git checkout (e.g. a copied tree): still a valid, if note-less, build.
const build = Math.floor(Date.now() / 1000);
return { app, build, version: `1.${build}`, commit: null, builtAt, history: [] };
}
}
/**
* Vite plugin: defines __APP_BUILD__ / __APP_VERSION__ and emits version.json.
* The dev server has no version.json, so the update check stays quiet there.
*/
export function appVersion(app) {
const info = computeAppVersion(app);
return {
name: 'app-version',
config() {
return {
define: {
__APP_BUILD__: JSON.stringify(info.build),
__APP_VERSION__: JSON.stringify(info.version),
},
};
},
generateBundle() {
this.emitFile({ type: 'asset', fileName: 'version.json', source: JSON.stringify(info, null, 2) });
},
};
}