Browse docsAPI and commands

MicroTS reference

MicroTS compiles Vue templates or Solid TSX and TypeScript contracts into Rust. This page covers the Vue form. See Solid TSX to Rust for the Solid source subset. Use Getting started for the build workflow and Components for props, events, models, slots, instance state, generics and shared context. For the connection between template bindings and generated Rust methods, read How Vue becomes Rust. The TypeScript support reference defines the source subsets. The model guide explains execution of compiled model bodies, and TypeScript and native code defines the model and host contracts.

The supported source language is TypeScript. JavaScript mentioned in runtime or formatting rules is generated output or engine behavior. It does not imply an AOT support contract for .js models or untyped view scripts.

Files and setup

An AOT component has one <template> and one <script setup lang="ts">. Its view-model import uses the component's basename without an extension: Dial.vue imports from ./Dial. Other script blocks, <style> blocks and custom SFC blocks are rejected.

File What you write
Dial.vue Template, imports and component declarations
Dial.ts TypeScript state and functions; compiled mode translates their bodies, while Rust mode uses their types as the native contract
Dial.d.ts Contract declarations for Rust mode; use this instead of Dial.ts
Rust source Connect the app to a native host; in Rust mode, also implement the generated view-model trait

A component cannot have both basename module forms. A .d.ts browser preview supplies default values, such as zero, empty strings and empty arrays; it does not execute the Rust application logic.

app.model defaults to "rust". Set app.aot: true and app.model: "compiled" to compile the .ts model. This selection applies to the root model and child factories. Unsupported compiled model source is an error; model selection does not change after admission fails.

<script setup> accepts these declarations:

Form Example
PocketJS host imports import { View, Text } from "@pocketjs/framework/vue-vapor/components"
Child imports import Row from "./Row.vue"
View-model imports import { count, increment } from "./Dial"
Standard functions and types import { len, type i32 } from "@pocketjs/framework/vue-vapor/std"
Types import type { Item } from "./types", interface, type
Component macros defineProps, withDefaults, defineEmits, defineModel, defineSlots
Instance state const { count, increment } = createRow() from the basename module
Shared context provide and inject, imported from vue

Put ref, computed, functions and other application logic in the .ts view-model module. Import ref and computed from vue. In compiled models, import watch and watchEffect from @pocketjs/framework/vue-vapor/reactive. Setup does not accept local runtime variables or statements beyond the factory, macros, context and PocketJS lifecycle forms.

Host elements and input

Import these elements from @pocketjs/framework/vue-vapor/components.

Element Accepted attributes Event
View class, :class, :style, bare focusable, static debug-name, :ref to a model node slot @press requires focusable
Text class, :class —
Image class, :class, static src asset name —
ActionHandler Static :button="BTN.NAME", boolean active, static latched @press
AxisHandler Static axis="primary" or "secondary", boolean active @delta
MotionHandler Static value motion value, static minQuality, boolean active @update

Put text and interpolation inside Text. Image has no children. Register image names with the Rust host's Ui::register_image before mounting views that use them; the host loads font atlases.

For example, a dial exposes a count, a reset action and incremental motion:

<!-- Dial.vue -->
<script setup lang="ts">
import { ActionHandler, AxisHandler, Text, View }
  from "@pocketjs/framework/vue-vapor/components";
import { BTN } from "@pocketjs/framework/vue-vapor/input";
import { count, resetCount, adjustCount } from "./Dial";
</script>

<template>
  <View class="flex-col gap-2 p-4">
    <ActionHandler :button="BTN.CROSS" :active="count !== 0"
      latched @press="resetCount()" />
    <AxisHandler axis="primary" @delta="adjustCount($event)" />
    <Text>{{ count }}</Text>
    <View focusable @press="resetCount()"><Text>Reset</Text></View>
  </View>
</template>
// Dial.d.ts — implement these methods in the Rust view model.
import type { i32 } from "@pocketjs/framework/vue-vapor/std";
export declare const count: i32;
export declare function resetCount(): void;
export declare function adjustCount(delta: i32): void;

Axis events carry signed i32 millidegrees: 1000 means one degree. The app chooses sensitivity and retains any remainder between steps. An AxisHandler consumes the hardware-neutral relative-axis channel; device adapters translate physical motion into this channel.

Each axis handler receives one nonzero accumulated delta per frame. Deltas for an axis sum with i32 saturation. active gates delivery. A latched button handler waits for a release before accepting a press. Action and axis handlers run in document order.

Rust hosts provide motion through Input::default().with_axis(0, delta); axis 0 is primary and axis 1 is secondary. A host needs the generated app's HasButton<MASK> and HasRelativeAxis<ID> implementations for the inputs the template uses.

Motion state

A MotionHandler receives fused device attitude, never sensor readings. The host's native motion driver owns sampling, calibration and fusion, and publishes one MotionState per estimate. Vectors use the W3C DeviceMotion device frame: +x toward the right edge, +y toward the top edge, +z out of the screen. contracts/spec/motion.ts defines the state.

value Payload after the components Components Driver level
gravityDirection quality, timestamp unit vector toward the ground: x, y, z gravity
inclination quality, timestamp degrees between the screen normal and up: 0 face up, 90 upright gravity
linearAcceleration quality, timestamp m/s² with gravity removed: x, y, z inertial
rotationRate quality, timestamp bias-corrected degrees per second: x, y, z inertial
orientation quality, referenceFrame, epoch, timestamp unit quaternion w, x, y, z from the device frame to the reference frame inertial
heading quality, referenceFrame, timestamp degrees clockwise from north, accuracy in degrees geomagnetic
screenRotation quality, timestamp clockwise degrees that keep content upright gravity
tilt quality, timestamp W3C beta, gamma degrees gravity
angles quality, referenceFrame, epoch, timestamp W3C alpha, beta, gamma degrees inertial

Components are f32; quality and referenceFrame are u8, epoch is u32 and timestamp is u64 microseconds on the driver's clock. A handler declares the leading parameters it uses. screenRotation, tilt and angles are conveniences the driver derives from gravityDirection and orientation and publishes with its own quality; angles carries the orientation's referenceFrame and epoch. The runtime passes every value through unchanged.

Quality gates delivery. MotionQuality is unavailable, unreliable, low, medium or high; the handler fires on each frame whose state carries the value at minQuality or better, default low. A driver marks screenRotation unreliable while the screen lies near horizontal, where the upright direction is undefined. referenceFrame is local for an inertial driver, whose horizontal origin is arbitrary, and magneticNorth or trueNorth for a geomagnetic one. epoch increments whenever the driver re-establishes its reference frame.

<MotionHandler value="screenRotation" @update="upright" />
<MotionHandler value="rotationRate" minQuality="high" @update="spin" />

As in Vue, a method handler receives the payload values its parameters name (spin(x, y, z) above), and an inline statement reads only the first value, as $event.

Rust hosts pass the driver's estimate with Input::default().with_motion(state), where state is a microts::MotionState. A driver at the gravity level supplies gravityDirection and inclination; the inertial level adds linear acceleration, rotation rate and local orientation; the geomagnetic level adds north-referenced orientation and heading. The generated app requires HasMotion<LEVEL> for each level its values need, with levels from spec::motion::level.

Template lookup

Feature Accepted form and requirement
Conditional branches v-if, v-else-if, v-else; conditions must be boolean
Visibility v-show="visible" on a host element keeps its subtree mounted
Text {{ value }} or <Text v-text="value" />; v-text allows no children
Lists v-for="item in items" or v-for="(item, index) in items"; items must be an array
Keys Every v-for needs :key; use i32, i64, string or a string-literal enum
Child props :title="title"; expression types must match the child's declarations
Child models v-model="value", v-model:name="value"
Slots <slot />, named outlets and typed scoped slots; see Components

List keys must be unique and stable for each item. A keyed child keeps its instance state when its row moves. An unmounted child loses that state. The loop index has type i32.

A handler accepts save(), save(id), save($event), count = value, count += 1, count -= 1, count++, count--, or emit('saved', id). Assignments target view-model values or defineModel bindings. Statement sequences and if branches combine these operations: @press="if (count < 10) count++; resetAxis();". An emission ends the handler or a branch of its final if; loops, local variables and early returns are rejected. Arguments are evaluated once per statement. Owned payloads used by several calls are cloned before the last use.

Roots and children with a model factory can register onMounted and onUnmounted from @pocketjs/framework/vue-vapor/lifecycle. Each hook calls a zero-argument model method. Cleanup runs in reverse creation order, then mount hooks run in creation order. Hook writes trigger another update before the frame renders.

Model node references use createNodeRef from @pocketjs/framework/animation in the basename module and <View :ref="target" /> in the template. The reference binds to a native UI node and is cleared on unmount. It supplies an animation target; it does not expose DOM methods or a device SDK handle.

HTML elements, DOM events, directive modifiers, v-html, v-once, v-memo, object v-bind, dynamic event names, arbitrary Vue template refs, dynamic components, Teleport, Transition, KeepAlive and Suspense are outside the accepted template language. Use transition-* classes for style transitions.

Classes and styles

Static class values use the PocketJS styling classes. Dynamic classes select complete class strings with a ternary. Nested ternaries are accepted; class objects, arrays and string construction are not. A prop typed StyleClass, imported from @pocketjs/framework/vue-vapor/std, forwards a compiled style ID through :class="props.tone". Pass a class literal, a ternary of class literals, or an unchanged StyleClass prop. A static class cannot accompany a style-prop binding.

<View class="p-4"
  :class="selected ? 'bg-blue-600 text-white' : 'bg-slate-100 text-slate-900'"
  :style="{ width: 80 + count * 12, opacity: 0.8 }" />

:style belongs on View and takes an object literal with fixed property names. Use PocketJS names such as width, paddingT, bgColor and rotate. Values must match the property's numeric type or unit below.

Types and Rust methods

The shared TypeScript support reference contains the type-to-Rust table, rejected data shapes, tuple restrictions, generic component rules and capacity semantics. The same data mapping is used by Solid and Vue contracts.

This section describes how a view's use of a binding determines its Rust method. Function arguments, results, setters and event payloads use owned values; rendering getters can borrow storage.

Template use Generated view-model method
Read count: i32 fn count(&self) -> i32
Read title: string fn title(&self) -> &str
Assign to count, or bind it with v-model Additional fn set_count(&mut self, value: i32)
Call label(): string in a binding fn label(&self) -> String
Call reset(): void in handlers fn reset(&mut self)
Start compiled async load(): Promise<T> in a handler fn load(&mut self, cmds: &mut Vec<Cmd>)

A function used in both a binding and a handler receives &self. Bindings can call a function on each view update. Expose a list through a value getter when the template iterates it, to borrow its storage during rendering. In compiled mode, a function used in a binding must be synchronous and cannot write state or emit host commands. Async calls start compiled tasks; their source results go to awaiting tasks, while the public Rust method returns (). A native Promise object does not cross the trait boundary.

An exported literal declaration such as export declare const LIMIT: 20 supplies a compile-time constant. A numeric constant adopts its use's expected numeric type. A distinct identifier type can use the __newtype form above. A homogeneous literal tuple such as export declare const FILTERS: readonly ["ALL", "ACTIVE", "DONE"] becomes a fixed native constant array. Literal indices and len(FILTERS) fold at compile time; a variable index produces an optional value. For string | undefined, the getter returns Option<&str> and stored values use Option<String>.

An optional function such as export declare const refresh: (() => void) | undefined has an empty default Rust method. A browser call does nothing when the function is absent. Optional functions must return void.

Function-valued callbacks and slots are designated view contracts, not general model data. See functions and callbacks for the distinction and the rules for model function signatures.

Numeric rules and units

The numeric rules define annotations, literal inference, integer widths, floating precision, units, division and formatting for each execution path. View and model arithmetic have different admission rules. For example, integer / is rejected in a view binding; model / promotes integer operands to f64. Use idiv for integer division.

A host style property can supply an expected unit or width. A numeric width binding may widen to the property's f32 storage type. A dimensionless f32 value cannot substitute for a different declared unit.

Expressions and standard functions

The view/model expression table is the source-language reference. A Vue template binding uses the view column; its basename .ts model uses the model column when app.model is "compiled". The collection rules list the standard functions and explain the different array-index results.

In Vue bindings, value !== undefined and string discriminant checks narrow values inside v-if. Text accepts scalars and optional scalars, with empty text for an absent optional value. Read a scalar field or call a read-only model method to format an object or array.

Object/array literals in model bodies do not make them general template expressions. :style, prop values and other designated template forms retain their own rules above. Move admitted business computation into the model; that does not remove the model's own restrictions.

Command-line reference

Run from the repository root:

bun microts/compiler/cli.ts check vue-sfc-lab --strict
bun microts/compiler/cli.ts build vue-sfc-lab --strict
cargo check --manifest-path apps/vue-sfc-lab/Cargo.toml

The input can be an app name under apps/, an app directory or a root .vue or .tsx path. check analyzes the component tree and any selected compiled models without writing generated code. build writes view Rust and styles.bin to gen/ beside the root component; compiled mode adds model Rust modules. Demo gen/ directories are ignored by Git. Run build before Cargo.

Option Effect
--strict Reject unannotated number in contracts
build --out <directory> Choose the generated output directory
build --no-format Skip rustfmt; the default uses it when installed
build/check --ir <file> Save View IR; compiled mode also writes a sibling .model.json with Model IR
check --json Print analysis and requested board results as JSON
check --boards Report input coverage for all existing board profiles
--board <name> Require a board's input profile to cover the app; a build checks before writing output

bun microts/compiler/cli.ts run <app> --tape <file> executes the compiled model with the reference tape player. It does not launch a native display host. See the model guide for tape fields and explicit view targets.

Board reports cover input mappings. They do not establish a target toolchain or display integration. Existing profiles have no relative-axis adapter; an AxisHandler produces a missing-adapter error for those profiles. Each profile declares the level of its motion driver; meowbit has none. A native host states its own motion level by implementing the HasMotion<LEVEL> bounds the generated app requires. An AOT build generates application source assets; a device host's build compiles and packages the application.

Common diagnostics

Diagnostic Fix
Cannot resolve root component Supply its .vue path or an app directory containing app.vue, App.vue or the configured entry
View-model import must use the SFC basename For Dial.vue, use ./Dial and keep one Dial.ts or Dial.d.ts
Unannotated number, or numeric type mismatch Annotate the contract with i32, f32 or another numeric type; convert floats before integer use
:class must be a ternary Select complete class literals with condition ? '...' : '...'
@press requires a focusable View Add bare focusable, or use ActionHandler for a named button
Invalid v-for source or key Supply an array and a unique key with a supported type
Text interpolation requires a scalar Select a field, call len, or expose a formatting method
Missing prop, slot parameter or context provider Match the child's declarations; see Components
Board has no relative-axis adapter Use a host that implements the required axis capability, or change the app's input requirement
Board has no motion driver at a value's level (VB106) Subscribe to values of the board's level, or use a board whose driver fuses the required sensors
Rust view-model trait implementation is incomplete Regenerate after contract changes, then implement the trait's required methods and associated child types
Compiled model source is outside the supported subset Change the TypeScript body according to the source diagnostic, or select Rust mode and provide its native implementation