Files
civitai__civitai/scripts/generate-moderator-endpoint-catalog.mjs
T

70 lines
3.0 KiB
JavaScript

// Generates the moderator endpoint catalog that `/moderator/api` renders.
//
// The spoke gets this free from Vite's `import.meta.glob`. Next has no equivalent, and a registry the
// builder populates at import time does not help: the side effect only fires once a module is imported,
// so the catalog would have to import every endpoint to discover them — dragging every moderator service
// into the docs page's module graph, which is what the generated lazy `import()`s exist to avoid.
//
// So: scan for the files, emit the loaders.
//
// Run BY HAND — `pnpm run generate:moderator-endpoints` — deliberately not wired into install or build.
// The output is committed, so a fresh checkout typechecks without it. The cost of that choice: adding an
// endpoint and forgetting this leaves it missing from /moderator/api, and nothing fails.
import { readdir, readFile, writeFile } from 'node:fs/promises';
import { join, relative, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = join(fileURLToPath(new URL('.', import.meta.url)), '..');
const API_ROOT = join(ROOT, 'src', 'pages', 'api');
const OUT = join(ROOT, 'src', 'server', 'utils', 'moderator-endpoint-catalog.generated.ts');
const MARKER = 'defineModeratorEndpoint(';
async function walk(dir) {
const entries = await readdir(dir, { withFileTypes: true });
const files = await Promise.all(
entries.map((entry) => {
const full = join(dir, entry.name);
if (entry.isDirectory()) return walk(full);
return entry.isFile() && entry.name.endsWith('.ts') ? [full] : [];
})
);
return files.flat();
}
/** `src/pages/api/mod/homeblock/create.ts` -> `/api/mod/homeblock/create`. */
function routePath(file) {
const rel = relative(join(ROOT, 'src', 'pages'), file).split(sep).join('/');
return '/' + rel.replace(/\.ts$/, '').replace(/\/index$/, '');
}
const files = await walk(API_ROOT);
const matched = [];
for (const file of files) {
const source = await readFile(file, 'utf-8');
if (source.includes(MARKER)) matched.push(file);
}
// Sorted so the output is stable — an unstable order would show up as a spurious diff on every run.
const entries = matched
.map((file) => ({ path: routePath(file), module: routePath(file).replace(/^\/api/, '~/pages/api') }))
.sort((a, b) => a.path.localeCompare(b.path));
const header = [
'// GENERATED by scripts/generate-moderator-endpoint-catalog.mjs — do not edit.',
"// Re-run 'pnpm run generate:moderator-endpoints' after adding or removing a moderator endpoint.",
'',
'export const MODERATOR_ENDPOINT_MODULES: Record<string, () => Promise<unknown>> = {',
].join('\n');
const lines = entries.map((e) => " '" + e.path + "': () => import('" + e.module + "'),");
const body = [header, ...lines, '};', ''].join('\n');
const previous = await readFile(OUT, 'utf-8').catch(() => null);
if (previous !== body) {
await writeFile(OUT, body, 'utf-8');
console.log(`moderator endpoint catalog: wrote ${entries.length} endpoint(s)`);
} else {
console.log(`moderator endpoint catalog: up to date (${entries.length} endpoint(s))`);
}