waffensachkunde
Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.
| 1 | /** |
| 2 | * Wie aktuell ist das gepackte Paket unter `release/win-unpacked/`? |
| 3 | * |
| 4 | * ## Warum es diese Datei gibt |
| 5 | * |
| 6 | * `e2e/gepackt.spec.ts` prüft die **gepackte** Anwendung – die Auflösung der |
| 7 | * Pfade unter `resources/`, die Versionsanzeige, den Start aus dem Paket |
| 8 | * heraus. Diese Prüfungen liefen bisher gegen das, was gerade in |
| 9 | * `release/win-unpacked/` lag, gleich wie alt es war. |
| 10 | * |
| 11 | * Das hat in diesem Projekt **zweimal** einen echten Fehler verdeckt: |
| 12 | * |
| 13 | * 1. Beim Umbau der Reifekennzahl hing die Prüfung weiter am Wortlaut |
| 14 | * „0 von 575 Fragen sicher“, den es nicht mehr gab. |
| 15 | * 2. Als der Systemzustand den Quellort des Fragenkatalogs bekam, wurde der |
| 16 | * Locator `.statusliste code` mehrdeutig. |
| 17 | * |
| 18 | * Beide Male meldete `npm run gate` grün, und beide Male fiel es erst beim |
| 19 | * nächsten Paketbau auf – weil das Paket bis dahin die Änderung gar nicht |
| 20 | * enthielt. |
| 21 | * |
| 22 | * `bauPruefen()` in `e2e/electron-hilfe.ts` führt dieses Argument für `out/` |
| 23 | * schon selbst: „Ein Bau von gestern gegen den Quelltext von heute ist |
| 24 | * schlimmer als gar keiner: Der Lauf ist grün und misst die falsche |
| 25 | * Anwendung.“ Auf das Paket wurde es nie angewandt. |
| 26 | * |
| 27 | * ## Warum der Vergleich nicht bei `src/` aufhört |
| 28 | * |
| 29 | * Die erste Fassung verglich das Paket allein mit `app/src`. Das ist die |
| 30 | * halbe Lieferung: Der **Inhalt** – Fragenkatalog, Erklärungen, Glossar, |
| 31 | * Prüfzeichen – liegt gar nicht unter `app/src`, sondern unter `content/` |
| 32 | * und wird über `extraResources` in `electron-builder.yml` mitgepackt. Wer |
| 33 | * eine Erklärung ändert und dann prüft, bekam ein Paket gemeldet, das „so |
| 34 | * jung wie der Quelltext“ sei – und die Prüfungen maßen die vorige |
| 35 | * Fassung der Inhalte und meldeten grün. Genau die Fehlerklasse, gegen die |
| 36 | * diese Datei angelegt wurde, nur eine Tür weiter. |
| 37 | * |
| 38 | * Deshalb liest der Vergleich die Liste der mitgelieferten Pfade **aus |
| 39 | * `electron-builder.yml`** statt sie hier noch einmal aufzuschreiben. Zwei |
| 40 | * Listen desselben Inhalts laufen beim nächsten Umbau auseinander, und dann |
| 41 | * schweigt die Wache wieder. Was electron-builder packt, wird geprüft; was |
| 42 | * dazukommt, wird ohne Zutun mitgeprüft. |
| 43 | * |
| 44 | * ## Warum eine eigene Datei und kein zweiter Abgleich |
| 45 | * |
| 46 | * Zwei Stellen brauchen dieselbe Antwort: die Prüfung selbst, damit sie sich |
| 47 | * mit Grund überspringt, und `gate-stempel.mjs`, damit der Gate-Bericht sagt, |
| 48 | * was er **nicht** geprüft hat. Zwei Umsetzungen desselben Vergleichs liefen |
| 49 | * irgendwann auseinander, und dann widerspräche der Bericht dem Lauf. |
| 50 | * |
| 51 | * Als `.mjs` und nicht als TypeScript, weil `gate-stempel.mjs` sie ohne |
| 52 | * Übersetzungsschritt lädt. |
| 53 | */ |
| 54 | |
| 55 | import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; |
| 56 | import { dirname, join, relative, resolve } from 'node:path'; |
| 57 | |
| 58 | /** |
| 59 | * Toleranz gegen die Auflösung des Dateisystems. |
| 60 | * |
| 61 | * Derselbe Wert wie in `bauPruefen()`: Ein Paketbau schreibt seine Ausgaben |
| 62 | * nicht in derselben Millisekunde, in der er die Quellen liest. |
| 63 | */ |
| 64 | const TOLERANZ_MS = 2000; |
| 65 | |
| 66 | /** Verzeichnisse, die für den Vergleich nichts beitragen. */ |
| 67 | const UEBERGANGEN = new Set(['node_modules', '.git', 'out', 'release', 'test-results']); |
| 68 | |
| 69 | /** |
| 70 | * Was `out/` für den Vergleich ersetzt. |
| 71 | * |
| 72 | * `electron-builder.yml` packt `out/**` – das ist aber kein Quelltext, |
| 73 | * sondern das Erzeugnis von `electron-vite build` aus `src/`. Auf `out/` |
| 74 | * selbst zu schauen ginge zweimal daneben: `npm run gate` baut `out/` in |
| 75 | * jedem Lauf neu, das Paket sähe also unmittelbar nach jedem Gate veraltet |
| 76 | * aus; und die Frage, ob der Quelltext weitergewandert ist, beantwortet es |
| 77 | * ohnehin nicht. `src/` ist die ehrliche Entsprechung. |
| 78 | */ |
| 79 | const ERSATZ_FUER_ERZEUGNIS = new Map([['out', 'src']]); |
| 80 | |
| 81 | /** Zeichen, an denen electron-builder einen Glob erkennt. */ |
| 82 | const GLOB_ZEICHEN = /[*?[\]{}]/u; |
| 83 | |
| 84 | /** |
| 85 | * Nimmt einer YAML-Skalarzeile Anführungszeichen und Zeilenkommentar ab. |
| 86 | * |
| 87 | * @param {string} roh |
| 88 | * @returns {string} |
| 89 | */ |
| 90 | function entwerten(roh) { |
| 91 | const wert = roh.trim(); |
| 92 | |
| 93 | for (const anfuehrung of ["'", '"']) { |
| 94 | if (wert.startsWith(anfuehrung)) { |
| 95 | const ende = wert.indexOf(anfuehrung, 1); |
| 96 | if (ende < 0) { |
| 97 | throw new Error(`electron-builder.yml: unbeendetes Anführungszeichen in ${wert}`); |
| 98 | } |
| 99 | return wert.slice(1, ende); |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | const kommentar = wert.indexOf(' #'); |
| 104 | return kommentar < 0 ? wert : wert.slice(0, kommentar).trim(); |
| 105 | } |
| 106 | |
| 107 | /** |
| 108 | * Liest die beiden Listen aus `electron-builder.yml`, die etwas ausliefern. |
| 109 | * |
| 110 | * Bewusst ein winziger, **strenger** Ausschnitt von YAML statt einer |
| 111 | * Abhängigkeit: `js-yaml` liegt zwar unter `node_modules`, aber nur als |
| 112 | * Beipack von electron-builder – ein Import darauf wäre eine nicht erklärte |
| 113 | * Abhängigkeit. Der Ausschnitt versteht genau die Formen, die diese Datei |
| 114 | * heute enthält, und **wirft** bei allem anderen. Ein lauter Fehler beim |
| 115 | * nächsten Umbau ist der Punkt: Eine Wache, die eine neue Schreibweise still |
| 116 | * überliest, bewacht wieder nichts. |
| 117 | * |
| 118 | * @param {string} text Inhalt von `electron-builder.yml`. |
| 119 | * @returns {{ programm: string[], inhalte: string[] }} Muster, wie sie in der |
| 120 | * Datei stehen: `programm` aus `files:`, `inhalte` aus `extraResources:`. |
| 121 | */ |
| 122 | export function musterAusBauplan(text) { |
| 123 | /** @type {string[]} */ const programm = []; |
| 124 | /** @type {string[]} */ const inhalte = []; |
| 125 | |
| 126 | /** @type {'files' | 'extraResources' | null} */ |
| 127 | let block = null; |
| 128 | // Einzug der `- `-Zeilen des laufenden Blocks; -1, solange unbekannt. |
| 129 | let eintragsEinzug = -1; |
| 130 | |
| 131 | for (const zeile of text.split(/\r?\n/u)) { |
| 132 | const ohneEinzug = zeile.trimStart(); |
| 133 | if (ohneEinzug === '' || ohneEinzug.startsWith('#')) { |
| 134 | continue; |
| 135 | } |
| 136 | const einzug = zeile.length - ohneEinzug.length; |
| 137 | |
| 138 | // Oberste Ebene: ein neuer Schlüssel beendet den laufenden Block. |
| 139 | if (einzug === 0) { |
| 140 | const schluessel = /^([A-Za-z][A-Za-z0-9_]*):/u.exec(ohneEinzug); |
| 141 | const name = schluessel?.[1]; |
| 142 | block = name === 'files' || name === 'extraResources' ? name : null; |
| 143 | eintragsEinzug = -1; |
| 144 | continue; |
| 145 | } |
| 146 | |
| 147 | if (block === null) { |
| 148 | continue; |
| 149 | } |
| 150 | |
| 151 | if (!ohneEinzug.startsWith('- ')) { |
| 152 | // Eigenschaft eines Eintrags (`to:`, `filter:`) – liefert selbst nichts. |
| 153 | if (eintragsEinzug >= 0 && einzug > eintragsEinzug) { |
| 154 | continue; |
| 155 | } |
| 156 | throw new Error(`electron-builder.yml: unverstandene Zeile im Block ${block}: ${ohneEinzug}`); |
| 157 | } |
| 158 | |
| 159 | if (eintragsEinzug < 0) { |
| 160 | eintragsEinzug = einzug; |
| 161 | } |
| 162 | if (einzug > eintragsEinzug) { |
| 163 | // Unterliste eines Eintrags, etwa `filter:` – kein eigener Pfad. |
| 164 | continue; |
| 165 | } |
| 166 | if (einzug < eintragsEinzug) { |
| 167 | throw new Error( |
| 168 | `electron-builder.yml: Listeneintrag mit fremdem Einzug im Block ${block}: ${ohneEinzug}`, |
| 169 | ); |
| 170 | } |
| 171 | |
| 172 | const wert = ohneEinzug.slice(2).trim(); |
| 173 | // Beide Blöcke erlauben sowohl `- pfad` als auch `- from: pfad`. |
| 174 | const ausFrom = /^from:\s*(.+)$/u.exec(wert); |
| 175 | const muster = entwerten(ausFrom ? ausFrom[1] : wert); |
| 176 | |
| 177 | // Ausschlussmuster liefern nichts aus und tragen zum Alter nichts bei. |
| 178 | if (muster.startsWith('!')) { |
| 179 | continue; |
| 180 | } |
| 181 | |
| 182 | (block === 'files' ? programm : inhalte).push(muster); |
| 183 | } |
| 184 | |
| 185 | return { programm, inhalte }; |
| 186 | } |
| 187 | |
| 188 | /** |
| 189 | * Macht aus einem Auslieferungsmuster den Pfad, dessen Alter zählt. |
| 190 | * |
| 191 | * `out/**\/*` wird zu `src`, `../content/katalog` zum Verzeichnis selbst. |
| 192 | * |
| 193 | * @param {string} appWurzel |
| 194 | * @param {string} muster |
| 195 | * @returns {string} Absoluter Pfad. |
| 196 | */ |
| 197 | function pfadZuMuster(appWurzel, muster) { |
| 198 | const teile = muster.split('/'); |
| 199 | /** @type {string[]} */ const fest = []; |
| 200 | for (const teil of teile) { |
| 201 | if (GLOB_ZEICHEN.test(teil)) { |
| 202 | break; |
| 203 | } |
| 204 | fest.push(teil); |
| 205 | } |
| 206 | |
| 207 | if (fest.length === 0) { |
| 208 | throw new Error( |
| 209 | `electron-builder.yml: Muster ${muster} beginnt mit einem Platzhalter. ` + |
| 210 | 'Es ließe sich nur auf das ganze Projektverzeichnis auflösen und ' + |
| 211 | 'wäre als Altersvergleich wertlos.', |
| 212 | ); |
| 213 | } |
| 214 | |
| 215 | const ersatz = ERSATZ_FUER_ERZEUGNIS.get(fest[0]); |
| 216 | if (ersatz !== undefined) { |
| 217 | fest.splice(0, fest.length, ersatz); |
| 218 | } |
| 219 | |
| 220 | const pfad = resolve(appWurzel, ...fest); |
| 221 | if (!existsSync(pfad)) { |
| 222 | throw new Error( |
| 223 | `electron-builder.yml verweist auf ${muster}, unter ${pfad} liegt aber nichts. ` + |
| 224 | 'Entweder ist der Bauplan veraltet oder der Altersvergleich in ' + |
| 225 | 'tools/paketstand.mjs löst ihn falsch auf – beides muss auffallen.', |
| 226 | ); |
| 227 | } |
| 228 | return pfad; |
| 229 | } |
| 230 | |
| 231 | /** |
| 232 | * Das Verzeichnis, aus dem electron-builder die Bauzutaten holt. |
| 233 | * |
| 234 | * Das Programmsymbol liegt weder unter `files:` noch unter `extraResources:` |
| 235 | * – es steht in `directories.buildResources` und wird von electron-builder |
| 236 | * in die exe und in das Installationsprogramm eingebaut. Genau deshalb fehlte |
| 237 | * es hier zuerst: Wer das Symbol änderte, bekam ein Paket gemeldet, das „so |
| 238 | * jung wie der Quelltext“ sei, und trug das alte Bild weiter. Dieselbe |
| 239 | * Fehlerklasse wie damals bei `content/`, nur eine Tür weiter. |
| 240 | * |
| 241 | * Gelesen wird auch das aus dem Bauplan statt es hier aufzuschreiben. Zwei |
| 242 | * Listen desselben Sachverhalts laufen beim nächsten Umbau auseinander. |
| 243 | * |
| 244 | * @param {string} text Inhalt von `electron-builder.yml`. |
| 245 | * @returns {string | null} Der Verzeichnisname, oder `null`, wenn keiner steht. |
| 246 | */ |
| 247 | export function bauressourcenAusBauplan(text) { |
| 248 | let imBlock = false; |
| 249 | for (const zeile of text.split(/\r?\n/u)) { |
| 250 | const ohneEinzug = zeile.trimStart(); |
| 251 | if (ohneEinzug === '' || ohneEinzug.startsWith('#')) { |
| 252 | continue; |
| 253 | } |
| 254 | if (zeile.length === ohneEinzug.length) { |
| 255 | // Oberste Ebene: ein neuer Schlüssel beendet den laufenden Block. |
| 256 | imBlock = /^directories:/u.test(ohneEinzug); |
| 257 | continue; |
| 258 | } |
| 259 | if (imBlock) { |
| 260 | const treffer = /^buildResources:\s*(\S+)/u.exec(ohneEinzug); |
| 261 | if (treffer) { |
| 262 | return treffer[1]; |
| 263 | } |
| 264 | } |
| 265 | } |
| 266 | return null; |
| 267 | } |
| 268 | |
| 269 | /** |
| 270 | * Alle Pfade, deren Alter über die Aktualität des Pakets entscheidet. |
| 271 | * |
| 272 | * Eigene Funktion und nicht in `paketstand()` versteckt, weil sie für sich |
| 273 | * prüfbar sein muss: Sie ist die Stelle, an der Bauplan und Vergleich |
| 274 | * auseinanderlaufen könnten. `paketstand()` beantwortet sie nicht, wenn gar |
| 275 | * kein Paket dasteht – ein Test, der nur über `paketstand()` ginge, prüfte |
| 276 | * die Auflösung an einem Arbeitsplatz ohne `release/` also gar nicht. |
| 277 | * |
| 278 | * @param {string} appWurzel Das Verzeichnis `app/`. |
| 279 | * @returns {{ quelltext: string[], inhalte: string[] }} Absolute Pfade. |
| 280 | */ |
| 281 | export function ausgelieferteQuellen(appWurzel) { |
| 282 | const bauplan = join(appWurzel, 'electron-builder.yml'); |
| 283 | if (!existsSync(bauplan)) { |
| 284 | throw new Error( |
| 285 | `Kein Bauplan unter ${bauplan}. Ohne ihn ist nicht zu sagen, was das Paket ` + |
| 286 | 'ausliefert – und ein Altersvergleich, der das nicht weiß, prüft nichts.', |
| 287 | ); |
| 288 | } |
| 289 | |
| 290 | const text = readFileSync(bauplan, 'utf8'); |
| 291 | const muster = musterAusBauplan(text); |
| 292 | const quelltext = muster.programm.map((m) => pfadZuMuster(appWurzel, m)); |
| 293 | |
| 294 | /* |
| 295 | Der Bauplan selbst gehört dazu. |
| 296 | |
| 297 | Er sagt nicht nur, **was** ausgeliefert wird, sondern bestimmt auch, **wie**: |
| 298 | Ziele, Dateilisten, ASAR-Entpackung, die Angaben des Store-Pakets. Wer |
| 299 | daran etwas ändert, hat ein anderes Paket vor sich – bis Fassung 0.24.1 |
| 300 | galt der alte Beleg trotzdem weiter, weil die Wache jeden Pfad ansah, |
| 301 | den der Bauplan nennt, nur nicht ihn selbst. Derselbe blinde Fleck wie |
| 302 | bei den Bauressourcen (7.23), eine Ebene höher. |
| 303 | */ |
| 304 | quelltext.push(bauplan); |
| 305 | |
| 306 | const bauressourcen = bauressourcenAusBauplan(text); |
| 307 | if (bauressourcen !== null) { |
| 308 | quelltext.push(pfadZuMuster(appWurzel, bauressourcen)); |
| 309 | } |
| 310 | |
| 311 | return { |
| 312 | quelltext, |
| 313 | inhalte: muster.inhalte.map((m) => pfadZuMuster(appWurzel, m)), |
| 314 | }; |
| 315 | } |
| 316 | |
| 317 | /** |
| 318 | * @typedef {object} Aenderung |
| 319 | * @property {number} zeit Änderungszeit in Millisekunden; 0, wenn nichts da war. |
| 320 | * @property {string} pfad Die Datei, von der sie stammt; leer bei 0. |
| 321 | */ |
| 322 | |
| 323 | /** |
| 324 | * Jüngste Änderung unterhalb eines Pfades – samt der Datei, die sie trägt. |
| 325 | * |
| 326 | * Die Datei mitzuführen kostet nichts und macht aus „irgendetwas ist neuer“ |
| 327 | * eine nachprüfbare Aussage: Wer die Meldung liest, weiß sofort, ob er neu |
| 328 | * bauen muss oder ob er selbst eine Datei angefasst hat. |
| 329 | * |
| 330 | * @param {string} pfad Datei oder Verzeichnis. |
| 331 | * @returns {Aenderung} |
| 332 | */ |
| 333 | function juengsteAenderung(pfad) { |
| 334 | const eigenschaften = statSync(pfad); |
| 335 | if (!eigenschaften.isDirectory()) { |
| 336 | return { zeit: eigenschaften.mtimeMs, pfad }; |
| 337 | } |
| 338 | |
| 339 | /** @type {Aenderung} */ let juengste = { zeit: 0, pfad: '' }; |
| 340 | for (const eintrag of readdirSync(pfad, { withFileTypes: true })) { |
| 341 | if (UEBERGANGEN.has(eintrag.name)) { |
| 342 | continue; |
| 343 | } |
| 344 | const kind = juengsteAenderung(join(pfad, eintrag.name)); |
| 345 | if (kind.zeit > juengste.zeit) { |
| 346 | juengste = kind; |
| 347 | } |
| 348 | } |
| 349 | return juengste; |
| 350 | } |
| 351 | |
| 352 | /** |
| 353 | * Jüngste Änderung über mehrere Pfade hinweg. |
| 354 | * |
| 355 | * @param {string[]} pfade |
| 356 | * @returns {Aenderung} |
| 357 | */ |
| 358 | function juengsteUeber(pfade) { |
| 359 | /** @type {Aenderung} */ let juengste = { zeit: 0, pfad: '' }; |
| 360 | for (const pfad of pfade) { |
| 361 | const kandidat = juengsteAenderung(pfad); |
| 362 | if (kandidat.zeit > juengste.zeit) { |
| 363 | juengste = kandidat; |
| 364 | } |
| 365 | } |
| 366 | return juengste; |
| 367 | } |
| 368 | |
| 369 | /** |
| 370 | * Pfad, wie ihn ein Mensch im Projekt sucht: relativ zur Projektwurzel. |
| 371 | * |
| 372 | * @param {string} appWurzel |
| 373 | * @param {string} pfad |
| 374 | * @returns {string} |
| 375 | */ |
| 376 | function lesbar(appWurzel, pfad) { |
| 377 | return relative(dirname(appWurzel), pfad).replaceAll('\\', '/'); |
| 378 | } |
| 379 | |
| 380 | /** |
| 381 | * @typedef {object} Paketstand |
| 382 | * @property {boolean} vorhanden Ob überhaupt ein gepacktes Paket dasteht. |
| 383 | * @property {boolean} veraltet Ob es älter ist als das, was es ausliefert. |
| 384 | * @property {number} alterSekunden Um wie viel es zurückliegt; 0, wenn aktuell. |
| 385 | * @property {'quelltext' | 'inhalte' | 'beides' | null} ursache |
| 386 | * Was weitergewandert ist. `null`, solange nichts veraltet ist. |
| 387 | * @property {string} juengsteQuelle |
| 388 | * Die Datei, die den Ausschlag gibt, relativ zur Projektwurzel; leer, wenn |
| 389 | * das Paket aktuell ist oder fehlt. |
| 390 | * @property {string} grund Ein Satz, der den Zustand benennt. |
| 391 | */ |
| 392 | |
| 393 | /** |
| 394 | * Vergleicht das gepackte Paket mit allem, was es ausliefert. |
| 395 | * |
| 396 | * @param {string} appWurzel Das Verzeichnis `app/`. |
| 397 | * @returns {Paketstand} |
| 398 | */ |
| 399 | export function paketstand(appWurzel) { |
| 400 | const exe = join(appWurzel, 'release', 'win-unpacked', 'Waffensachkunde Lernsoftware.exe'); |
| 401 | |
| 402 | if (!existsSync(exe)) { |
| 403 | return { |
| 404 | vorhanden: false, |
| 405 | veraltet: false, |
| 406 | alterSekunden: 0, |
| 407 | ursache: null, |
| 408 | juengsteQuelle: '', |
| 409 | grund: |
| 410 | 'Kein gepacktes Paket vorhanden. Die Prüfungen gegen das gebaute Paket ' + |
| 411 | 'wurden übersprungen – zuerst `npm run dist:win` ausführen.', |
| 412 | }; |
| 413 | } |
| 414 | |
| 415 | const quellen = ausgelieferteQuellen(appWurzel); |
| 416 | const quelltext = juengsteUeber(quellen.quelltext); |
| 417 | const inhalte = juengsteUeber(quellen.inhalte); |
| 418 | |
| 419 | const gepacktAm = statSync(exe).mtimeMs; |
| 420 | const grenze = gepacktAm + TOLERANZ_MS; |
| 421 | const quelltextNeuer = quelltext.zeit > grenze; |
| 422 | const inhalteNeuer = inhalte.zeit > grenze; |
| 423 | |
| 424 | if (!quelltextNeuer && !inhalteNeuer) { |
| 425 | return { |
| 426 | vorhanden: true, |
| 427 | veraltet: false, |
| 428 | alterSekunden: 0, |
| 429 | ursache: null, |
| 430 | juengsteQuelle: '', |
| 431 | grund: 'Das gepackte Paket ist so jung wie Quelltext und mitgelieferte Inhalte.', |
| 432 | }; |
| 433 | } |
| 434 | |
| 435 | const juengste = quelltext.zeit >= inhalte.zeit ? quelltext : inhalte; |
| 436 | const alterSekunden = Math.round((juengste.zeit - gepacktAm) / 1000); |
| 437 | const quelle = lesbar(appWurzel, juengste.pfad); |
| 438 | |
| 439 | /* Der Grundtext sagt weiterhin, WARUM übersprungen wird – und jetzt auch, |
| 440 | WAS weitergewandert ist. Der Unterschied ist keine Feinheit: „Quelltext“ |
| 441 | heißt neu bauen, „Inhalte“ heißt, dass eine Änderung an content/ noch |
| 442 | nicht im Paket steckt. Beim ersten Auftreten dieses Falls stand in der |
| 443 | Meldung nichts davon, weil der Vergleich content/ gar nicht ansah. */ |
| 444 | /** @type {'quelltext' | 'inhalte' | 'beides'} */ |
| 445 | let ursache = 'beides'; |
| 446 | if (!inhalteNeuer) { |
| 447 | ursache = 'quelltext'; |
| 448 | } else if (!quelltextNeuer) { |
| 449 | ursache = 'inhalte'; |
| 450 | } |
| 451 | |
| 452 | /* „das Programm“ statt „der Quelltext“, seit auch das Verzeichnis mit dem |
| 453 | Programmsymbol mitzählt: Ein Symbol ist kein Quelltext, steckt aber |
| 454 | genauso in der gebauten exe. Die Meldung nennt ohnehin die Datei, die den |
| 455 | Ausschlag gibt – daran sieht man sofort, welcher Fall vorliegt. */ |
| 456 | const benennung = { |
| 457 | quelltext: `das Programm (${quelle})`, |
| 458 | inhalte: `die mitgelieferten Inhalte (${quelle})`, |
| 459 | beides: `Programm und mitgelieferte Inhalte (jüngste Änderung: ${quelle})`, |
| 460 | }[ursache]; |
| 461 | |
| 462 | return { |
| 463 | vorhanden: true, |
| 464 | veraltet: true, |
| 465 | alterSekunden, |
| 466 | ursache, |
| 467 | juengsteQuelle: quelle, |
| 468 | grund: |
| 469 | `Das gepackte Paket ist ${String(alterSekunden)} s älter als ${benennung}. ` + |
| 470 | 'Die Prüfungen dagegen würden die vorige Fassung messen und grün melden; ' + |
| 471 | 'sie wurden deshalb übersprungen – zuerst `npm run dist:win` ausführen.', |
| 472 | }; |
| 473 | } |