Files
bworld/tools/pack_mod.ts
T
2026-09-26 17:33:07 -03:00

102 lines
3.9 KiB
TypeScript

// 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<ScriptSide, "deno" | "browser"> = { server: "deno", client: "browser", worldgen: "browser" };
// a mod that passed check-mods, as .bmod bytes
export async function pack_mod(mod: LoadedMod): Promise<Uint8Array<ArrayBuffer>> {
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<string, Uint8Array>();
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<string> {
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 <mod folder> [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)`);
}