pageta/apps/extension/scripts/check.mjs
Till JS 3dbd6611f2 feat: mehrseitige Artikel vollständig sichern
Golems `…-2607-211258.html` hat drei Seiten; gespeichert wurde bisher
Seite 1 — 911 von rund 2400 Wörtern. Serverseitig ist da nichts zu holen:
Seite 2 liegt hinter derselben Zustimmungs-Wand wie Seite 1. Ein `fetch`
aus dem Tab bekommt sie (gemessen: 200, kein Redirect). Also sammelt der
Tab sie.

- `page-collector.js` (`collectPageta`) findet Folgeseiten ohne
  quellenspezifische Selektoren: Zähler-Adressen (`-2.html`, `?page=2`,
  `/seite/2/`), sonst die `rel="next"`-Kette. Deckel 20 Seiten / 6 MB,
  300 ms Pause je Abruf. Läuft im Bookmarklet (Quelltext in der
  `javascript:`-Adresse) und in der Erweiterung (`executeScript`) —
  eine Quelle, zeichengleiche Kopie, von `check.mjs` erzwungen.
- Weiches Strippen vor dem Senden: `<style>/<noscript>/<template>` raus,
  Skript-Inhalte geleert, Tags/Attribute und JSON-LD bleiben. Sonst
  verlören `siteNameFromJsonLd` und die CMP-Signaturen ihre Grundlage.
  Extraktions-Ergebnis nachweislich unverändert (the-decoder, winfuture).
- API nimmt `pages` (max 20) zusätzlich zu `html` und antwortet mit
  `pageCount`; `merge-pages.ts` fügt in Lese-Reihenfolge zusammen.
  `html` bleibt gültig — Bookmarklets werden nicht mitdeployt.
- Fehlt eine erkannte Seite, springt die add-Seite nicht weiter, sondern
  sagt „x von y Seiten".

Am Golem-Artikel verifiziert: 3 Seiten, 0 fehlend, 298 KB statt ~1,2 MB.
Wer ein Bookmarklet installiert hat, muss es einmal neu ablegen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 14:25:03 +02:00

123 lines
5.3 KiB
JavaScript

#!/usr/bin/env node
/**
* Prüft die Erweiterung, ohne einen Browser zu starten.
*
* Eine Erweiterung hat keinen Compiler, der einen beim Tippfehler
* aufhält — das Ding scheitert sonst erst beim Laden im Browser, mit einer
* Fehlermeldung in einer Konsole, die niemand offen hat. Diese Prüfungen
* fangen genau die Fehler, die dabei entstehen:
* - Manifest ist kein gültiges JSON / falsche Version
* - eine im Manifest genannte Datei fehlt
* - eine Berechtigung ist eingeschlichen, die wir bewusst nicht wollen
* - eine Skript-Datei hat einen Syntaxfehler
*/
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const fehler = [];
const ok = (msg) => console.log(` ✓ ${msg}`);
// ─── Manifest ────────────────────────────────────────────────────────
let manifest;
try {
manifest = JSON.parse(readFileSync(join(ROOT, 'manifest.json'), 'utf8'));
ok('manifest.json ist gültiges JSON');
} catch (e) {
console.error(` ✗ manifest.json: ${e.message}`);
process.exit(1);
}
if (manifest.manifest_version !== 3) fehler.push('manifest_version muss 3 sein');
else ok('Manifest V3');
for (const feld of ['name', 'version', 'description', 'icons', 'action', 'background']) {
if (!manifest[feld]) fehler.push(`Feld fehlt: ${feld}`);
}
// ─── Referenzierte Dateien existieren ────────────────────────────────
const referenziert = [
...Object.values(manifest.icons ?? {}),
...Object.values(manifest.action?.default_icon ?? {}),
manifest.background?.service_worker,
...(manifest.background?.scripts ?? []),
manifest.options_ui?.page,
].filter(Boolean);
for (const datei of new Set(referenziert)) {
if (!existsSync(join(ROOT, datei))) fehler.push(`im Manifest genannt, aber nicht da: ${datei}`);
}
ok(`${new Set(referenziert).size} referenzierte Dateien vorhanden`);
// ─── Berechtigungen: nur das Nötige ──────────────────────────────────
// Bewusste Liste. Wächst sie, ist das eine Entscheidung — kein Versehen.
// Begründung je Eintrag steht in background.js und README.md.
const ERLAUBT = new Set(['activeTab', 'scripting', 'storage']);
for (const p of manifest.permissions ?? []) {
if (!ERLAUBT.has(p)) {
fehler.push(
`Berechtigung "${p}" ist nicht in der bewussten Liste. ` +
'Wenn sie wirklich nötig ist: hier eintragen UND in README.md begründen.',
);
}
}
// `<all_urls>` als feste host_permission wäre eine dauerhafte Leseerlaubnis
// für jede Seite — genau das, was activeTab vermeidet.
for (const h of manifest.host_permissions ?? []) {
if (h.includes('*://*/') || h === '<all_urls>') {
fehler.push(`host_permissions enthält "${h}" — das ist eine dauerhafte Leseerlaubnis.`);
}
}
ok('Berechtigungen bleiben im vereinbarten Rahmen');
// ─── Geteilte Sammel-Logik: eine Quelle, zwei Kopien ─────────────────
// `page-collector.js` wird von der Web-App (als Bookmarklet-Quelltext) UND
// von hier (via `executeScript`) benutzt. Ein Bundler, der beide bedient,
// existiert nicht — die Erweiterung hat bewusst keinen Build. Also: Kopie,
// aber eine nachweislich gleiche.
const COLLECTOR = 'page-collector.js';
const ORIGINAL = join(ROOT, '..', 'web', 'src', 'lib', COLLECTOR);
const KOPIE = join(ROOT, COLLECTOR);
if (!existsSync(KOPIE)) {
fehler.push(`${COLLECTOR} fehlt — background.js importiert es.`);
} else if (!existsSync(ORIGINAL)) {
fehler.push(`Original nicht gefunden: ${ORIGINAL}`);
} else if (readFileSync(ORIGINAL, 'utf8') !== readFileSync(KOPIE, 'utf8')) {
fehler.push(
`${COLLECTOR} weicht von apps/web/src/lib/${COLLECTOR} ab. ` +
`Beide müssen zeichengleich sein: cp apps/web/src/lib/${COLLECTOR} apps/extension/${COLLECTOR}`,
);
} else {
ok(`${COLLECTOR} ist deckungsgleich mit der Web-App`);
}
// ─── Skripte parsen ──────────────────────────────────────────────────
// `node --check` parst, ohne auszuführen — Ausführen würde Browser-APIs
// anfassen, die es in Node nicht gibt. Die Kopie mit `.mjs` sorgt dafür,
// dass Node `import`/`export` akzeptiert (die Erweiterung lädt als Modul,
// siehe manifest.background.type).
mkdirSync(join(ROOT, 'dist'), { recursive: true });
for (const datei of ['background.js', 'page-collector.js', 'options.js']) {
const pfad = join(ROOT, datei);
if (!existsSync(pfad)) continue;
const temp = join(ROOT, 'dist', `.syntax-check-${datei}.mjs`);
try {
writeFileSync(temp, readFileSync(pfad, 'utf8'));
execFileSync(process.execPath, ['--check', temp], { stdio: 'pipe' });
ok(`${datei} parst`);
} catch (e) {
fehler.push(`${datei}: ${(e.stderr?.toString() || e.message).trim()}`);
} finally {
rmSync(temp, { force: true });
}
}
if (fehler.length) {
console.error(`\n${fehler.length} Problem(e):`);
for (const f of fehler) console.error(` ✗ ${f}`);
process.exit(1);
}
console.log('\nErweiterung: grün.');