mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-26 13:06:57 +09:00
Themes are data, not code — so the backend is a simple validated-JSON store, no zips/checksums/permissions. - Theme model + migration 078_themes - theme_tokens.py: server-side mirror of the token whitelist + validators (the gate: a malicious/malformed theme never reaches the DB) - theme_service: bundled-seed loading, import/list/delete, panel default, pre-auth public/active - /api/v1/themes: /installed, /import (admin), DELETE (admin), /default get+set (admin), unauthenticated /public/active for login/setup, plus /registry + /registry/<slug>/install - theme_registry_service: offline-tolerant registry fetch (TTL + last-good + bundled fallback), mirrors the extension registry pattern - bundled seed themes synced to app/data/themes + themes_index.json - 18 proving tests (server-side validation sanitizes url()/;/} smuggling, admin gate, panel-default → public/active, offline fallback, install flow) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
60 lines
2.4 KiB
JavaScript
60 lines
2.4 KiB
JavaScript
#!/usr/bin/env node
|
|
// Sync the bundled seed themes from the frontend (the authoring source of
|
|
// truth) into the backend, so the pre-auth GET /themes/public/active default
|
|
// and the offline registry fallback have the same theme data without a second
|
|
// hand-maintained copy (plan 60). Mirrors the repo's other sync-* scripts.
|
|
//
|
|
// Run from anywhere: node frontend/scripts/sync-bundled-themes.mjs
|
|
//
|
|
// Writes:
|
|
// backend/app/data/themes/<slug>.json (verbatim copies)
|
|
// backend/app/data/themes_index.json (generated {schema_version, themes:[…]})
|
|
|
|
import { readFileSync, writeFileSync, readdirSync, mkdirSync, rmSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, resolve, join } from 'node:path';
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = resolve(here, '..', '..');
|
|
const srcDir = resolve(repoRoot, 'frontend', 'src', 'data', 'themes');
|
|
const outDir = resolve(repoRoot, 'backend', 'app', 'data', 'themes');
|
|
const indexPath = resolve(repoRoot, 'backend', 'app', 'data', 'themes_index.json');
|
|
|
|
// Fresh copy of the themes dir so a removed seed theme doesn't linger.
|
|
rmSync(outDir, { recursive: true, force: true });
|
|
mkdirSync(outDir, { recursive: true });
|
|
|
|
const files = readdirSync(srcDir).filter((f) => f.endsWith('.json')).sort();
|
|
const summaries = [];
|
|
|
|
for (const file of files) {
|
|
const raw = readFileSync(join(srcDir, file), 'utf8');
|
|
const theme = JSON.parse(raw);
|
|
writeFileSync(join(outDir, file), raw.endsWith('\n') ? raw : `${raw}\n`);
|
|
// The index carries only the light gallery metadata (not the full token
|
|
// maps) — install fetches the full theme.json when needed.
|
|
summaries.push({
|
|
slug: theme.slug,
|
|
name: theme.name,
|
|
author: theme.author || 'serverkit',
|
|
version: theme.version || '1.0.0',
|
|
description: theme.description || '',
|
|
base: theme.base || 'dark',
|
|
preview: theme.preview || [],
|
|
bundled: true,
|
|
});
|
|
}
|
|
|
|
// Stable order: default first, then alphabetical by name.
|
|
summaries.sort((a, b) => {
|
|
if (a.slug === 'default') return -1;
|
|
if (b.slug === 'default') return 1;
|
|
return (a.name || a.slug).localeCompare(b.name || b.slug);
|
|
});
|
|
|
|
const index = { schema_version: 1, themes: summaries };
|
|
writeFileSync(indexPath, `${JSON.stringify(index, null, 2)}\n`);
|
|
|
|
console.log(`✓ synced ${files.length} bundled theme(s) → backend/app/data/themes/`);
|
|
console.log(`✓ wrote themes_index.json (${summaries.length} entries)`);
|