feat: add build-time content sync scaffold
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
|
||||
type ProjectSeed = {
|
||||
name: string;
|
||||
description: string;
|
||||
gitea_repo: string;
|
||||
cover_image: string;
|
||||
demo_video?: string;
|
||||
download_link?: string;
|
||||
tags: string[];
|
||||
featured: boolean;
|
||||
};
|
||||
|
||||
type ShareSeed = {
|
||||
name: string;
|
||||
description: string;
|
||||
url: string;
|
||||
time: string;
|
||||
};
|
||||
|
||||
type GeneratedProject = ProjectSeed & {
|
||||
repo_url?: string;
|
||||
updated_at: string;
|
||||
source: 'seed' | 'gitea' | 'gitea+seafile';
|
||||
};
|
||||
|
||||
type GeneratedShare = ShareSeed & {
|
||||
size?: number;
|
||||
source: 'seed' | 'seafile';
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
type ActivityDay = {
|
||||
date: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
type RecentActivity = {
|
||||
type: string;
|
||||
repo: string;
|
||||
message: string;
|
||||
url: string;
|
||||
time: string;
|
||||
};
|
||||
|
||||
type GeneratedGiteaActivity = {
|
||||
updatedAt: string;
|
||||
source: 'placeholder' | 'gitea';
|
||||
itemCount: number;
|
||||
days: ActivityDay[];
|
||||
recent: RecentActivity[];
|
||||
};
|
||||
|
||||
type SyncConfig = {
|
||||
rootDir: string;
|
||||
outputDir: string;
|
||||
strictSync: boolean;
|
||||
gitea: {
|
||||
baseUrl: string;
|
||||
token: string;
|
||||
username: string;
|
||||
};
|
||||
seafile: {
|
||||
baseUrl: string;
|
||||
token: string;
|
||||
mirrorDownloads: boolean;
|
||||
downloadsOutputDir: string;
|
||||
};
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const config = loadConfig(process.cwd());
|
||||
const syncedAt = new Date().toISOString();
|
||||
|
||||
console.log('[sync-content] start');
|
||||
console.log(`[sync-content] output dir: ${config.outputDir}`);
|
||||
|
||||
await mkdir(config.outputDir, { recursive: true });
|
||||
|
||||
if (config.seafile.mirrorDownloads) {
|
||||
console.warn(
|
||||
'[sync-content] SEAFILE_MIRROR_DOWNLOADS=true, but file mirroring is not implemented yet.',
|
||||
);
|
||||
}
|
||||
|
||||
const [projects, shares, giteaActivity] = await Promise.all([
|
||||
resolveProjects(config, syncedAt),
|
||||
resolveShares(config, syncedAt),
|
||||
resolveGiteaActivity(config, syncedAt),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
writeJson(path.join(config.outputDir, 'projects.json'), projects),
|
||||
writeJson(path.join(config.outputDir, 'shares.json'), shares),
|
||||
writeJson(path.join(config.outputDir, 'gitea-activity.json'), giteaActivity),
|
||||
]);
|
||||
|
||||
console.log(`[sync-content] wrote ${projects.length} projects`);
|
||||
console.log(`[sync-content] wrote ${shares.length} shares`);
|
||||
console.log(`[sync-content] wrote ${giteaActivity.itemCount} activity items`);
|
||||
console.log('[sync-content] done');
|
||||
}
|
||||
|
||||
function loadConfig(rootDir: string): SyncConfig {
|
||||
return {
|
||||
rootDir,
|
||||
outputDir: resolveFromRoot(rootDir, process.env.SYNC_OUTPUT_DIR ?? 'src/data/generated'),
|
||||
strictSync: getBooleanEnv('STRICT_SYNC', false),
|
||||
gitea: {
|
||||
baseUrl: process.env.GITEA_BASE_URL?.trim() ?? '',
|
||||
token: process.env.GITEA_TOKEN?.trim() ?? '',
|
||||
username: process.env.GITEA_USERNAME?.trim() ?? '',
|
||||
},
|
||||
seafile: {
|
||||
baseUrl: process.env.SEAFILE_BASE_URL?.trim() ?? '',
|
||||
token: process.env.SEAFILE_TOKEN?.trim() ?? '',
|
||||
mirrorDownloads: getBooleanEnv('SEAFILE_MIRROR_DOWNLOADS', false),
|
||||
downloadsOutputDir: resolveFromRoot(
|
||||
rootDir,
|
||||
process.env.DOWNLOADS_OUTPUT_DIR ?? 'public/downloads',
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveProjects(
|
||||
config: SyncConfig,
|
||||
syncedAt: string,
|
||||
): Promise<GeneratedProject[]> {
|
||||
const seedProjects = await readJson<ProjectSeed[]>(
|
||||
path.join(config.rootDir, 'src/content/projects/index.json'),
|
||||
);
|
||||
|
||||
if (hasGiteaConfig(config) || hasSeafileConfig(config)) {
|
||||
console.warn(
|
||||
'[sync-content] remote project sync not implemented yet, falling back to local seed data.',
|
||||
);
|
||||
}
|
||||
|
||||
return seedProjects.map((project) => ({
|
||||
...project,
|
||||
repo_url:
|
||||
project.gitea_repo && config.gitea.baseUrl
|
||||
? `${trimTrailingSlash(config.gitea.baseUrl)}/${project.gitea_repo}`
|
||||
: undefined,
|
||||
updated_at: syncedAt,
|
||||
source: 'seed',
|
||||
}));
|
||||
}
|
||||
|
||||
async function resolveShares(config: SyncConfig, syncedAt: string): Promise<GeneratedShare[]> {
|
||||
const seedShares = await readJson<ShareSeed[]>(
|
||||
path.join(config.rootDir, 'src/content/shares/index.json'),
|
||||
);
|
||||
|
||||
if (hasSeafileConfig(config)) {
|
||||
console.warn(
|
||||
'[sync-content] remote share sync not implemented yet, falling back to local seed data.',
|
||||
);
|
||||
}
|
||||
|
||||
return seedShares.map((share) => ({
|
||||
...share,
|
||||
source: 'seed',
|
||||
updated_at: syncedAt,
|
||||
}));
|
||||
}
|
||||
|
||||
async function resolveGiteaActivity(
|
||||
config: SyncConfig,
|
||||
syncedAt: string,
|
||||
): Promise<GeneratedGiteaActivity> {
|
||||
if (hasGiteaConfig(config)) {
|
||||
if (config.strictSync) {
|
||||
throw new Error(
|
||||
'STRICT_SYNC=true, but remote Gitea activity sync is not implemented yet.',
|
||||
);
|
||||
}
|
||||
|
||||
console.warn(
|
||||
'[sync-content] remote Gitea activity sync not implemented yet, writing placeholder output.',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
updatedAt: syncedAt,
|
||||
source: 'placeholder',
|
||||
itemCount: 0,
|
||||
days: [],
|
||||
recent: [],
|
||||
};
|
||||
}
|
||||
|
||||
function hasGiteaConfig(config: SyncConfig) {
|
||||
return Boolean(config.gitea.baseUrl && config.gitea.token && config.gitea.username);
|
||||
}
|
||||
|
||||
function hasSeafileConfig(config: SyncConfig) {
|
||||
return Boolean(config.seafile.baseUrl && config.seafile.token);
|
||||
}
|
||||
|
||||
function getBooleanEnv(name: string, fallback: boolean) {
|
||||
const value = process.env[name];
|
||||
if (value == null || value.trim() === '') {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase());
|
||||
}
|
||||
|
||||
function resolveFromRoot(rootDir: string, targetPath: string) {
|
||||
return path.isAbsolute(targetPath) ? targetPath : path.join(rootDir, targetPath);
|
||||
}
|
||||
|
||||
function trimTrailingSlash(value: string) {
|
||||
return value.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
async function readJson<T>(filePath: string): Promise<T> {
|
||||
const raw = await readFile(filePath, 'utf-8');
|
||||
return JSON.parse(raw) as T;
|
||||
}
|
||||
|
||||
async function writeJson(filePath: string, data: unknown) {
|
||||
await mkdir(path.dirname(filePath), { recursive: true });
|
||||
await writeFile(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf-8');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('[sync-content] failed');
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Reference in New Issue
Block a user