Files
vendoo/tools/verify-esm-export-contracts.mjs
Masterluke77andGitHub 6f48827f4d Vendoo 1.41.2 – Git Ignore Boundary & Module Tracking Hotfix (#18)
* docs: prepare Vendoo 1.41.2 git-ignore boundary hotfix

* fix: track native source modules with root-anchored runtime ignores
2026-07-09 17:09:00 +02:00

101 lines
4.3 KiB
JavaScript

import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
import { dirname, extname, join, normalize, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const defaultRoot = dirname(dirname(fileURLToPath(import.meta.url)));
const rootArgIndex = process.argv.indexOf('--root');
const root = resolve(rootArgIndex >= 0 && process.argv[rootArgIndex + 1] ? process.argv[rootArgIndex + 1] : defaultRoot);
const failures = [];
const extensions = ['.mjs', '.js', '.cjs'];
const sourceDirectories = ['app', 'lib', 'public', 'scripts', 'tools'];
const rootSourceFiles = ['bootstrap.mjs', 'server.mjs'];
const ignoredDirectoryNames = new Set([
'node_modules', '.git', 'release-output', 'runtime', 'uploads', 'backups', 'logs', 'updates', 'modules',
'archiv', 'archive', 'archives',
]);
function walk(dir, out = []) {
if (!existsSync(dir)) return out;
for (const name of readdirSync(dir)) {
if (ignoredDirectoryNames.has(name.toLowerCase())) continue;
const abs = join(dir, name);
const stat = statSync(abs);
if (stat.isDirectory()) walk(abs, out);
else if (extensions.includes(extname(name))) out.push(abs);
}
return out;
}
function productionSourceFiles() {
const files = [];
for (const name of rootSourceFiles) {
const file = join(root, name);
if (existsSync(file)) files.push(file);
}
for (const directory of sourceDirectories) walk(join(root, directory), files);
return [...new Set(files.map(file => resolve(file)))];
}
function resolveLocalImport(importer, specifier) {
const base = resolve(dirname(importer), specifier);
const candidates = [base, ...extensions.map(ext => `${base}${ext}`), ...extensions.map(ext => join(base, `index${ext}`))];
return candidates.find(candidate => existsSync(candidate)) || null;
}
function collectExportsFromFile(file, visited = new Set()) {
const resolvedFile = resolve(file);
if (visited.has(resolvedFile)) return new Set();
visited.add(resolvedFile);
const source = readFileSync(resolvedFile, 'utf8');
const names = new Set();
for (const match of source.matchAll(/\bexport\s+(?:async\s+)?(?:function|class|const|let|var)\s+([A-Za-z_$][\w$]*)/g)) names.add(match[1]);
for (const match of source.matchAll(/\bexport\s*\{([^}]*)\}(?:\s*from\s*['"]([^'"]+)['"])?\s*;?/g)) {
for (const rawPart of match[1].split(',')) {
const part = rawPart.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/g, '').trim();
if (!part) continue;
const alias = part.split(/\s+as\s+/i).pop()?.trim();
if (/^[A-Za-z_$][\w$]*$/.test(alias || '')) names.add(alias);
}
}
for (const match of source.matchAll(/\bexport\s*\*\s*from\s*['"](\.[^'"]+)['"]\s*;?/g)) {
const target = resolveLocalImport(resolvedFile, match[1]);
if (!target) continue;
for (const name of collectExportsFromFile(target, visited)) {
if (name !== 'default') names.add(name);
}
}
if (/\bexport\s+default\b/.test(source)) names.add('default');
return names;
}
const files = productionSourceFiles();
for (const importer of files) {
const source = readFileSync(importer, 'utf8');
const importPattern = /^[ \t]*import\s*\{([^}]*)\}\s*from\s*['"](\.[^'"]+)['"](?:\s+with\s*\{[^}]*\})?\s*;?/gm;
for (const match of source.matchAll(importPattern)) {
const target = resolveLocalImport(importer, match[2]);
if (!target) {
failures.push(`${normalize(importer)} importiert fehlende lokale Datei ${match[2]}`);
continue;
}
const exported = collectExportsFromFile(target);
for (const rawPart of match[1].split(',')) {
const part = rawPart.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/g, '').trim();
if (!part) continue;
const importedName = part.split(/\s+as\s+/i)[0]?.trim();
if (!/^[A-Za-z_$][\w$]*$/.test(importedName || '')) continue;
if (!exported.has(importedName)) {
failures.push(`${normalize(importer)} importiert '${importedName}' aus ${normalize(target)}, aber der Export fehlt.`);
}
}
}
}
if (failures.length) {
console.error(`ESM-Export-Vertragsprüfung fehlgeschlagen (${failures.length}):`);
failures.slice(0, 100).forEach(item => console.error(`- ${item}`));
process.exit(1);
}
console.log(`ESM-Export-Vertragsprüfung erfolgreich: ${files.length} produktive Quelldateien geprüft; Archive und Übergabepakete ausgeschlossen.`);