UmbrelApps: community app store whatsnext
Electrum Gate, Evolu Relay en Atreides Assets, met de image-recepten in tools/ en de tests in tests/. Begonnen zonder voorgeschiedenis op 09-10-2026. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
85d85a1295
63 files changed
+13987
No files matched your search
@@ -0,0 +1,173 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// Toetst dat de JavaScript in de statuspagina's parseert, en dat hij dat ook nog
|
||||
// doet nadat umbreld de template heeft ingevuld.
|
||||
//
|
||||
// Draaien: node tests/test_paginas_parsen.mjs
|
||||
//
|
||||
// Waarom dit bestand er is. De pagina's zijn met bijna drieduizend regels het
|
||||
// grootste deel van deze repo en er staat geen enkele toets op: alles eraan is
|
||||
// handwerk in een browser. Dat blijft zo voor wat het doet, maar één klasse fouten
|
||||
// hoeft niet op het apparaat gevonden te worden: een tikfout waardoor het script
|
||||
// helemaal niet loopt. Het gevolg daarvan is een pagina die stil op "unknown"
|
||||
// blijft staan, en dat lijkt op een agent die niet antwoordt.
|
||||
//
|
||||
// Aanleiding: op 30-08-2026 is de pagina van Evolu Relay in zijn geheel opnieuw
|
||||
// geschreven, negenhonderd regels in één keer. Zo'n wijziging hoort niet ongetoetst
|
||||
// naar een apparaat te gaan.
|
||||
//
|
||||
// De invulling wordt hier nagedaan en niet alleen de bron gelezen, want umbreld
|
||||
// haalt elke *.template door envsubst en wat er draait is dus niet wat er staat.
|
||||
// Dat vindt de gevallen waar het weghalen van een accolade-vorm de syntaxis breekt.
|
||||
//
|
||||
// LET OP wat deze helft NIET vindt, want dat is precies de fout die dit project
|
||||
// vreest: een template-literal als `iets ${x}` wordt na de invulling `iets ` en dat
|
||||
// parseert prima. Hij is dan stil kapot en niet luidruchtig. Wat dát dichthoudt is
|
||||
// de dollarteken-toets in tests/test_appstore_vorm.py, die élke accolade-vorm
|
||||
// vlagt die umbrelOS niet zelf invult. Deze toets is de tweede zeef en niet de
|
||||
// eerste.
|
||||
//
|
||||
// Wat dit verder NIET toetst: of het script het juiste doet. Alleen dat het loopt.
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const HIER = dirname(fileURLToPath(import.meta.url));
|
||||
const REPO = join(HIER, '..');
|
||||
|
||||
let goed = 0;
|
||||
let fout = 0;
|
||||
|
||||
const toets = (omschrijving, voorwaarde, uitleg) => {
|
||||
if (voorwaarde) {
|
||||
goed += 1;
|
||||
return;
|
||||
}
|
||||
fout += 1;
|
||||
console.log(`FOUT: ${omschrijving}${uitleg ? ` - ${uitleg}` : ''}`);
|
||||
};
|
||||
|
||||
/** Elke map in de repo-root met een umbrel-app.yml erin. Zelf gevonden, zoals de
|
||||
* Python-toets over de store dat ook doet: een derde app valt er dan vanzelf
|
||||
* onder. */
|
||||
const appMappen = () =>
|
||||
readdirSync(REPO, { withFileTypes: true })
|
||||
.filter((regel) => regel.isDirectory())
|
||||
.map((regel) => regel.name)
|
||||
.filter((naam) => existsSync(join(REPO, naam, 'umbrel-app.yml')))
|
||||
.sort();
|
||||
|
||||
/** De inhoud van elk <script>-blok zonder src-attribuut. */
|
||||
const scriptBlokken = (html) => {
|
||||
const blokken = [];
|
||||
const patroon = /<script(?![^>]*\bsrc=)[^>]*>([\s\S]*?)<\/script>/gi;
|
||||
let treffer = patroon.exec(html);
|
||||
while (treffer !== null) {
|
||||
blokken.push(treffer[1]);
|
||||
treffer = patroon.exec(html);
|
||||
}
|
||||
return blokken;
|
||||
};
|
||||
|
||||
/**
|
||||
* Doet na wat umbreld met een template doet.
|
||||
*
|
||||
* envsubst vervangt `${NAAM}` en `$NAAM` door de waarde uit de omgeving, en door
|
||||
* een lege string als die variabele niet bestaat. Wat wij hier invullen is een
|
||||
* plausibele waarde voor de variabelen die de pagina's gebruiken; voor de rest de
|
||||
* lege string, want dat is precies wat er op het apparaat zou gebeuren.
|
||||
*/
|
||||
const vulIn = (tekst) => {
|
||||
const WAARDEN = { APP_VERSION: '0.5.0' };
|
||||
return tekst.replace(/\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?/g, (heel, naam) =>
|
||||
Object.prototype.hasOwnProperty.call(WAARDEN, naam) ? WAARDEN[naam] : '',
|
||||
);
|
||||
};
|
||||
|
||||
/** Parseert het script zonder het te draaien. Geeft de foutmelding of null. */
|
||||
const parseert = (bron) => {
|
||||
try {
|
||||
// new vm.Script parseert bij het aanmaken en draait niets. Dat is precies wat
|
||||
// hier nodig is: draaien zou een document en een window vragen die er niet
|
||||
// zijn, en dan toets je je eigen namaakbrowser.
|
||||
new vm.Script(bron);
|
||||
return null;
|
||||
} catch (error) {
|
||||
return String(error && error.message ? error.message : error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Waar de pagina van een app staat, en of umbreld hem invult.
|
||||
*
|
||||
* Twee plekken: als *.template in de app-map (dan gaat hij door envsubst), of als
|
||||
* gewoon bestand in het bouwrecept onder tools/<app-id zonder store-id>/ (dan zit
|
||||
* hij in een eigen image en wordt hij door niemand ingevuld). Electrum Gate is
|
||||
* sinds 0.1.0 het tweede geval; zie het plan Eigenimage.
|
||||
*/
|
||||
const vindPagina = (app) => {
|
||||
const template = join(REPO, app, 'index.html.template');
|
||||
if (existsSync(template)) {
|
||||
return { pad: template, template: true };
|
||||
}
|
||||
const kort = app.includes('-') ? app.slice(app.indexOf('-') + 1) : app;
|
||||
const inImage = join(REPO, 'tools', kort, 'index.html');
|
||||
if (existsSync(inImage)) {
|
||||
return { pad: inImage, template: false };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
for (const app of appMappen()) {
|
||||
const pagina = vindPagina(app);
|
||||
if (pagina === null) {
|
||||
// Geen pagina is geen fout: een app hoeft er geen te hebben.
|
||||
continue;
|
||||
}
|
||||
|
||||
const bron = readFileSync(pagina.pad, 'utf8');
|
||||
const blokken = scriptBlokken(bron);
|
||||
|
||||
toets(`${app}: de pagina heeft een scriptblok`, blokken.length > 0);
|
||||
|
||||
blokken.forEach((blok, index) => {
|
||||
const naam = `${app}: scriptblok ${index + 1}`;
|
||||
|
||||
const rauw = parseert(blok);
|
||||
toets(`${naam} parseert zoals hij in de repo staat`, rauw === null, rauw);
|
||||
|
||||
if (pagina.template) {
|
||||
const ingevuld = parseert(vulIn(blok));
|
||||
toets(`${naam} parseert ook nadat umbreld hem invult`, ingevuld === null, ingevuld);
|
||||
}
|
||||
});
|
||||
|
||||
if (!pagina.template) {
|
||||
// Een pagina in een image gaat door geen envsubst meer, dus wat er staat is
|
||||
// wat de browser krijgt. Een accolade-variabele is dan een restant uit de
|
||||
// template-tijd die letterlijk op de pagina zou belanden.
|
||||
toets(`${app}: de pagina in de image bevat geen accolade-variabele`, !bron.includes('${'));
|
||||
}
|
||||
|
||||
// De HTML zelf: geen parser, maar wel de fout die je in een bestand van
|
||||
// tweeduizend regels echt maakt. Een dialog of een header die niet gesloten is,
|
||||
// laat de rest van de pagina in dat element hangen.
|
||||
for (const tag of ['html', 'head', 'body', 'header', 'script', 'style', 'dialog', 'details']) {
|
||||
const open = (bron.match(new RegExp(`<${tag}[\\s>]`, 'gi')) || []).length;
|
||||
const dicht = (bron.match(new RegExp(`</${tag}>`, 'gi')) || []).length;
|
||||
if (open === 0 && dicht === 0) {
|
||||
continue;
|
||||
}
|
||||
toets(
|
||||
`${app}: elke <${tag}> is gesloten`,
|
||||
open === dicht,
|
||||
`${open} keer open, ${dicht} keer dicht`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log(`${goed} goed, ${fout} fout`);
|
||||
process.exit(fout === 0 ? 0 : 1);
|
||||
Reference in new issue
Block a user