import fs from "node:fs" import path from "node:path" import { PACK_DEFINITIONS, buildPacks } from "./pack-builder.mjs" const systemRoot = path.resolve(import.meta.dirname, "..") const targetRoot = path.resolve( process.env.FVTT_LES_OUBLIES_BASE_ROOT || path.join(systemRoot, "..", "fvtt-les-oublies-base"), ) const systemManifestPath = path.join(systemRoot, "system.json") const systemPackagePath = path.join(systemRoot, "package.json") const systemSourceRoot = path.join(systemRoot, "packs-src") const targetSourceRoot = path.join(targetRoot, "packs-src") const targetPacksRoot = path.join(targetRoot, "packs") const moduleRepoUrl = "https://www.uberwald.me/gitea/public/fvtt-les-oublies-base" const systemManifest = JSON.parse(fs.readFileSync(systemManifestPath, "utf8")) const systemPackage = JSON.parse(fs.readFileSync(systemPackagePath, "utf8")) const richFieldMap = Object.fromEntries( Object.entries(systemManifest.documentTypes?.Item ?? {}).map(([type, data]) => [type, data.htmlFields ?? []]), ) const coreVersion = String(systemManifest.compatibility?.verified ?? systemManifest.compatibility?.minimum ?? "") const basePackDefinitions = PACK_DEFINITIONS.map((pack) => ({ ...pack, outputFolder: `base-${pack.outputFolder}`, })) function setDeepValue(target, propertyPath, value) { const segments = String(propertyPath || "").split(".").filter(Boolean) if (!segments.length) return let cursor = target while (segments.length > 1) { const segment = segments.shift() if (!(segment in cursor) || typeof cursor[segment] !== "object" || cursor[segment] === null) { cursor[segment] = {} } cursor = cursor[segment] } cursor[segments[0]] = value } function sanitizeEntries(entries = []) { let clearedFields = 0 const sanitized = entries.map((entry) => { const fields = richFieldMap[entry.type] ?? [] if (!fields.length) return entry const clone = structuredClone(entry) clone.system ??= {} for (const fieldPath of fields) { setDeepValue(clone.system, fieldPath, "") clearedFields += 1 } return clone }) return { sanitized, clearedFields } } function countNonEmptyRichFields(entries = []) { let nonEmpty = 0 for (const entry of entries) { for (const fieldPath of richFieldMap[entry.type] ?? []) { const value = fieldPath .split(".") .reduce((cursor, segment) => cursor?.[segment], entry.system ?? {}) if ((value ?? "") !== "") nonEmpty += 1 } } return nonEmpty } function parseJsonArray(rawText, filePath) { const parsed = JSON.parse(rawText) if (!Array.isArray(parsed)) { return { parsed, entries: null, isArray: false, error: `${filePath} must contain a JSON array`, } } return { parsed, entries: parsed, isArray: true, error: null, } } function ensureWritableTargetRoot() { const parentDir = path.dirname(targetRoot) fs.mkdirSync(parentDir, { recursive: true }) fs.accessSync(parentDir, fs.constants.W_OK) fs.mkdirSync(targetRoot, { recursive: true }) fs.accessSync(targetRoot, fs.constants.W_OK) } function ensureTargetModuleScaffold() { ensureWritableTargetRoot() fs.mkdirSync(targetSourceRoot, { recursive: true }) fs.mkdirSync(targetPacksRoot, { recursive: true }) const moduleManifestPath = path.join(targetRoot, "module.json") const moduleManifest = { id: "fvtt-les-oublies-base", title: "Les Oubliés Base", description: "Module de contenu pour Les Oubliés, conservant les compendiums complets avec leurs textes descriptifs.", manifest: `${moduleRepoUrl}/raw/branch/main/module.json`, download: "#{DOWNLOAD}#", url: moduleRepoUrl, version: systemPackage.version, authors: [ { name: "Copilot", flags: {}, }, ], compatibility: systemManifest.compatibility, relationships: { requires: [ { id: systemManifest.id, type: "system", compatibility: { minimum: systemManifest.compatibility?.minimum ?? undefined, verified: systemManifest.compatibility?.verified ?? undefined, }, }, ], systems: [ { id: systemManifest.id, type: "system", compatibility: { minimum: systemManifest.compatibility?.minimum ?? undefined, verified: systemManifest.compatibility?.verified ?? undefined, }, }, ], }, packs: (systemManifest.packs ?? []).map((pack) => ({ ...pack, name: `base-${pack.name}`, path: `packs/base-${pack.name}`, system: systemManifest.id, })), } fs.writeFileSync(moduleManifestPath, `${JSON.stringify(moduleManifest, null, 2)}\n`) } function pruneStalePackDirectories(outputRoot, expectedDefinitions) { if (!fs.existsSync(outputRoot)) return const expected = new Set(expectedDefinitions.map((definition) => definition.outputFolder)) for (const entry of fs.readdirSync(outputRoot, { withFileTypes: true })) { if (!entry.isDirectory()) continue if (expected.has(entry.name)) continue fs.rmSync(path.join(outputRoot, entry.name), { recursive: true, force: true }) } } function copyAndSanitizeSources() { const summaries = [] const sourceFiles = fs.readdirSync(systemSourceRoot) .filter((entry) => entry.endsWith(".json")) .sort((left, right) => left.localeCompare(right, "fr")) for (const fileName of sourceFiles) { const systemSourcePath = path.join(systemSourceRoot, fileName) const targetSourcePath = path.join(targetSourceRoot, fileName) const rawText = fs.readFileSync(systemSourcePath, "utf8") const systemJson = parseJsonArray(rawText, systemSourcePath) const targetRawText = fs.existsSync(targetSourcePath) ? fs.readFileSync(targetSourcePath, "utf8") : null const targetJson = targetRawText ? parseJsonArray(targetRawText, targetSourcePath) : null const systemRichCount = systemJson.isArray ? countNonEmptyRichFields(systemJson.entries) : -1 const targetRichCount = targetJson?.isArray ? countNonEmptyRichFields(targetJson.entries) : -1 const authoritativeRawText = targetRichCount > systemRichCount ? targetRawText : rawText fs.writeFileSync(targetSourcePath, authoritativeRawText) if (!systemJson.isArray) { summaries.push({ fileName, clearedFields: 0, copiedOnly: true }) continue } const { sanitized, clearedFields } = sanitizeEntries(systemJson.entries) fs.writeFileSync(systemSourcePath, `${JSON.stringify(sanitized, null, 2)}\n`) summaries.push({ fileName, clearedFields, copiedOnly: false }) } return summaries } ensureTargetModuleScaffold() const summaries = copyAndSanitizeSources() pruneStalePackDirectories(targetPacksRoot, basePackDefinitions) await buildPacks({ sourceRoot: systemSourceRoot, outputRoot: path.join(systemRoot, "packs"), packDefinitions: PACK_DEFINITIONS, documentSystemId: systemManifest.id, documentSystemVersion: systemPackage.version, coreVersion, }) await buildPacks({ sourceRoot: targetSourceRoot, outputRoot: targetPacksRoot, packDefinitions: basePackDefinitions, documentSystemId: systemManifest.id, documentSystemVersion: systemPackage.version, coreVersion, }) console.info(`Base module root: ${targetRoot}`) for (const summary of summaries) { if (summary.copiedOnly) { console.info(`${summary.fileName}: copied as-is`) } else { console.info(`${summary.fileName}: cleared ${summary.clearedFields} rich fields in system source`) } }