1. Project Overview
Jepia is a TypeScript framework for programmatic Minecraft Bedrock Add-On generation using Bun. It provides a fluent, object-oriented API for creating behavior packs, resource packs, and their components.
| Property | Value |
|---|---|
| Language | TypeScript (Bun runtime) |
| Target | Minecraft Bedrock Edition |
| Format Version | 2 (stable) |
| Validation | AJV + Zod |
| Output | behavior_pack/ + resource_pack/ folders, .mcaddon / .mcpack |
The core idea: describe your add-on in TypeScript, let Jepia handle manifest generation, file layout, schema validation, and packaging.
import { Addon } from 'jepia';
const addon = new Addon('TechMod', 'Industrial automation add-on');
// addon.behaviorPack and addon.resourcePack are auto-created
addon.addItem(myItem);
addon.addBlock(myBlock);
await addon.generate('./output');
2. Architecture Diagram
The layered architecture separates concerns into entry point, packs, components, scripting, and utilities.
┌──────────────────────────────────────────────────────────────┐
│ Addon │
│ (entry point, orchestrator) │
└────────────┬───────────────────────┬──────────┬─────────────┘
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌─────────────────────┐
│ BehaviorPack │ │ ResourcePack │ │ ScriptPipeline │
│ │ │ │ │ │
│ - Items │ │ - Textures │ │ - Script Blocks │
│ - Blocks │ │ - Models │ │ - File Refs │
│ - Entities │ │ - Materials │ │ - Custom Comps │
│ - Recipes │ │ - RenderCtrl │ │ - Custom Commands │
│ - Loot Tables │ │ - TextureSets │ │ - Constants │
│ - Script Module │ │ - TextureAtlas │ │ - Bun.build │
└──────────────────┘ └──────────────────┘ └─────────────────────┘
│ │ │
└───────────────────────┴──────────┬─────────┘
▼
┌────────────────────────────────────────────────────────────────┐
│ Components │
│ │
│ Item Block Entity Texture Model Material │
│ RenderController TextureLayer TextureSet │
│ TextureAtlas ComponentRegistry DependencyResolver │
└────────────────────────────┬───────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────┐
│ Utilities │
│ │
│ UUID Molang Validation (AJV) AtlasPacker │
│ FileSystem Zod Config Archiver (ZIP) │
└────────────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────┐
│ Output │
│ │
│ behavior_pack/ resource_pack/ │
│ ├── manifest.json ├── manifest.json │
│ ├── scripts/main.js ├── textures/ │
│ ├── items/ ├── materials/ │
│ ├── blocks/ └── render_controllers/ │
│ └── entities/ .mcaddon / .mcpack │
│ jepia.config.json │
└────────────────────────────────────────────────────────────────┘
3. Component System
All game objects derive from an abstract Component base class that enforces a consistent interface.
Base Component
| Member | Type | Purpose |
|---|---|---|
id | string | Namespaced identifier (e.g. myaddon:iron_sword) |
displayName | string | Human-readable name |
description | string | Optional description |
validate() | boolean | AJV schema validation |
toJSON() | object | Bedrock-format JSON output |
Component Hierarchy
Component (abstract)
├── Item — behavior pack items
├── Block — behavior pack blocks
├── Entity — behavior + resource pack entities
├── Texture — resource pack texture references
├── TextureLayer — individual images with PBR roles
├── TextureSet — groups layers into .texture_set.json
├── Material — .material files with inheritance
├── RenderController — wires geometry + textures + materials
├── Model — geometry definitions
├── TextureAtlas — sprite sheet packing
├── Recipe — crafting, furnace, brewing recipes
├── LootTable — loot pools and item drops
├── SpawnRule — natural entity spawning conditions
└── TradeTable — merchant tier trades
ComponentRegistry
Centralized registration of all components within a pack, enabling lookup by ID and type-safe retrieval.
// Components register themselves when added to a pack
addon.addItem(sword); // registers in BehaviorPack registry
addon.addBlock(ore); // registers in BehaviorPack registry
DependencyResolver
Resolves cross-component references at build time. For example, an Entity references a RenderController, which in turn references a Material and Texture. The resolver ensures all referenced components exist before output.
4. Pack System
Both pack types extend a shared Pack base class that handles manifest generation.
| Base Pack Member | Purpose |
|---|---|
name | Pack display name |
description | Pack description |
uuid / moduleUUID | Persisted identifiers |
version | Semantic version array [major, minor, patch] |
generateManifest() | Outputs manifest.json per Bedrock spec |
BehaviorPack
Contains server-side game logic.
| Content Type | Output Folder |
|---|---|
| Items | items/ |
| Blocks | blocks/ |
| Entities | entities/ |
| Recipes | recipes/ |
| Loot Tables | loot_tables/ |
| Spawn Rules | spawn_rules/ |
| Trade Tables | trading/ |
ResourcePack
Contains client-side visuals and rendering definitions.
| Content Type | Output Folder |
|---|---|
| Textures | textures/ |
| Models / Geometry | models/ |
| Materials | materials/ |
| Render Controllers | render_controllers/ |
| Client Entity Defs | entity/ |
| Texture Sets (PBR) | textures/ (alongside base textures) |
5. Rendering Pipeline
The rendering pipeline was built across Phases 1 through 5 and maps directly to Minecraft Bedrock's deferred rendering system.
TextureLayer ──▶ TextureSet ──▶ Material ──▶ RenderController ──▶ TextureAtlas
(images) (PBR groups) (.material) (Molang wiring) (sprite sheets)
Phase 1: TextureLayer
Represents a single image file with a designated PBR role.
| Role | Description |
|---|---|
color | Base color / albedo (RGB) |
normal | Normal map for surface detail |
mer | Metalness / Emissive / Roughness packed channels |
heightmap | Height data for parallax |
const colorLayer = new TextureLayer('iron_color', {
role: 'color',
path: 'textures/blocks/iron_block',
resolution: { width: 16, height: 16 }
});
Phase 2: TextureSet
Groups multiple TextureLayers into a .texture_set.json file for Bedrock's PBR pipeline.
const textureSet = new TextureSet('iron_pbr');
textureSet
.setColorLayer(colorLayer)
.setNormalLayer(normalLayer)
.setMERLayer(merLayer);
Phase 3: Material
Defines .material files with vanilla inheritance, shader defines, blend states, and render states.
const mat = new Material('custom:glowing_ore');
mat
.setParent('entity_alphatest')
.addDefine('FANCY')
.setBlendSrc('SourceAlpha')
.setBlendDst('OneMinusSrcAlpha')
.setState('DisableCulling', true);
Materials support the full inheritance chain: a child material inherits all defines, states, and blend factors from its parent, then overrides selectively.
Phase 4: RenderController
Wires geometry, textures, and materials together using Molang expressions. Outputs .render_controllers.json files.
const rc = new RenderController('controller.render.my_entity');
rc
.setGeometry("Geometry.default")
.addTexture("Texture.default")
.addMaterial("Material.default", "*")
.setPartVisibility("head", "query.is_baby");
Phase 5: TextureAtlas
Packs multiple textures into optimized sprite sheets using two strategies.
| Strategy | Best For | Algorithm |
|---|---|---|
shelf | Uniform-size textures | Row-based packing with shelf height tracking |
maxrects | Mixed-size textures | Maximal rectangles with best-short-side-fit heuristic |
const atlas = new TextureAtlas('item_atlas', {
strategy: 'maxrects',
maxWidth: 2048,
maxHeight: 2048,
padding: 1
});
atlas.addSprite('sword', 16, 16);
atlas.addSprite('shield', 32, 32);
const result = atlas.pack(); // { placements, width, height }
5b. Scripting Pipeline
The scripting pipeline bridges TypeScript source authored at build time with JavaScript that runs inside Minecraft Bedrock at runtime. It is orchestrated by ScriptPipeline (src/script/pipeline.ts) and runs as part of addon.generate().
addon.script(callback) → ScriptPipeline.addBlock()
addon.scriptFile(path) → ScriptPipeline.addFile()
block.addCustomComponent(...) → ScriptPipeline.addRawSource()
addon.addCustomCommand(...) → ScriptPipeline.addRawSource()
constants injection → ScriptPipeline.addRawSource()
│
▼
ScriptPipeline.process()
│
┌───────┴──────────┐
│ │
▼ ▼
Extractor.ts FileBundleOptions
(inline blocks) (file-based)
│ │
▼ ▼
assembleScriptFile() read files
│ │
└────────┬──────────┘
▼
Bun.build()
@minecraft/* → external
│
▼
behavior_pack/scripts/main.js
Extraction
Inline script blocks use Function.prototype.toString() to obtain the callback's source text at build time. The extractor (src/script/extractor.ts) then:
- Strips the outer function wrapper to isolate the body.
- Detects
ctx.server,ctx.serverUi, etc. with word-boundary matching. - Extracts destructured imports (
const { world } = ctx.server) and generates proper ESimportstatements. - Rewrites inline
ctx.server.worldreferences into direct variable references. - Merges multiple blocks into a single ES module with deduplicated imports.
// Input (callback at build time):
addon.script((ctx) => {
const { world } = ctx.server;
world.afterEvents.playerJoin.subscribe((event) => {
world.sendMessage(event.playerName + ' joined!');
});
});
// Extracted and rewritten:
import { world } from '@minecraft/server';
world.afterEvents.playerJoin.subscribe((event) => {
world.sendMessage(event.playerName + ' joined!');
});
Bundling
The bundler (src/script/bundler.ts) uses Bun.build() with the following configuration:
| Option | Value | Reason |
|---|---|---|
target | "browser" | Closest to Minecraft's QuickJS runtime |
format | "esm" | Bedrock requires ES modules |
external | ["@minecraft/*"] | Provided by the game engine at runtime — must not be bundled |
minify | configurable | Controlled via addon.setMinify() or ScriptPipeline |
The assembled source is written to a temporary .ts file, passed to Bun.build(), the output is read, and the temp file is cleaned up. The final JS is written to behavior_pack/scripts/main.js.
Manifest Integration
After the pipeline processes scripts, BehaviorPack.addPackSpecificModules() adds two things to the manifest:
- A
"type": "script"module with"language": "javascript"and"entry": "scripts/main.js". - A
dependenciesarray entry for each detected@minecraft/*module with its semver string version (e.g."1.17.0").
Script module dependencies use semver strings (not version tuples) — this is the correct format per the official Bedrock pack manifest specification. The script module UUID is persisted to jepia.config.json so Minecraft recognizes the same pack across rebuilds.
Execution Model
Per the Minecraft Script API documentation, scripts have three execution contexts with different privilege levels:
| Context | When | Privilege |
|---|---|---|
| Early (startup) | system.beforeEvents.startup.subscribe() | Registration only — blocks, items, commands. No world writes. |
| Restricted (beforeEvents) | Before-event handlers | Read-only. Use system.run() to defer writes. |
| Default | After-events, system.run(), system.runInterval() | Full API access. |
Jepia respects these contexts automatically:
- Custom component and custom command registrations are always placed inside
system.beforeEvents.startup.subscribe()in the generated code. - Inline
addon.script()blocks run at the default (module-level) context. Usesystem.run()inside event handlers when you need to modify world state from a before-event.
Source File Layout
src/script/
├── types.ts — ScriptModuleId, ScriptOptions, ScriptBlock, ScriptDependency,
│ CustomCommandConfig, CustomCommandRegistration, constants
├── extractor.ts — Function.toString() extraction, ctx rewriting, import generation
├── bundler.ts — Bun.build() wrapper, @minecraft/* externals, file bundling
├── pipeline.ts — Orchestrator: collect → extract → merge → bundle → output
├── custom-components.ts — Startup registration code generation for custom components
├── custom-commands.ts — Startup registration code generation for custom commands
├── constants-generator.ts — ITEMS / BLOCKS / ENTITIES export generation
└── index.ts — Public exports
5c. Visual & Client Systems
Phase 9 adds five resource-pack component types covering Minecraft's client-side visual and audio pipeline. Unlike the behavior-pack components from Phases 7–8, all five write exclusively to the resource pack.
Component Overview
| Component | Output path | format_version |
|---|---|---|
Particle | resource_pack/particles/<id>.json | 1.10.0 |
Animation | resource_pack/animations/<id>.json | 1.8.0 |
AnimationController | resource_pack/animation_controllers/<id>.json | 1.10.0 |
Attachable | resource_pack/attachables/<id>.json | 1.8.0 |
ClientBiome | resource_pack/biomes_client/<id>.json | 1.0.0 |
Identifier Derivation
Animation and controller identifiers are derived automatically from the component id to match Minecraft's naming conventions:
Component id → JSON dict key
"myaddon:walk" → "animation.myaddon.walk" (Animation)
"myaddon:base" → "controller.animation.myaddon.base" (AnimationController)
Both can be overridden with setAnimationId() / setControllerId().
Source File Layout
src/component/
├── particle.ts — Particle (format_version 1.10.0; emitter rate/lifetime/shape, billboard, tinting, curves)
├── animation.ts — Animation (format_version 1.8.0; bone keyframes, timeline events)
├── animation-controller.ts — AnimationController (format_version 1.10.0; state machine, Molang transitions)
├── attachable.ts — Attachable (format_version 1.8.0; materials, textures, geometry, scripts)
└── client-biome.ts — ClientBiome (format_version 1.0.0; water/sky/fog/grass/foliage colors)
ResourcePack Storage
Particles and animations were already stored in ResourcePack maps. Phase 9 adds three new maps:
animationControllers: Map<string, object> // addAnimationController / getAnimationControllers
attachables: Map<string, object> // addAttachable / getAttachables
clientBiomes: Map<string, object> // addClientBiome / getClientBiomes
All three are written to disk in addon.generate() after the existing particle write step, using the standard writeComponent() helper.
6. File Output Structure
When addon.generate() is called, the following directory tree is produced.
my_addon/
├── behavior_pack/
│ ├── manifest.json
│ ├── items/
│ │ └── *.json
│ ├── blocks/
│ │ └── *.json
│ ├── entities/
│ │ └── *.json
│ ├── recipes/
│ │ └── *.json
│ ├── loot_tables/
│ │ └── *.json
│ ├── spawn_rules/
│ │ └── *.json
│ └── trading/
│ └── *.json
├── resource_pack/
│ ├── manifest.json
│ ├── textures/
│ │ ├── *.png
│ │ └── *.texture_set.json
│ ├── models/
│ │ └── *.geo.json
│ ├── materials/
│ │ └── *.material
│ ├── render_controllers/
│ │ └── *.render_controllers.json
│ └── entity/
│ └── *.entity.json
└── jepia.config.json
Each component's toJSON() method produces the exact JSON structure expected by Minecraft Bedrock. Jepia writes files to the correct subfolder automatically based on component type.
7. Validation Strategy
Validation runs at three levels, catching errors before files are written.
| Level | Tool | What It Checks |
|---|---|---|
| Component | AJV | Each component's JSON output against its Bedrock JSON schema |
| Pack | AJV | Manifest structure, required fields, version format |
| Addon | Custom | Cross-pack consistency (e.g. entity in BP has matching client def in RP) |
Bundled Schemas
| Schema | Validates |
|---|---|
item | Item component JSON |
block | Block component JSON |
entity | Entity behavior definition |
material | .material file structure |
render_controller | Render controller JSON |
texture_set | PBR texture set JSON |
// Validation happens automatically during generate()
// or can be triggered manually:
const errors = sword.validate();
if (errors.length > 0) {
console.error('Validation failed:', errors);
}
8. Configuration System
Jepia uses a jepia.config.json (or .ts) file validated with Zod schemas.
| Option | Type | Purpose |
|---|---|---|
output | string | Output directory path |
packagingFormat | "mcaddon" | "mcpack" | Archive format for distribution |
validation | boolean | Enable/disable schema validation |
minify | boolean | Minify JSON output |
uuid | object | Persisted UUIDs for stable pack identity across rebuilds |
{
"output": "./build",
"packagingFormat": "mcaddon",
"validation": true,
"minify": false,
"uuid": {
"behaviorPack": "a1b2c3d4-...",
"behaviorModule": "e5f6a7b8-...",
"resourcePack": "c9d0e1f2-...",
"resourceModule": "a3b4c5d6-..."
}
}
UUIDs are generated on first build and persisted to jepia.config.json so that Minecraft recognizes the same pack across updates rather than treating each build as a new add-on.
9. Packaging
Jepia uses Archiver to create distributable ZIP archives in Minecraft's expected formats.
| Format | Extension | Contents |
|---|---|---|
| Combined | .mcaddon | Both behavior pack and resource pack in one archive |
| Individual | .mcpack | A single pack (behavior or resource) per archive |
// Package after generation
await addon.generate('./output');
// produces: output/MyAddon.mcaddon
// or: output/MyAddon_BP.mcpack + output/MyAddon_RP.mcpack
10. Design Decisions
| Decision | Rationale |
|---|---|
| Auto-create packs on Addon instantiation | Every add-on needs at least one pack. Eliminates boilerplate and prevents forgetting to create a pack. |
| Fluent API with method chaining | Reduces verbosity. Component configuration reads like a declarative spec rather than imperative setup code. |
| Schema-driven validation with AJV | Catches errors at build time rather than in-game. Schemas stay close to Mojang's official format definitions. |
| Bun runtime | Native TypeScript execution without a compile step. Fast file I/O and built-in test runner. |
| UUID persistence | Minecraft identifies packs by UUID. Changing UUIDs between builds causes duplicate pack entries and breaks world references. |
| Component base class | Enforces a uniform contract (validate(), toJSON()) across all content types, making the system extensible. |
| Layered rendering pipeline | Mirrors Bedrock's own rendering stack. Each layer (texture → texture set → material → render controller) maps 1:1 to a Bedrock concept. |
Function.prototype.toString() for script extraction |
Allows users to write Script API code in the same TypeScript file as JSON components, with IDE type-checking. The extractor handles the build/runtime boundary transparently. |
Bun.build() as the script bundler |
Zero extra dependencies — Bun is already the runtime. Provides native TypeScript compilation, external support for @minecraft/*, and fast I/O. |
| Custom Components V2 auto-registration | Eliminates the need for users to manually write startup registration code. Jepia generates the correct system.beforeEvents.startup.subscribe() block from handler functions automatically. |
// The fluent API in action:
const block = new Block('Reactor Core', 'techmod:reactor_core')
.setCategory('construction')
.addComponent('minecraft:destructible_by_mining', {
seconds_to_destroy: 5.0
})
.addComponent('minecraft:light_emission', 14)
.addComponent('minecraft:map_color', '#00ff88');
// Each method returns `this`, enabling chaining.