First commit of the Hedena Gold product (separate from gold-bank): branding, package names, and deploy defaults aligned to the new brand; legacy Henza trees stay local-only via .gitignore.
88 lines
3.3 KiB
JavaScript
88 lines
3.3 KiB
JavaScript
// 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) });
|
||
},
|
||
};
|
||
}
|