The .vyx single-file component compiler, in comptime-pure Vyrn.
components(dir) reads every <Name>.vyx in dir at compile time and synthesizes one module that exports one pure view function per component, built from std/html calls. std/ui's pages compiles .vyx pages, layouts and error pages through vyxPage, vyxLayout and vyxError. vyxParseTemplate gives a generator or a tool the parsed <template>.
import { components } from "std/vyx"
import { itemRow, cart } from components("./components")
A .vyx file has two sections:
<script> -- optional: a `props { name: Type, ... }` block (the view fn's
parameters), `import` lines (relative paths rebased to the
synthesized module, deduplicated across components), and
plain `fn`/`let`/`type` helpers (module-private).
<template> -- exactly one root element, compiled to the view fn body.
Template grammar (Vue-flavored):
{{ expr }} -> text((expr).toString()) (escaped)
v-html="expr" -> Raw(expr) as the element's content; `expr` is a
`Markup`, which `Raw` never escapes
<slot/> -> splices the trailing children param
v-if="c" / v-else-if="c" / v-else -> nested Empty-elided conditionals; a
v-else-if/v-else must be the next element
sibling (whitespace and comments between are
fine)
v-for="x in expr" (+ required :key="expr") -> keyed loop
:name="expr" (incl. :class, :value) -> dynamic attribute
name="..." -> static attribute (`class` is checked when
themed)
<Capitalized :p="e" q="s"/> / <Capitalized>...</Capitalized> -> component call
@click="handler" / @click="handler(scalar)" (also @input/@change/@submit/...)
-> On(event, "handler", (scalar).toString())
Whitespace: an all-whitespace run that spans a newline is dropped; a same-line whitespace run is one space; other runs collapse to single spaces. <!-- ... --> comments are inert, and a lone { in text is literal.
Every generation failure is a std/diag report at the .vyx line it came from. vyrn emit-gen <file> shows the synthesized module.
Generation-time components. A capitalized tag resolves first against sibling .vyx files, then against names the <script> sections import. An imported name is a provider: std/vyx emits a nested generator import that runs the imported gen fn with the tag's attributes as constant arguments, and splices its output where the tag stood.
<script>
import { Icon } from "./icon-provider"
</script>
<template>
<p><Icon name="brand:github" label="GitHub"/></p>
</template>
A provider is an ordinary module that exports a gen fn of this shape:
export gen fn <TagName>(attrs: String, file: String, line: Int64, col: Int64) -> String
attrs the tag's static attributes as a JSON object of strings, in source
order, written by `std/json` so `std/jsonread` reads it back exactly;
`{}` when there are none.
file the `.vyx` file the tag was written in.
line the tag's 1-based line in that file.
col always 1: a template node carries no column.
The generated module must export
export fn provide() -> Html
which std/vyx calls at the tag site through a namespace alias it mints. The name is not render: a user function named after a surface builtin shadows it program-wide, so it would break render here.
A provider tag takes static attributes only and no children; :attr, @event and children are diagnostics. Compute a value in a sibling .vyx component instead. The provider runs in its own sandbox and cache entry, and anchors its own reports at the file/line it is given. std/vyx cannot check the provider's shape, because a template-named provider is outside its read roots; a wrong shape fails the emitted import with the loader's diagnostic.