MarkupError
UpdatedThe rule behind this error is on the Markup page.
An .alx file is Alloy with markup in it: tags, attributes, fragments, and text, the way JSX sits in TypeScript. The markup lowers to calls on a factory the project names, and the Alloy around it compiles as in any .aly.
local function Row(props: { item: Item, on_pick: () -> () }) return ( <TextButton Size={new UDim2(1, 0, 0, 24)} Activated={props.on_pick}> {props.item.name} x{props.item.count} </TextButton> ) end
A tag names a Roblox class or a component in scope; a component starts with a capital and is called with its props. The props are a plain table the lowering builds, so a component declares their type as a type or an interface; a struct is nominal and no tag satisfies it. An attribute is Name={expr} or Name="text"; an event name takes a function. An attribute value must fit the type of its property, or the type of the prop the component declares, and any other value is a type error at the attribute. A nil value leaves a property unset. With interpolate = "wrap", a Roblox tag's value can also be a source, so the check skips it. A bare {props} among the attributes spreads a table into them. {expr} in a body is a child, and text with {holes} interpolates. In a class with a Text property, such as TextLabel, the text between the tags sets Text, and so does a lone {expr}. When an attribute sets Text, each {expr} in the body is a child. With interpolate = "plain", the default of the element form, that value must be a string or a number, or a React binding of one, and any other value is a type error. An attribute of a Roblox tag takes a binding too, and the value the binding holds must fit the property. With "wrap" it can also be a source that a reactive library reads. <> ... </> is a fragment, <!-- --> a comment. A child that depends on a condition wraps in a function, {function() return if open then <Menu /> else nil end}, so a reactive library re-runs it; a bare conditional child is built once, and the static_conditional_child lint says so.
The [alx] table of alloy.toml sets the lowering. The table form, for Vide or Fusion:
[alx.factory] backend = "table" # table: create(name)(props); element: create(name, props, children) create = "create" # the function a tag calls; a bare name must be in scope children = "Children" # the key children go under; unset, they are numeric entries event = "OnEvent" # wraps an event name: OnEvent("Activated") compute = "computed" # wraps interpolated text; `use` names the reader inside it interpolate = "wrap" # wrap: text with holes is a function that reads them; plain: a string merge = "merge" # how a spread group `{props}` combines
The element form, for React, needs a fragment; the table form takes none, and a fragment there is a plain table:
[alx.factory] backend = "element" create = "React.createElement" fragment = "React.Fragment" event = "React.Event." # a trailing dot indexes: React.Event.Activated
Either form renames tags and properties:
[alx.elements] all = "camelCase" # a naming scheme for every class, or one alias per name [alx.properties] Text = "text" # an alias for a property, everywhere or per class
The level of a markup lint sits with the other lints, under alx.:
[lint.rules] alx.static_conditional_child = "warn" # allow, warn, deny
With no [alx], the lowering is the element form for React: a tag calls React.createElement, so React must be in scope, and a fragment is React.Fragment. An [alx.factory] table sets every name itself. [fmt.alx] sets how alloy fmt lays a tag out. An ingot may read a markup attribute, ClassName for Enamel, and rewrite the tag before the lowering.
In the editor a tag and an attribute hover and complete from the Roblox API, and the semantic tokens of the lowered code stay out of the markup.
