// deno task pack-mod mods/copper_tools [copper_tools.bmod] // builds a mod folder into a .bmod: its data merged into data.json, each script bundled into one module, and its // textures and credits. deno task build does this for every mod in mods/, see "Mod files" in MODS.md import { basename, dirname, resolve } from "@std/path"; import { type Bmod, BMOD_EXTENSION, SCRIPT_SIDES, type ScriptSide, write_bmod } from "$/common/bmod.ts"; import type { ManifestJson } from "$/common/mod_data.ts"; import { load_and_check, type LoadedMod } from "./check_mods.ts"; // what each script is bundled for: the server runs in deno, clients in browsers, worldgen in both const PLATFORMS: Record = { server: "deno", client: "browser", worldgen: "browser" }; // a mod that passed check-mods, as .bmod bytes export async function pack_mod(mod: LoadedMod): Promise> { const manifest = mod.manifest as unknown as ManifestJson; const scripts: Bmod["scripts"] = {}; for (const side of SCRIPT_SIDES) { const entry = manifest.scripts?.[side]; if (entry) scripts[side] = await bundle_script(mod.dir, `${mod.dir}/${entry}`, PLATFORMS[side]); } const textures = new Map(); for (const [id, path] of [...mod.texture_files].sort(([a], [b]) => a.localeCompare(b))) { textures.set(id, Deno.readFileSync(path)); } return write_bmod({ manifest, data: { blocks: mod.blocks.map((b) => b.json), models: mod.models.map((m) => m.json), items: mod.items.map((i) => i.json), recipes: mod.recipes.map((r) => r.json), ores: mod.ores.map((o) => o.json), }, scripts, textures, credits: manifest.credits ? Deno.readTextFileSync(`${mod.dir}/${manifest.credits}`) : undefined, }); } // a mod is a deno project: with its own deno.json its scripts are bundled with it (its imports, npm packages and // so on), otherwise with the deno.json of the folder the build runs in async function bundle_script(mod_dir: string, entry: string, platform: "deno" | "browser"): Promise { const config = ["deno.json", "deno.jsonc"].map((name) => `${mod_dir}/${name}`).find(exists); if (!config) { const result = await Deno.bundle({ entrypoints: [entry], platform, write: false, minify: false }); if (!result.success || !result.outputFiles?.length) { throw new Error(`Couldn't bundle ${entry}:\n${result.errors.map((e) => e.text).join("\n")}`); } return result.outputFiles[0].text(); } const output = await Deno.makeTempFile({ suffix: ".js" }); try { const result = await new Deno.Command(Deno.execPath(), { args: ["bundle", "--config", config, "--platform", platform, "--output", output, entry], stdout: "piped", stderr: "piped", env: { NO_COLOR: "1" }, }).output(); if (!result.success) { throw new Error(`Couldn't bundle ${entry}:\n${new TextDecoder().decode(result.stderr).trim()}`); } return Deno.readTextFileSync(output); } finally { Deno.removeSync(output); } } function exists(path: string) { try { Deno.statSync(path); return true; } catch { return false; } } if (import.meta.main) { const [dir, out] = Deno.args; if (!dir) { console.error("usage: deno task pack-mod [output.bmod]"); Deno.exit(1); } // its siblings are checked too, so references to mods it depends on can be checked const folder = resolve(dir); const mod = load_and_check(dirname(folder)).find((m) => resolve(m.dir) === folder); if (!mod) { console.error(`${dir} isn't a mod folder`); Deno.exit(1); } if (mod.report.errors.length > 0) { console.error(`${mod.id} has errors (deno task check-mods for details):`); for (const error of mod.report.errors) console.error(` ${error}`); Deno.exit(1); } const file = out ?? `${basename(folder)}${BMOD_EXTENSION}`; const bytes = await pack_mod(mod); Deno.writeFileSync(file, bytes); console.log(`Packed ${mod.id} into ${file} (${(bytes.length / 1024).toFixed(1)} KB)`); }