const gulp = require("gulp"); const fs = require("fs-extra"); const path = require("path"); const chalk = require("chalk"); const stringify = require("json-stringify-pretty-compact"); const typescript = require("typescript"); const ts = require("gulp-typescript"); const sass = require("gulp-sass"); const argv = require("yargs").argv; sass.compiler = require("sass"); function getConfig() { const configPath = path.resolve(process.cwd(), "foundryconfig.json"); let config; if (fs.existsSync(configPath)) { config = fs.readJSONSync(configPath); return config; } else { return; } } function getManifest() { const json = {}; if (fs.existsSync("src")) { json.root = "src"; } else { json.root = "dist"; } const modulePath = path.join(json.root, "module.json"); const systemPath = path.join(json.root, "system.json"); if (fs.existsSync(modulePath)) { json.file = fs.readJSONSync(modulePath); json.name = "module.json"; } else if (fs.existsSync(systemPath)) { json.file = fs.readJSONSync(systemPath); json.name = "system.json"; } else { return; } return json; } /** * TypeScript transformers * @returns {typescript.TransformerFactory} */ function createTransformer() { /** * @param {typescript.Node} node */ function shouldMutateModuleSpecifier(node) { if (!typescript.isImportDeclaration(node) && !typescript.isExportDeclaration(node)) return false; if (node.moduleSpecifier === undefined) return false; if (!typescript.isStringLiteral(node.moduleSpecifier)) return false; if (!node.moduleSpecifier.text.startsWith("./") && !node.moduleSpecifier.text.startsWith("../")) return false; if (path.extname(node.moduleSpecifier.text) !== "") return false; return true; } /** * Transforms import/export declarations to append `.js` extension * @param {typescript.TransformationContext} context */ function importTransformer(context) { return (node) => { /** * @param {typescript.Node} node */ function visitor(node) { if (shouldMutateModuleSpecifier(node)) { if (typescript.isImportDeclaration(node)) { const newModuleSpecifier = typescript.createLiteral(`${node.moduleSpecifier.text}.js`); return typescript.updateImportDeclaration( node, node.decorators, node.modifiers, node.importClause, newModuleSpecifier, ); } else if (typescript.isExportDeclaration(node)) { const newModuleSpecifier = typescript.createLiteral(`${node.moduleSpecifier.text}.js`); return typescript.updateExportDeclaration( node, node.decorators, node.modifiers, node.exportClause, newModuleSpecifier, ); } } return typescript.visitEachChild(node, visitor, context); } return typescript.visitNode(node, visitor); }; } return importTransformer; } const tsConfig = ts.createProject("tsconfig.json", { getCustomTransformers: (_program) => ({ after: [createTransformer()], }), }); /********************/ /* BUILD */ /********************/ /** * Build TypeScript */ function buildTS() { return gulp.src("src/**/*.ts").pipe(tsConfig()).pipe(gulp.dest("dist")); } /** * Build SASS */ function buildSASS() { return gulp.src("src/*.scss").pipe(sass().on("error", sass.logError)).pipe(gulp.dest("dist")); } /** * Copy static files */ async function copyFiles() { const statics = ["lang", "fonts", "assets", "templates", "module.json", "system.json", "template.json"]; try { for (const file of statics) { if (fs.existsSync(path.join("src", file))) { await fs.copy(path.join("src", file), path.join("dist", file)); } } return Promise.resolve(); } catch (err) { Promise.reject(err); } } /** * Watch for changes for each build step */ function buildWatch() { gulp.watch("src/**/*.ts", { ignoreInitial: false }, buildTS); gulp.watch("src/**/*.scss", { ignoreInitial: false }, buildSASS); gulp.watch(["src/fonts", "src/lang", "src/templates", "src/*.json"], { ignoreInitial: false }, copyFiles); } /********************/ /* CLEAN */ /********************/ /** * Remove built files from `dist` folder * while ignoring source files */ async function clean() { const name = path.basename(path.resolve(".")); const files = []; // If the project uses TypeScript if (fs.existsSync(path.join("src", `${name}.ts`))) { files.push( "lang", "templates", "assets", "module", `${name}.js`, "module.json", "system.json", "template.json", ); } // If the project uses SASS if (fs.existsSync(path.join("src", `${name}.scss`))) { files.push("fonts", `${name}.css`); } console.log(" ", chalk.yellow("Files to clean:")); console.log(" ", chalk.blueBright(files.join("\n "))); // Attempt to remove the files try { for (const filePath of files) { await fs.remove(path.join("dist", filePath)); } return Promise.resolve(); } catch (err) { Promise.reject(err); } } /********************/ /* LINK */ /********************/ /** * Link build to User Data folder */ async function linkUserData() { const name = path.basename(path.resolve(".")); const config = fs.readJSONSync("foundryconfig.json"); let destDir; try { if ( fs.existsSync(path.resolve(".", "dist", "module.json")) || fs.existsSync(path.resolve(".", "src", "module.json")) ) { destDir = "modules"; } else if ( fs.existsSync(path.resolve(".", "dist", "system.json")) || fs.existsSync(path.resolve(".", "src", "system.json")) ) { destDir = "systems"; } else { throw Error(`Could not find ${chalk.blueBright("module.json")} or ${chalk.blueBright("system.json")}`); } let linkDir; if (config.dataPath) { if (!fs.existsSync(path.join(config.dataPath, "Data"))) throw Error("User Data path invalid, no Data directory found"); linkDir = path.join(config.dataPath, "Data", destDir, name); } else { throw Error("No User Data path defined in foundryconfig.json"); } if (argv.clean || argv.c) { console.log(chalk.yellow(`Removing build in ${chalk.blueBright(linkDir)}`)); await fs.remove(linkDir); } else if (!fs.existsSync(linkDir)) { console.log(chalk.green(`Copying build to ${chalk.blueBright(linkDir)}`)); await fs.symlink(path.resolve("./dist"), linkDir); } return Promise.resolve(); } catch (err) { Promise.reject(err); } } /*********************/ /* PACKAGE */ /*********************/ /** * Update version and URLs in the manifest JSON */ function updateManifest(cb) { const packageJson = fs.readJSONSync("package.json"); const manifest = getManifest(); if (!manifest) cb(Error(chalk.red("Manifest JSON not found"))); try { const version = argv.update || argv.u; /* Update version */ const versionMatch = /^(\d{1,}).(\d{1,}).(\d{1,})$/; const currentVersion = manifest.file.version; let targetVersion = ""; if (!version) { cb(Error("Missing version number")); } if (versionMatch.test(version)) { targetVersion = version; } else { targetVersion = currentVersion.replace(versionMatch, (substring, major, minor, patch) => { console.log(substring, Number(major) + 1, Number(minor) + 1, Number(patch) + 1); if (version === "major") { return `${Number(major) + 1}.0.0`; } else if (version === "minor") { return `${major}.${Number(minor) + 1}.0`; } else if (version === "patch") { return `${major}.${minor}.${Number(patch) + 1}`; } else { return ""; } }); } if (targetVersion === "") { return cb(Error(chalk.red("Error: Incorrect version arguments."))); } if (targetVersion === currentVersion) { return cb(Error(chalk.red("Error: Target version is identical to current version."))); } console.log(`Updating version number to '${targetVersion}'`); packageJson.version = targetVersion; manifest.file.version = targetVersion; /* Update URL */ const result = `https://git.f3l.de/dungeonslayers/ds4/-/jobs/artifacts/${targetVersion}/download?job=build`; manifest.file.download = result; const prettyProjectJson = stringify(manifest.file, { maxLength: 40, indent: 4, }) + "\n"; fs.writeJSONSync("package.json", packageJson, { spaces: 4 }); fs.writeFileSync(path.join(manifest.root, manifest.name), prettyProjectJson, "utf8"); return cb(); } catch (err) { cb(err); } } const execBuild = gulp.parallel(buildTS, buildSASS, copyFiles); exports.build = gulp.series(clean, execBuild); exports.watch = buildWatch; exports.clean = clean; exports.link = linkUserData; exports.updateManifest = updateManifest;