90 lines
3.3 KiB
JavaScript
90 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:
|
|||
|
|
//
|
|||
|
|
// اپ: … both customer apps
|
|||
|
|
// اپ۱: … user-app only
|
|||
|
|
// اپ۲: … user-app-v2 only
|
|||
|
|
// پنل: … 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'],
|
|||
|
|
'user-app-v2': ['اپ', 'اپ۲', 'اپ2'],
|
|||
|
|
'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) });
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
}
|