npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bedrock-core/ui-runtime

v0.9.2

Published

@bedrock-core/ui runtime

Readme

@bedrock-core/ui-runtime

Logo

⚠️ Beta Status: Active development. Breaking changes may occur until 1.0.0. Pin exact versions for stability.

This is not ready for production use.

Core framework library for @bedrock-core/ui. This package contains the JSX runtime, serialization protocol, component system, and rendering logic that powers @bedrock-core/ui.

📦 Installation

This package is typically installed as a dependency when using @bedrock-core/ui:

yarn add @bedrock-core/ui

Or directly:

yarn add @bedrock-core/ui-runtime

🧱 Architecture Overview

| Layer | Responsibility | Key Files | |-------|----------------|-----------| | JSX Runtime | Transforms JSX to { type, props } elements | src/jsx/jsx-runtime.ts | | Component Functions | Pure functions returning JSX.Element objects | src/core/components/*.ts | | Serialization Protocol | UTF‑8 fixed-width, semicolon-padded segments | src/core/serializer.ts | | Runtime (Entry) | Orchestrates build → snapshot → show → response; owns effect loop | src/core/runtime.ts | | Renderer Adapter | Serializes UI tree to a form snapshot | src/core/rendererAdapter.ts | | Fiber Registry | Manages component instances and hook state | src/core/fiber.ts | | Context System | React-like context providers and consumers | src/core/context.ts | | Hooks System | State management and side effects | src/core/hooks/*.ts |

🔄 Component Routing System

The system routes components through the native form factory's typed slots — the engine itself dispatches each entry to the matching RP control, which is the cheapest routing available (no per-entry #type gating for the slot decision):

For Client-Only Components (Panel, Text, Image, Fragment):

  1. Panel/Text serialize via form.label()label_router; Image serializes via form.header() → the slim header_router (engine-level type routing; on the modal backend images fall back to the label slot)
  2. label_router decodes the cell's scroll-region field once, then mounts one merged label_cell: a single control-block decode plus small self-gating leaves — a background image (the common [440] field, which also serves panel cells and folded Panel+Text pairs) and per-type [1,1] label frames gated on #type
  3. Serializer cell elision: a background-less <Panel> emits nothing (its children are independent absolutely-positioned cells), and a Panel(background) wrapping a single <Text> folds into one text cell — both cut the engine's per-cell instantiation cost, the dominant cost on large screens
  4. Decode bindings carry binding_condition: gate chains are once (the payload is constant per screen instance) and everything downstream is visible, so a non-matching leaf costs only its ~7-binding gate and nothing re-evaluates per frame

For Native Form Components (Form and its Form.* fields — shipped in 0.9):

  • A <Form> on the tree switches the renderer from the all-buttons ActionForm backend to a native ModalFormData build (one atomic submit)
  • Field controls (Form.Toggle, Form.Slider, Form.Dropdown, Form.InlineSelect, Form.Input) map to the native modal control factory (toggle / slider / dropdown / input) instead of the label router
  • They still use the same serialization protocol, so the RP decoders share the field-slicing machinery; values arrive once, on submit, keyed by each control's name
  • Form.Button is NOT a native control — it consumes no formValues slot; its geometry + styling ride the form title payload (assembled post-layout) and wire to the engine's submit / close button ids

This "label-as-entry-point" system allows unlimited custom client components, while the modal backend leverages Minecraft's native form factory for interactive fields.

⚡ Performance Principles

JSON UI cost is dominated by how many controls the engine instantiates and how many bindings it evaluates at screen creation — not by payload string length (measured: +10KB per element made no difference) and not by re-evaluation once conditions are set. The architecture therefore follows these rules (established with in-game measurements):

  1. Fewer cells beats everything. Every emitted form entry instantiates a router subtree in every mounted scroll-pool slot. The serializer skips no-op cells (background-less panels) and folds Panel+Text pairs before anything reaches the engine.
  2. One decode per cell. The merged label_cell runs the control-block decode once; type-specific visuals are cheap self-gating leaves, not sibling full-decode variants.
  3. binding_condition on everything. Gate chains (#type, region) are once — the payload is constant for the lifetime of a screen instance. All other decode chains are visible, so gated-off leaves skip their work. Only genuinely live channels (toggle state, dropdown open state/selection, typed text) stay unconditioned. Never condition a binding that a hidden control needs to compute its own visibility.
  4. Instantiation is parse-time. visible: false does NOT prevent instantiation and #collection_length can NOT be view-computed (both probe-verified) — the only real levers are emitting fewer entries and mounting fewer pool slots. That is why the scroll pool is capped at 2 pooled scrolls (MAX_POOLED_SCROLLS); growing it means re-adding the RP slot + router variants and accepting the per-slot cost.

🧩 Component Pattern

Components are pure functions that return JSX.Element objects (using the custom JSX runtime):

import { FunctionComponent, JSX } from '@bedrock-core/ui';
import { ControlProps, withControl } from './control';

export interface PanelProps extends ControlProps {
  // Component-specific props go here
}

export const Panel: FunctionComponent<PanelProps> = ({ children, ...rest }: PanelProps): JSX.Element => ({
  type: 'panel',
  props: {
    ...withControl(rest),  // Must be first - applies control props in canonical order
    // Component-specific props with defaults go here after withControl
    children,              // Children always last
  },
});

Conventions:

  • Dimension and position props use flexbox style values (texels or percent strings like "50%"); the layout engine resolves these to absolute Pocket-space texels before serialization
  • Props order is critical: withControl(rest) must always be first in the props object, followed by component-specific props with default values, then children last
  • Component prop names are camelCase; JSON UI bindings use snake_case
  • Use the custom JSX runtime - no need to import React
  • All "optional" props must have defined defaults - no undefined/null values in serialized output
  • The serialize() function in core/serializer.ts handles the encoding automatically
  • The withControl() helper applies standard control properties in canonical order

🔐 Serialization Protocol

Defined in src/core/serializer.ts.

Payload always starts with a 9-character header: bcui + vXXXX (e.g., bcuiv0005). Decoders must skip these first 9 chars before field slicing.

Each field is composed of three conceptual parts concatenated in this order:

  1. Type prefix (2 bytes)
  2. Value (padded with semicolons ; until defined byte length)
  3. Unique 1‑byte field marker (disambiguates otherwise identical full regions during JSON UI subtraction)

Field Widths (bytes)

| Type | Prefix | Prefix Width | Type Width | Marker Width | Full Width | Notes | |----------|--------|--------------|------------|--------------|------------|-------| | String | s: | 2 | 80 | 1 | 83 | Prefer to use translation keys when text larger than 32 characters is needed | | Number | n: | 2 | 80 | 1 | 83 | Expanded to match string width in v0003; unified field sizing since v0004 | | Boolean | b: | 2 | 5 | 1 | 8 | Serialized as 'true' or 'false' | | Reserved | r: | 0 | variable | 0 | variable | No prefix/marker for easier JSON UI skipping |

Markers

Markers come from a stable ordered alphabet (0-9A-Za-z-_), limiting components to 64 props max. Index position = field order. Never reorder existing markers (backward decode offsets rely on stable sequence).

Encoding Example

import { serializeProps } from '@bedrock-core/ui-runtime';

const [encoded, bytes] = serializeProps({
  type: 'example',      // string → 83 bytes
  message: 'hello',     // string → 83 bytes
  count: 123,           // number → 83 bytes
  ratio: 45.67,         // number → 83 bytes
  ok: true,             // bool → 8 bytes
});
// Total: 83 + 83 + 83 + 83 + 8 = 340 bytes (plus 9-byte header = 349 bytes)

Field Binding Template Pattern (Decoding)

Decoding inside the resource pack uses a progressive "slice → subtract" strategy. Each field follows a 3‑step lifecycle:

extract_raw → update_remainder → extract_value

Generic template (JSON UI binding entries) — copy & replace placeholders:

{
    "binding_type": "view", // full_width
    "source_property_name": "('%.{FULL_WIDTH}s' * #rem_after_{PREV})",
    "target_property_name": "#raw_{FIELD_NAME}"
},
{
    "binding_type": "view",
    "source_property_name": "(#rem_after_{PREV} - #raw_{FIELD_NAME})",
    "target_property_name": "#rem_after_{FIELD_NAME}"
},
{
    "binding_type": "view", // (full_width - marker_width) - prefix_width - padding_char (;)
    "source_property_name": "(('%.{FM_WIDTH}s' * #raw_{FIELD_NAME}) - ('%.2s' * #raw_{FIELD_NAME}) - ';')",
    "target_property_name": "#{FIELD_NAME}"
}

Placeholder reference:

  • {FIELD_NAME} - unique identifier (e.g. type, visible)
  • {PREV} - previous remainder token (first field uses header, others use previous field name)
  • {FULL_WIDTH} - from table full_width column
  • {FM_WIDTH} - table (full_width - marker_width)

For reserved blocks: Simply skip by subtracting the fixed byte count from remainder, no extraction needed.

Base Control Properties Deserialization Order

All components inherit these base control properties, which are deserialized in this exact order after the 9-byte protocol header (bcuiv0005):

Field 0: type (string, 83 bytes)       - component type identifier
Field 1: width (number, 83 bytes)      - element width
Field 2: height (number, 83 bytes)     - element height
Field 3: x (number, 83 bytes)          - horizontal position
Field 4: y (number, 83 bytes)          - vertical position
Field 5: visible (bool, 8 bytes)       - visibility state (default: true)
Field 6: enabled (bool, 8 bytes)       - interaction enabled (default: true)
Field 7: background (string, 83 bytes) - background texture path
Field 8: $reserved (501 bytes)         - reserved for future expansion

Total control block: 1024 bytes (9 + 83 + (5×83) + (2×8) + 83 + 501)

Component-specific properties are appended after the reserved block.

Title Metadata (Screen Selection)

Two distinct payloads share the protocol. Each form button label carries a component's control block (above), while the form title (#title_text) carries screen-level metadata. The title is produced by serializeTitleMetadata() and selects which Resource Pack screen layout to mount:

| Field | Type | Bytes | Purpose | |--------|--------|-------|---------| | header | — | 9 | bcuiv0005 protocol header | | 0 | string | 83 | screen type: scroll (extensible — more types may be added) | | 1 | number | 83 | root content height (used to size the scroll container) |

Total: 175 bytes. The screen type comes first because every screen reads it to route; the height comes second because only the scrolling layouts need it.

Deserialization (core-ui/common/screen_container.json):

  1. Bind #title_text, strip the 9-byte header → #rem_after_header_sc.
  2. Slice the 83-byte screen-type field and extract its value (drop the s: prefix + ; padding) → #screen_type.
  3. Route by comparison on #screen_type (currently only scroll; the container mounts core_ui_screens.scroll when #screen_type = 'scroll'). The routing stays so new screen types can be added without protocol changes.

Scrolling layouts (core_ui_screens.scroll) additionally skip the screen-type field to reach the height field, then bind it to #size_binding_y. No protocol guard is needed inside screen_containerserver_form.json only mounts it for forms whose title carries the header (it collapses the container to 0px otherwise).

Decoding Rules & Tips

  • Always slice FULL field (value + prefix + marker) first, then subtract to create the remainder
  • Strip padding only after isolating the core full segment (second slice) so you don't accidentally remove semicolons in later fields
  • Never assume a marker character appears only once globally—its uniqueness is only relative to its position
  • Protocol extension rule: append new fields (new markers) at the end; never reorder or shrink earlier core lengths
  • Reserved blocks are skipped entirely in deserialization—they create "gaps" in the payload that the JSON UI decoder jumps over

🪝 Hooks System

Hooks follow React-like patterns but adapted for Minecraft server environment:

  • useState(initial) – State hook with setter function
  • useEffect(fn, deps) – Effect hook with dependency array
  • useRef(initial) – Mutable ref container
  • useContext(context) – Access context value from Provider
  • useReducer(reducer, initial) – Reducer hook for complex state
  • usePlayer() – Current player from render context
  • useEvent(eventKey) – Listen to global events
  • useExit() – Cleanup callback when form closes

Rules:

  • Hooks must be called in consistent order (no conditional hook calls)
  • Hooks are stored per-component-instance in FiberRegistry
  • useEffect cleanup functions run on instance deletion

🧪 Testing

This package uses Vitest with mocked Minecraft APIs:

yarn test              # Run tests
yarn coverage          # Generate coverage report

Mocks are located in src/__mocks__/@minecraft/.

⚠️ Known Caveats

  • JSON UI string ops with numbers can behave unpredictably; prefix markers before numeric-derived substrings client-side.
  • Texel values & JSON UI: Dimension and position values are serialized as raw integer texels (Pocket-space). JSON UI ignores numbers with decimal points, so the layout engine rounds all values to integers before serialization.
  • Subtraction operator (-) removes all occurrences; use distinct prefixes to avoid collisions.

⚠️ Breaking Change Guards

  • Never modify TYPE_WIDTH, PAD_CHAR, or canonical field order
  • Never change the 9-char header format (bcui + version)
  • Always append new fields to end; use reserveBytes() for future space
  • Always increment VERSION when making protocol-breaking changes (with migration docs)
  • Test rigorously – serialization format is frozen once clients decode it

📖 Reference Documentation

For JSON UI decoding implementation details: