import * as gulp from "gulp"; import fs from "fs-extra"; import * as path from "path"; import archiver from "archiver"; import stringify from "json-stringify-pretty-compact"; const sourcemaps = require("gulp-sourcemaps"); const uglify = require("gulp-uglify"); const concat = require("gulp-concat"); const buffer = require("vinyl-buffer"); const source = require("vinyl-source-stream"); const through = require("through2"); const jsonminify = require("gulp-jsonminify"); const merge2 = require("merge2"); const git = require("gulp-git-streamed"); const loadJson = (path: string): any => { try { let str = fs.readFileSync(path).toString(); return JSON.parse(str); } catch { throw Error("Unable to load " + path); } }; import { createLiteral, factory, isExportDeclaration, isImportDeclaration, isStringLiteral, LiteralExpression, Node, TransformationContext, Transformer as TSTransformer, TransformerFactory, visitEachChild, visitNode, } from "typescript"; import less from "gulp-less"; import Logger from "./source/utils/Logger"; import { ModuleData } from "@league-of-foundry-developers/foundry-vtt-types/src/foundry/common/packages.mjs"; import browserify from "browserify"; import { data } from "jquery"; const tsify = require("tsify"); const ts = require("gulp-typescript"); const argv = require("yargs").argv; let distPath = "dist"; function getConfig() { const configPath = path.resolve(process.cwd(), "foundryconfig.json"); let config; if (fs.existsSync(configPath)) { config = loadJson(configPath); return config; } else { return; } } interface Manifest { root: string; file: ModuleData; name: string; } const getManifest = (): Manifest | null => { const json: Manifest = { root: "", // @ts-ignore file: {}, name: "", }; if (fs.existsSync("source")) { json.root = "source"; } else { json.root = distPath; } const modulePath = path.join(json.root, "module.json"); const systemPath = path.join(json.root, "system.json"); if (fs.existsSync(modulePath)) { json.file = loadJson(modulePath) as ModuleData; json.name = "module.json"; } else if (fs.existsSync(systemPath)) { json.file = loadJson(systemPath) as ModuleData; json.name = "system.json"; } else { return null; } return json; }; const createTransformer = (): TransformerFactory => { /** * @param {typescript.Node} node */ const shouldMutateModuleSpecifier = (node: Node): boolean => { if (!isImportDeclaration(node) && !isExportDeclaration(node)) return false; if (node.moduleSpecifier === undefined) return false; if (!isStringLiteral(node.moduleSpecifier)) return false; if (!node.moduleSpecifier.text.startsWith("./") && !node.moduleSpecifier.text.startsWith("../")) return false; return path.extname(node.moduleSpecifier.text) === ""; }; return (context: TransformationContext): TSTransformer => { return (node: Node) => { function visitor(node: Node): Node { if (shouldMutateModuleSpecifier(node)) { if (isImportDeclaration(node)) { const newModuleSpecifier = createLiteral(`${(node.moduleSpecifier as LiteralExpression).text}.js`); return factory.updateImportDeclaration(node, node.decorators, node.modifiers, node.importClause, newModuleSpecifier, undefined); } else if (isExportDeclaration(node)) { const newModuleSpecifier = createLiteral(`${(node.moduleSpecifier as LiteralExpression).text}.js`); return factory.updateExportDeclaration(node, node.decorators, node.modifiers, false, node.exportClause, newModuleSpecifier, undefined); } } return visitEachChild(node, visitor, context); } return visitNode(node, visitor); }; }; }; const tsConfig = ts.createProject("tsconfig.json", { getCustomTransformers: (_program: any) => ({ after: [createTransformer()], }), }); function buildTS() { const debug = process.env.npm_lifecycle_event !== "package"; let res = tsConfig.src().pipe(sourcemaps.init()).pipe(tsConfig()); return res.js.pipe(sourcemaps.write("", { debug: debug, includeContent: true, sourceRoot: "./ts/source" })).pipe(gulp.dest(distPath)); } const bundleModule = () => { const debug = argv.dbg || argv.debug; const bsfy = browserify(path.join(__dirname, "source/index.ts"), { debug: debug }); return bsfy .on("error", Logger.err) .plugin(tsify) .bundle() .pipe(source(path.join(distPath, "bundle.js"))) .pipe(buffer()) .pipe(sourcemaps.init({ loadMaps: true })) .pipe(uglify()) .pipe(sourcemaps.write("./")) .pipe(gulp.dest("./")); }; const buildLess = () => { return gulp.src("source/style/*.less").pipe(less()).pipe(concat("bundle.css")).pipe(gulp.dest(distPath)); }; interface Pack { root: string; type: string; name: string; } const buildPack = (pack: Pack): NodeJS.ReadWriteStream => { return gulp .src(pack.root + "/" + pack.type + "/" + pack.name + "/*.json") .pipe(jsonminify()) .pipe(concat(pack.name + ".db")) .pipe(gulp.dest(distPath + "/" + pack.root + "/" + pack.type)); }; const buildPacks = () => { let packs: Pack[] = []; const rootDir = "packs"; const packTypes = fs.readdirSync(rootDir).filter((p) => fs.statSync(path.join(rootDir, p)).isDirectory()); packTypes.forEach((packType) => { const packDir = path.join(rootDir, packType); const packNames = fs.readdirSync(packDir).filter((p) => fs.statSync(path.join(packDir, p)).isDirectory()); packNames.forEach((packName) => { packs.push({ name: packName, type: packType, root: rootDir, }); }); }); return merge2(packs.map((p) => buildPack(p))); }; const copyFiles = async () => { const recursiveFileSearch = (dir: string, callback: (err: NodeJS.ErrnoException | null, res: Array) => void) => { let results: Array = []; fs.readdir(dir, (err, list) => { if (err) return callback(err, results); let pending = list.length; if (!pending) return callback(null, results); for (let file of list) { file = path.resolve(dir, file); fs.stat(file, (err, stat) => { if (stat && stat.isDirectory()) { recursiveFileSearch(file, (err, res) => { results = results.concat(res); if (!--pending) callback(null, results); }); } else { results.push(file); if (!--pending) callback(null, results); } }); } }); }; try { const modulePath = path.join("source", "module.json"); if (fs.existsSync(modulePath)) await fs.copyFile(modulePath, path.join(distPath, "module.json")); const systemPath = path.join("source/system.json"); if (fs.existsSync(systemPath)) await fs.copyFile(systemPath, path.join(distPath, "system.json")); if (!fs.existsSync(path.resolve(__dirname, "assets"))) return Promise.resolve(); const filter = (src: string, dest: string): boolean => { Logger.ok("Copying file: " + dest); return true; }; await fs.copyFile(path.join("source", "template.json"), path.join(distPath, "template.json")); fs.copySync(path.resolve(__dirname, "assets"), path.resolve(__dirname, distPath + "/assets"), { overwrite: true, filter }); fs.copySync(path.resolve(__dirname, "lang"), path.resolve(__dirname, distPath + "/lang"), { overwrite: true, filter }); //fs.copySync(path.resolve(__dirname, "packs"), path.resolve(__dirname, distPath + "/packs"), { overwrite: true, filter }) fs.copySync(path.resolve(__dirname, "templates"), path.resolve(__dirname, distPath + "/templates"), { overwrite: true, filter }); return Promise.resolve(); } catch (err) { await Promise.reject(err); } }; const cleanDist = async () => { if (argv.dbg || argv.debug) return; Logger.log("Cleaning dist file clutter"); const files: string[] = []; const getFiles = async (dir: string) => { const arr = await fs.promises.readdir(dir); for (const entry of arr) { const fullPath = path.join(dir, entry); const stat = await fs.promises.stat(fullPath); if (stat.isDirectory()) await getFiles(fullPath); else files.push(fullPath); } }; await getFiles(path.resolve(distPath)); for (const file of files) { if (file.endsWith("bundle.js") || file.endsWith(".css") || file.endsWith("module.json")) continue; Logger.warn("Cleaning " + path.relative(process.cwd(), file)); await fs.promises.unlink(file); } }; /** * Watch for changes for each build step */ const buildWatch = () => { gulp.watch("source/**/*.ts", { ignoreInitial: false }, gulp.series(buildTS, bundleModule)); gulp.watch("source/**/*.less", { ignoreInitial: false }, buildLess); gulp.watch("packs", { ignoreInitial: false }, buildPacks); gulp.watch(["assets", "lang", "templates", "source/*.json"], { ignoreInitial: false }, copyFiles); }; /********************/ /* CLEAN */ /********************/ /** * Remove built files from `dist` folder * while ignoring source files */ const clean = async () => { if (!fs.existsSync(distPath)) fs.mkdirSync(distPath); else { // Attempt to remove the files try { fs.rmSync(distPath, { recursive: true, force: true }); fs.mkdirSync(distPath); return Promise.resolve(); } catch (err) { await Promise.reject(err); } } }; const setTargetDir = async () => { const dp = process.env.FOUNDRY_PATH; if (!dp) throw Error("FOUNDRY_PATH not defined in environment"); const name = getManifest()!.file.name ?? "midgard5"; distPath = path.join(dp, "Data", "systems", name); }; const linkUserData = async () => { const name = getManifest()!.file.name; let destDir; try { if (fs.existsSync(path.resolve(".", distPath, "module.json")) || fs.existsSync(path.resolve(".", "source", "module.json"))) { destDir = "modules"; } else if (fs.existsSync(path.resolve(".", distPath, "system.json")) || fs.existsSync(path.resolve(".", "source", "system.json"))) { destDir = "systems"; } else { throw Error(`Could not find module.json or system.json`); } let linkDir; const dataPath = process.env.FOUNDRY_PATH; if (dataPath) { if (!fs.existsSync(path.join(dataPath, "Data"))) throw Error("User Data path invalid, no Data directory found"); linkDir = path.join(dataPath, "Data", destDir, name as string); } else { throw Error("FOUNDRY_PATH not defined in environment"); } //if (argv.clean || argv.c) { Logger.warn(`Removing build in ${linkDir}`); fs.rmSync(linkDir, { recursive: true, force: true }); fs.mkdirSync(linkDir); //} Logger.ok(`Copying build to ${linkDir}`); fs.copySync(path.resolve(distPath), linkDir, { overwrite: true }); return Promise.resolve(); } catch (err) { await Promise.reject(err); } }; /*********************/ /* PACKAGE */ /*********************/ /** * Package build */ async function packageBuild() { const manifest = getManifest(); if (manifest === null) { Logger.err("Manifest file could not be loaded."); throw Error(); } return new Promise((resolve, reject) => { try { // Remove the package dir without doing anything else if (argv.clean || argv.c) { Logger.warn("Removing all packaged files"); fs.rmSync(distPath, { force: true, recursive: true }); return; } // Ensure there is a directory to hold all the packaged versions if (!fs.existsSync(distPath)) fs.mkdirSync(distPath); // Initialize the zip file const zipName = `${manifest.file.name}-v${manifest.file.version}.zip`; const zipFile = fs.createWriteStream(zipName); const zip = archiver("zip", { zlib: { level: 9 } }); zipFile.on("close", () => { Logger.ok(zip.pointer() + " total bytes"); Logger.ok(`Zip file ${zipName} has been written`); return resolve(true); }); zipFile.on("end", function () { throw "Data has been drained"; }); zip.on("error", (err) => { throw err; }); zip.pipe(zipFile); zip.directory(distPath, manifest.file.name); return zip.finalize(); } catch (err) { return reject(err); } }); } /*********************/ /* PACKAGE */ /*********************/ /** * Update version and URLs in the manifest JSON */ const updateManifest = (cb: any) => { const packageJson = loadJson("package.json"); const config = getConfig(), manifest = getManifest(), rawURL = config.rawURL, repoURL = config.repository, manifestRoot = manifest!.root; if (!config) cb(Error("foundryconfig.json not found")); if (manifest === null) { cb(Error("Manifest JSON not found")); return; } if (!repoURL) cb(Error("Repository URLs not configured in foundryconfig.json")); 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: string, major: string, minor: string, patch: string) => { 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("Error: Incorrect version arguments.")); } if (targetVersion === currentVersion) { return cb(Error("Error: Target version is identical to current version.")); } Logger.ok(`Updating version number to '${targetVersion}'`); packageJson.version = targetVersion; manifest.file.version = targetVersion; /* Update URLs */ const result = `${repoURL}/releases/download`; manifest.file.url = repoURL; manifest.file.manifest = `${result}/v${manifest.file.version}/${manifest.name}`; manifest.file.download = `${result}/v${manifest.file.version}/${manifest.file.name}-v${manifest.file.version}.zip`; const prettyProjectJson = stringify(manifest.file, { maxLength: 35, indent: "\t", }); fs.writeFileSync("package.json", JSON.stringify(packageJson, null, "\t")); fs.writeFileSync(path.join(manifest.root, manifest.name), prettyProjectJson, "utf8"); return cb(); } catch (err) { return cb(err); } }; const gitTaskManifest = (cb: gulp.TaskFunctionCallback) => { const manifest = getManifest(); if (!manifest) return cb(Error("could not load manifest.")); return gulp .src([`package.json`, `source/system.json`]) .pipe(git.add({ args: "--no-all -f" })) .pipe(git.commit(`v${manifest.file.version}`, { args: "-a", disableAppendPaths: true })); }; const gitTaskBuild = (cb: gulp.TaskFunctionCallback) => { const manifest = getManifest(); if (!manifest) return cb(Error("could not load manifest.")); return gulp .src(`${manifest.file.name}-v${manifest.file.version}.zip`) .pipe(git.checkout(`v${manifest.file.version}`, { args: "-b" })) .pipe(git.add({ args: "--no-all -f" })) .pipe(git.commit(`v${manifest.file.version}`, { args: "-a", disableAppendPaths: true })); }; const execBuild = gulp.parallel(buildTS, buildLess, buildPacks, copyFiles); exports.build = gulp.series(clean, execBuild, bundleModule); exports.buildTarget = gulp.series(setTargetDir, clean, execBuild, bundleModule); exports.watch = buildWatch; exports.watchTarget = gulp.series(setTargetDir, buildWatch); exports.clean = clean; exports.link = linkUserData; exports.package = packageBuild; exports.update = updateManifest; exports.publish = gulp.series(clean, updateManifest, execBuild, bundleModule, packageBuild, gitTaskManifest);