import { existsSync, promises as fs } from 'node:fs' import path from 'node:path' import { listOfficialEffects, resolveEffectSwfUrl } from '@/lib/services/effect-source' import { listLocalEffects, mergeEffect, removeEffect } from '@/lib/services/effectmap' import { downloadFile } from '@/lib/services/import/core/download' import { convertSwfToNitro } from '@/lib/services/swf-to-nitro' import type { EffectListItem, EffectMapEntry, ImportEffectResult } from '@/types/effects' export const EFFECT_NITRO_DIR = path.join( /*turbopackIgnore: true*/ process.cwd(), 'public/nitro-assets/bundled/effect', ) export const EFFECT_SWF_DIR = path.join( /*turbopackIgnore: true*/ process.cwd(), 'public/swf/gordon', ) export async function ensureEffectDirs(): Promise { await Promise.all( [EFFECT_NITRO_DIR, EFFECT_SWF_DIR] .filter((dir) => !existsSync(/*turbopackIgnore: true*/ dir)) .map((dir) => fs.mkdir(/*turbopackIgnore: true*/ dir, { recursive: true })), ) } function nitroPathFor(lib: string): string { return path.join(/*turbopackIgnore: true*/ EFFECT_NITRO_DIR, `${lib}.nitro`) } export async function importSingleEffect(params: { id: string lib: string type: string revision: number onProgress?: (status: string) => void }): Promise { const { id, lib, type, revision, onProgress } = params const warnings: string[] = [] await ensureEffectDirs() const nitroPath = nitroPathFor(lib) // Download SWF onProgress?.('downloading') const swfPath = path.join(/*turbopackIgnore: true*/ EFFECT_SWF_DIR, `${lib}.swf`) let swfUrl: string try { swfUrl = await resolveEffectSwfUrl(lib) } catch (err) { return { ok: false, lib, warnings, error: `Could not resolve SWF URL: ${(err as Error).message}`, } } const dl = await downloadFile(swfUrl, swfPath, { validate: 'swf' }) if (!dl.ok) return { ok: false, lib, warnings, error: `SWF download failed: ${swfUrl}` } // Convert SWF → Nitro onProgress?.('converting') try { const swfBuffer = await fs.readFile(/*turbopackIgnore: true*/ swfPath) const result = convertSwfToNitro(swfBuffer, lib) await fs.writeFile(/*turbopackIgnore: true*/ nitroPath, result.bundle) if (result.warnings?.length) warnings.push(...result.warnings) } catch (err) { return { ok: false, lib, warnings, error: `SWF→Nitro conversion failed: ${(err as Error).message}`, } } // Merge EffectMap.json (last step — no orphan entry if anything above failed) onProgress?.('writing_map') try { const entry: EffectMapEntry = { id, lib, type, revision } await mergeEffect(entry) } catch (err) { warnings.push(`EffectMap.json merge failed: ${(err as Error).message}`) return { ok: false, lib, warnings, error: 'EffectMap.json merge failed' } } return { ok: true, lib, warnings } } export async function deleteImportedEffect( idOrLib: string, ): Promise<{ removed: boolean; deletedFile: boolean }> { const local = await listLocalEffects() const entry = local.find((e) => e.id === idOrLib || e.lib === idOrLib) const removed = await removeEffect(idOrLib) let deletedFile = false if (entry) { const p = nitroPathFor(entry.lib) if (existsSync(/*turbopackIgnore: true*/ p)) { await fs.unlink(/*turbopackIgnore: true*/ p).catch(() => {}) deletedFile = true } } return { removed, deletedFile } } export async function getEffectList(search: string): Promise { const [official, local] = await Promise.all([listOfficialEffects(), listLocalEffects()]) const localByLib = new Map(local.map((e) => [e.lib, e])) const term = search.trim().toLowerCase() const merged = official.map((e) => { const present = localByLib.has(e.lib) return { ...e, present, nitroExists: existsSync(/*turbopackIgnore: true*/ nitroPathFor(e.lib)), } }) return term ? merged.filter( (e) => e.lib.toLowerCase().includes(term) || e.id.toLowerCase().includes(term) || e.type.includes(term), ) : merged } export async function getEffectStats(): Promise<{ official: number imported: number missing: number }> { const [official, local] = await Promise.all([listOfficialEffects(), listLocalEffects()]) const localLibs = new Set(local.map((e) => e.lib)) const imported = official.filter((e) => localLibs.has(e.lib)).length return { official: official.length, imported, missing: official.length - imported } }