// SPDX-FileCopyrightText: 2021 Johannes Loher // // SPDX-License-Identifier: MIT const { rollup } = require("rollup"); const argv = require("yargs").argv; const chalk = require("chalk"); const fs = require("fs-extra"); const gulp = require("gulp"); const path = require("path"); const rollupConfig = require("./rollup.config"); const semver = require("semver"); const sass = require("gulp-sass")(require("sass")); const Datastore = require("@seald-io/nedb"); const { Transform } = require("stream"); /********************/ /* CONFIGURATION */ /********************/ const name = path.basename(path.resolve(".")); const sourceDirectory = "./src"; const distDirectory = "./dist"; const stylesDirectory = path.join(sourceDirectory, "scss"); const packsDirectory = path.join(sourceDirectory, "packs"); const stylesExtension = ".scss"; const sourceFileExtension = ".ts"; const staticFiles = ["assets", "fonts", "lang", "templates", "system.json", "template.json"]; const getDownloadURL = (version) => `https://git.f3l.de/dungeonslayers/ds4/-/releases/${version}/downloads/ds4.zip`; const getChangelogURL = (version) => `https://git.f3l.de/dungeonslayers/ds4/-/releases/${version}`; /********************/ /* BUILD */ /********************/ /** * Build the distributable JavaScript code */ async function buildCode() { const build = await rollup({ input: rollupConfig.input, plugins: rollupConfig.plugins }); return build.write(rollupConfig.output); } /** * Build style sheets */ function buildStyles() { return gulp .src(path.join(stylesDirectory, `${name}${stylesExtension}`)) .pipe(sass().on("error", sass.logError)) .pipe(gulp.dest(path.join(distDirectory, "css"))); } /** * Remove unwanted data from a pack entry */ function cleanPackEntry(entry, cleanSourceId = true) { if (cleanSourceId) { delete entry.flags?.core?.sourceId; } Object.keys(entry.flags).forEach((scope) => { if (Object.keys(entry.flags[scope]).length === 0) { delete entry.flags[scope]; } }); if (entry.permission) entry.permission = { default: 0 }; const embeddedDocumentCollections = [ "drawings", "effects", "items", "lights", "notes", "results", "sounds", "templates", "tiles", "tokens", "walls", ]; embeddedDocumentCollections .flatMap((embeddedDocumentCollection) => entry[embeddedDocumentCollection] ?? []) .forEach((embeddedEntry) => cleanPackEntry(embeddedEntry, false)); return entry; } /** * Convert a stream of JSON files to NeDB files */ const jsonToNeDB = () => new Transform({ transform(file, _, callback) { try { file.contents = Buffer.from( JSON.parse(file.contents.toString()) .map(cleanPackEntry) .map((entry) => JSON.stringify(entry)) .join("\n") + "\n", ); file.path = path.join( path.dirname(file.path), path.basename(file.path, path.extname(file.path)) + ".db", ); callback(undefined, file); } catch (err) { callback(err); } }, objectMode: true, }); /** * build compendium packs */ function buildPacks() { return gulp .src(path.join(packsDirectory, "*.json")) .pipe(jsonToNeDB()) .pipe(gulp.dest(path.join(distDirectory, "packs"))); } /** * Convert a stream of NeDB files to JSON files */ const neDBToJSON = () => new Transform({ transform(file, _, callback) { try { const db = new Datastore({ filename: file.path, autoload: true }); db.find({}, (err, docs) => { if (err) { callback(err); } else { file.contents = Buffer.from(JSON.stringify(docs.map(cleanPackEntry), undefined, 4) + "\n"); file.path = path.join( path.dirname(file.path), path.basename(file.path, path.extname(file.path)) + ".json", ); callback(undefined, file); } }); } catch (err) { callback(err); } }, objectMode: true, }); /** * Generate JSON files from the compendium packs in the distribution directory */ function generateJSONsFromPacks() { return gulp .src(path.join(distDirectory, "packs", "*.db")) .pipe(neDBToJSON()) .pipe(gulp.dest(path.join(packsDirectory))); } /** * Copy static files */ async function copyFiles() { for (const file of staticFiles) { if (fs.existsSync(path.join(sourceDirectory, file))) { await fs.copy(path.join(sourceDirectory, file), path.join(distDirectory, file)); } } } /** * Watch for changes for each build step */ function buildWatch() { gulp.watch( path.join(sourceDirectory, "**", `*${sourceFileExtension}`).replace(/\\/g, "/"), { ignoreInitial: false }, buildCode, ); gulp.watch( path.join(stylesDirectory, "**", `*${stylesExtension}`).replace(/\\/g, "/"), { ignoreInitial: false }, buildStyles, ); gulp.watch(path.join(packsDirectory, "**", "*.json").replace(/\\/g, "/"), { ignoreInitial: false }, buildPacks); gulp.watch( staticFiles.map((file) => path.join(sourceDirectory, file).replace(/\\/g, "/")), { ignoreInitial: false }, copyFiles, ); } /********************/ /* CLEAN */ /********************/ /** * Remove built files from `dist` folder while ignoring source files */ async function clean() { const files = [...staticFiles, "packs", "module"]; if (fs.existsSync(path.join(stylesDirectory, `${name}${stylesExtension}`))) { files.push("css"); } console.log(" ", chalk.yellow("Files to clean:")); console.log(" ", chalk.blueBright(files.join("\n "))); for (const filePath of files) { await fs.remove(path.join(distDirectory, filePath)); } } /********************/ /* LINK */ /********************/ /** * Get the data path of Foundry VTT based on what is configured in `foundryconfig.json` */ function getDataPath() { const config = fs.readJSONSync("foundryconfig.json"); if (config?.dataPath) { if (!fs.existsSync(path.resolve(config.dataPath))) { throw new Error("User Data path invalid, no Data directory found"); } return path.resolve(config.dataPath); } else { throw new Error("No User Data path defined in foundryconfig.json"); } } /** * Link build to User Data folder */ async function linkUserData() { let destinationDirectory; if (fs.existsSync(path.resolve(".", sourceDirectory, "system.json"))) { destinationDirectory = "systems"; } else { throw new Error(`Could not find ${chalk.blueBright("system.json")}`); } const linkDirectory = path.resolve(getDataPath(), destinationDirectory, name); if (argv.clean || argv.c) { console.log(chalk.yellow(`Removing build in ${chalk.blueBright(linkDirectory)}.`)); await fs.remove(linkDirectory); } else if (!fs.existsSync(linkDirectory)) { console.log(chalk.green(`Copying build to ${chalk.blueBright(linkDirectory)}.`)); await fs.ensureDir(path.resolve(linkDirectory, "..")); await fs.symlink(path.resolve(".", distDirectory), linkDirectory); } } /********************/ /* VERSIONING */ /********************/ /** * Get the contents of the manifest file as object. */ function getManifest() { const manifestPath = path.join(sourceDirectory, "system.json"); if (fs.existsSync(manifestPath)) { return { file: fs.readJSONSync(manifestPath), name: "system.json", }; } } /** * Get the target version based on on the current version and the argument passed as release. */ function getTargetVersion(currentVersion, release) { if (["major", "premajor", "minor", "preminor", "patch", "prepatch", "prerelease"].includes(release)) { return semver.inc(currentVersion, release); } else { return semver.valid(release); } } /** * Update version and download URL. */ function bumpVersion(cb) { const packageJson = fs.readJSONSync("package.json"); const manifest = getManifest(); if (!manifest) cb(Error(chalk.red("Manifest JSON not found"))); try { const release = argv.release || argv.r; const currentVersion = packageJson.version; if (!release) { return cb(Error("Missing release type")); } const targetVersion = getTargetVersion(currentVersion, release); if (!targetVersion) { return cb(new Error(chalk.red("Error: Incorrect version arguments"))); } if (targetVersion === currentVersion) { return cb(new Error(chalk.red("Error: Target version is identical to current version"))); } console.log(`Updating version number to '${targetVersion}'`); packageJson.version = targetVersion; fs.writeJSONSync("package.json", packageJson, { spaces: 4 }); manifest.file.version = targetVersion; manifest.file.download = getDownloadURL(targetVersion); manifest.file.changelog = getChangelogURL(targetVersion); fs.writeJSONSync(path.join(sourceDirectory, manifest.name), manifest.file, { spaces: 4 }); return cb(); } catch (err) { cb(err); } } const execBuild = gulp.parallel(buildCode, buildStyles, buildPacks, copyFiles); exports.build = gulp.series(clean, execBuild); exports.watch = buildWatch; exports.clean = clean; exports.link = linkUserData; exports.bumpVersion = bumpVersion; exports.generateJSONsFromPacks = generateJSONsFromPacks;