Files
fvtt-les-oublies/scripts/split-compendium-content.mjs
T
2026-05-06 20:26:31 +02:00

234 lines
7.7 KiB
JavaScript

import fs from "node:fs"
import path from "node:path"
import { CONTENT_PACK_DEFINITIONS, SYSTEM_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 = CONTENT_PACK_DEFINITIONS.map((pack) => ({
...pack,
outputFolder: `base-${pack.outputFolder}`,
}))
const contentPackNames = new Set(CONTENT_PACK_DEFINITIONS.map((pack) => 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) => ({
// The base content module only mirrors content packs, not system journals or utility packs.
...pack,
}))
.filter((pack) => contentPackNames.has(pack.name))
.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: SYSTEM_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`)
}
}