import { readFile, readdir, mkdir, writeFile } from 'node:fs/promises'; import { fileURLToPath, URL } from 'node:url'; import { dirname, join } from 'node:path'; /** * Lizenztexte der gebuendelten Fremdbestandteile - eingesammelt, nicht gepflegt. * * Wozu das gut ist: Der MIT-Text verlangt, dass Urheber- und Genehmigungsvermerk * mit jeder Kopie gehen. Seit 5.14.0 stehen sie im uebersetzten Renderer selbst - * `lizenzbanner` in vite.config.ts setzt jedem Ausgabebrocken die Lizenztexte der * Bibliotheken voran, deren Kode wirklich darin steckt (nachgemessen in * docs/fremdbestandteile.md unter "Was aussteht"). Diese Datei traegt sie * zusaetzlich geschlossen neben die Programmdatei, dorthin, wo electron-builder * auch LICENSE.electron.txt und LICENSES.chromium.html ablegt: Wer nachsehen * will, unter welchen Bedingungen er das Programm bekommen hat, soll das nicht * in einer verkuerzten Ausgabedatei tun muessen. * * DIE REGEL FUER DEN DATEINAMEN STEHT AN ZWEI STELLEN. Hier als LIZENZDATEI und * ebenso in vite.config.ts; beide lesen dieselben Dateien aus node_modules. Ein * gemeinsamer Bezug ginge nur ueber eine Deklarationsdatei oder `allowJs` in * tsconfig.json - vite.config.ts wird typgeprueft und darf keine .mjs einbinden. * Zusammengehalten werden die beiden stattdessen von zwei Wachen in * tests/tools/werkbank.test.ts: Sie suchen die Lizenzdateien mit einer * dritten Fassung derselben Regel und verlangen, dass JEDE davon woertlich im * Banner ihrer Ausgabedatei steht und mit ihrem Rechteinhaber in der * Sammeldatei. Faellt eine der drei Regeln aus dem Tritt, wird eine der beiden * rot. * * WARUM ERZEUGT UND NICHT GESCHRIEBEN: Ein von Hand gefuehrtes Lizenzverzeichnis * veraltet beim naechsten `npm install` still. Die Liste der Bibliotheken kommt * deshalb aus den QUELLKARTEN des gebauten Renderers - also aus dem, was Vite * wirklich hineinuebersetzt hat, nicht aus einer Aufstellung, die jemand pflegen * muesste -, und der Text je Bibliothek woertlich aus dem installierten Paket. * * WARUM ES LIEBER ABBRICHT: Findet es zu einer Bibliothek keinen Text, schreibt * es nichts. Eine Sammeldatei, in der eine fehlt, ist schlimmer als keine - sie * sieht nach Erfuellung aus. * * Aufruf (beide Angaben haben Vorgabewerte und werden selten gebraucht): * * node scripts/lizenztexte.mjs [--bau ] [--ziel ] * * --bau Ordner mit den Ausgabedateien und Quellkarten des Renderers. * Vorgabe: build/renderer/assets * --ziel Zieldatei. Vorgabe: build/DRITTANBIETER-LIZENZEN.txt * * `npm run build` ruft es ohne Angaben; `build.extraFiles` in package.json legt * das Ergebnis danach neben die Programmdatei. */ const WURZEL = fileURLToPath(new URL('..', import.meta.url)); /** Eine Angabe von der Kommandozeile, oder der Vorgabewert. */ function angabe(name, vorgabe) { const stelle = process.argv.indexOf(`--${name}`); if (stelle < 0) return vorgabe; const wert = process.argv[stelle + 1]; if (wert === undefined || wert.startsWith('--')) { console.error(`Zu --${name} fehlt die Angabe.`); process.exit(1); } return wert; } const BAUABLAGE = angabe('bau', join(WURZEL, 'build', 'renderer', 'assets')); const ZIEL = angabe('ziel', join(WURZEL, 'build', 'DRITTANBIETER-LIZENZEN.txt')); /** Lizenzdatei, Lizenzhinweis, COPYING - so heissen die Texte in den Paketen. */ const LIZENZDATEI = /^(licen[sc]e|copying)/i; function abbruch(satz) { console.error(satz); process.exit(1); } /* * Welche Pakete stecken im gebauten Renderer? * * Die Quellkarten fuehren in `sources` jede Ursprungsdatei; alles unterhalb von * node_modules/ ist ein Fremdbestandteil. Ein Bereichsname (@babel/runtime) * besteht aus zwei Abschnitten, jeder andere aus einem. */ let karten; try { karten = (await readdir(BAUABLAGE)).filter((d) => d.endsWith('.js.map')); } catch { abbruch(`Kein Ordner "${BAUABLAGE}". Bauen Sie zuerst den Renderer, etwa mit "npm run build".`); } if (karten.length === 0) { abbruch( `Keine Quellkarten in "${BAUABLAGE}". Ohne sie ist nicht feststellbar, welche Bibliotheken gebuendelt wurden.`, ); } const namen = new Set(); for (const karte of karten) { const inhalt = JSON.parse(await readFile(join(BAUABLAGE, karte), 'utf8')); for (const quelle of inhalt.sources ?? []) { const stelle = quelle.lastIndexOf('node_modules/'); if (stelle < 0) continue; const teile = quelle.slice(stelle + 'node_modules/'.length).split('/'); namen.add(teile[0].startsWith('@') ? `${teile[0]}/${teile[1]}` : teile[0]); } } if (namen.size === 0) { abbruch( `Die Quellkarten in "${BAUABLAGE}" nennen keine Bibliothek. Das ist kein Ergebnis, sondern ein Fehler - abgebrochen.`, ); } /* * Je Paket: Fassung, Lizenzangabe, Herkunft und der Text selbst - alles aus dem * installierten Paket, nichts aus dieser Datei. */ const eintraege = []; for (const name of [...namen].sort()) { const ablage = join(WURZEL, 'node_modules', name); let paket; try { paket = JSON.parse(await readFile(join(ablage, 'package.json'), 'utf8')); } catch { abbruch(`Kein installiertes Paket "${name}" - abgebrochen, nichts geschrieben.`); } let dateien; try { dateien = (await readdir(ablage)).filter((d) => LIZENZDATEI.test(d)).sort(); } catch { dateien = []; } if (dateien.length === 0) { abbruch( `Keine Lizenzdatei in node_modules/${name} - abgebrochen, nichts geschrieben. Eine Sammeldatei ohne diesen Text sieht nach Erfuellung aus und ist keine.`, ); } const texte = []; for (const datei of dateien) { // Zeilenenden vereinheitlichen: Die Pakete bringen beides mit, und eine // gemischte Datei laesst ein Textfenster unter Windows Zeilen verschlucken. const text = (await readFile(join(ablage, datei), 'utf8')).replace(/\r\n/g, '\n').trim(); if (text === '') { abbruch(`node_modules/${name}/${datei} ist leer - abgebrochen, nichts geschrieben.`); } texte.push({ datei, text }); } const herkunft = typeof paket.homepage === 'string' ? paket.homepage : typeof paket.repository === 'string' ? paket.repository : typeof paket.repository?.url === 'string' ? paket.repository.url : ''; eintraege.push({ name, fassung: String(paket.version), lizenz: String(paket.license ?? '(keine Angabe im Paket)'), herkunft, texte, }); } const paketdatei = JSON.parse(await readFile(join(WURZEL, 'package.json'), 'utf8')); const trennlinie = '='.repeat(76); const unterstrich = '-'.repeat(76); const kopf = [ `LSA-Planer Professional ${paketdatei.version} – Lizenztexte der mitgelieferten Bibliotheken`, '', 'Der LSA-Planer übersetzt fremde Bibliotheken in sein Programm hinein. Ihre', 'Lizenzen verlangen, dass Urheber- und Genehmigungsvermerk mit jeder Kopie', 'gehen. Diese Datei trägt sie – wörtlich so, wie sie dem jeweiligen Paket', 'beiliegen.', '', 'Electron und Chromium stehen nicht hier. Ihre Texte liegen als', 'LICENSE.electron.txt und LICENSES.chromium.html daneben.', '', 'Was diese Bibliotheken sind, woher sie kommen und wozu das Programm sie', 'benutzt, verzeichnet FREMDBESTANDTEILE.md daneben.', '', 'Diese Datei ist erzeugt, nicht gepflegt: scripts/lizenztexte.mjs nimmt die', 'Liste aus den Quellkarten des gebauten Renderers – also aus dem, was wirklich', 'gebündelt wurde – und den Text je Bibliothek aus dem installierten Paket.', '', 'Das ist keine Rechtsauskunft.', '', `Es folgen ${eintraege.length} Texte.`, '', ]; const abschnitte = []; for (const e of eintraege) { abschnitte.push(trennlinie); abschnitte.push(`${e.name} ${e.fassung} – ${e.lizenz}`); if (e.herkunft !== '') abschnitte.push(e.herkunft); abschnitte.push(trennlinie); for (const t of e.texte) { abschnitte.push(''); abschnitte.push(`(node_modules/${e.name}/${t.datei})`); abschnitte.push(unterstrich); abschnitte.push(''); abschnitte.push(t.text); } abschnitte.push(''); abschnitte.push(''); } /* * Mit Bytefolgemarke, aus demselben Grund wie bei den Pruefsummen: Der Text * enthaelt Umlaute, und ein Windows-Editor, der die Datei ohne Marke als ANSI * liest, zeigt daraus Buchstabensalat. Der Dateiname bleibt ASCII. * * Als Fluchtfolge und nicht als Zeichen: Ein unsichtbares U+FEFF im Quelltext * beanstandet der Linter (no-irregular-whitespace), und zu Recht - man sieht * ihm nicht an, dass er da ist. */ await mkdir(dirname(ZIEL), { recursive: true }); await writeFile(ZIEL, `\uFEFF${[...kopf, ...abschnitte].join('\n')}`, 'utf8'); console.log( `Lizenztexte für ${eintraege.length} Bibliotheken: ${eintraege.map((e) => e.name).join(', ')}`, ); console.log(`geschrieben: ${ZIEL}`);