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 |
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.
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.tomlThe 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.
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 |
| 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 |