ferrovue
Vue single-file components compiled to Rust render functions. A Rust server renders a component's
HTML with no JavaScript runtime, and the browser hydrates that HTML with the same .vue file.
- No Node on the server. The compiler runs at build time; the server is your Rust binary, and a
render is a function call that writes into a
String, 7.7 to 135 times faster than Vue'srenderToStringin the benchmarks (see Performance). - Byte-identical to Vue. The generated Rust writes exactly the bytes Vue's
renderToStringwrites for the same props, so Vue hydrates the page without a mismatch. Every supported construct is held to fixtures recorded from Vue. - Exact or refused. What ferrovue cannot reproduce exactly is a compile error that names the file, line and construct. A component that compiles renders as Vue renders it.
ferrovue is at 0.8, before 1.0: a minor release may change the configuration, the generated code or the crates' API. The CHANGELOG says what changed, and the guide's upgrade pages (0.8 to 0.9, 0.7 to 0.8) say how to move a project.
Button.vue ──(ferrovue compiler, Node)──▶ button.rs ──(your Rust server)──▶ HTML ──▶ Vue hydrates it
ferrovue doesn't interpret templates. It takes what @vue/compiler-sfc compiles a template to for
server rendering (straight-line _push calls plus a few @vue/server-renderer helpers) and
translates that small, closed vocabulary into Rust that writes the same bytes. Vue still decides
element structure, attribute order, whitespace and fragment markers.
| What it is | Published as | |
|---|---|---|
packages/ferrovue |
The compiler, the ferrovue command, the Vite plugin and the browser client (TypeScript) |
npm: ferrovue |
crates/ferrovue |
The runtime the generated Rust calls: escaping, slots, islands, <RouterLink> matching. It re-exports ferrovue-router, ferrovue-i18n and ferrovue-core, released with it |
crates.io: ferrovue (API docs) |
Quick start
1. Install both halves
ferrovue needs Rust 1.89 or later (CI tests that and the latest stable). Generated code uses let-chains, so the crate that includes it needs edition 2024.
2. Configure
pnpm ferrovue init writes a starter ferrovue.config.json and components/Hello.vue. Or write
the configuration at your project root yourself:
$schema points an editor at the configuration's JSON Schema, published with each version of the
npm package, so VS Code and other JSON editors complete the keys, describe them on hover and flag a
misspelt one. Name the version of ferrovue you use; ferrovue init writes it. ferrovue does not
read $schema. Offline, the copy in the installed package works as well:
"$schema": "./node_modules/ferrovue/schema.json".
| Key | Required | Meaning |
|---|---|---|
components |
yes | Directory of .vue files to compile |
out |
yes | Directory the Rust modules are written to. Every module ferrovue wrote there is replaced, and one no component produces any more is removed; a .rs file without ferrovue's // @generated header is left alone, and --check reports it |
routes |
no | JSON file listing the app's routes: each a vue-router path, or { "path", "name", "children" }. Or { "pages": "client/pages" }: a folder of pages, whose file names give the routes as vue-router's file-based routing reads them (index.vue, [id].vue, [[id]].vue, [...path].vue, (group) folders, a name.vue beside name/ as its layout), each page compiled as a component. Needed for <RouterLink>, <RouterView> and useRoute() |
router |
no | Instead of routes: { routes, base?, linkActiveClass?, linkExactActiveClass? }, matching createWebHistory(base) and createRouter's options |
stores |
no | Directory of Pinia option stores whose state components may read |
trustedHtml |
no | Rust type of a TrustedHtml prop (needed for v-html): ferrovue::BasicHtml (text with a few formatting tags, built in), ferrovue::InlineHtml (inline formatting tags only, built in, which makes every TrustedHtml prop inline HTML), ferrovue::Sanitised (HTML cleaned by ammonia, the ammonia feature), or a type of your own such as crate::html::CleanHtml |
dateType |
no | Rust type of a Date prop: ferrovue::JsDate (the default), chrono::DateTime<chrono::Utc> (the chrono feature), or a type of your own that implements ferrovue::JsTime. See dates_and_prices |
helpers |
no | { module, functions }: functions a template may call, each mapped to a Rust twin |
twins |
no | Components ferrovue does not compile, each rendered by a Rust function of yours: { "VBtn": { "rust": "crate::ui::v_btn", "props": { "label": "string" }, "slots": ["default"] } }. See Rust twins |
i18n |
no | vue-i18n: { messages, locale?, fallbackLocale? }, the directory of locale files (en.json, nl.json), the default locale and the fallbacks |
clientDirectives |
no | Custom directives with no server output (no getSSRProps), by name without v-: ["focus"] |
scopeId |
no | How a <style scoped> id is hashed, as @vitejs/plugin-vue hashes it: "filepath-source" (the default, the plugin's in a production build) or "filepath". See scoped_styles |
viteRoot |
no | Vite's root, relative to this file's directory, from which a component's path is hashed (default .) |
isProduction |
no | Whether @vitejs/plugin-vue compiles for production, as Vite's isProduction: true (the default) for vite build, false for the dev server. It names the variables v-bind() in <style> sets. See scoped_styles |
cssModules |
no | { generateScopedName, hashPrefix?, context? }, as Vite's css.modules names CSS module classes: needed for <style module>. context is the directory Vite runs in (default viteRoot). See scoped_styles |
builders |
no | false leaves out each props struct's and shared type's new() and setters, for an app that builds its props as struct literals (default true) |
3. Write a component
<!-- client/components/Greeting.vue -->
<script setup lang="ts">
defineProps<{ name: string; unread: number; note?: string }>();
</script>
<template>
<p class="greeting">Hello, {{ name }}!<b v-if="unread">{{ unread }} new</b><i v-if="note">{{ note }}</i></p>
</template>
4. Generate and render
-c, --config <path> reads another configuration file. --watch regenerates on a change to what a
run reads (the configuration, the components, stores, locale messages, pages or routes file, and
the .ts files components import types from). A Vite project can use the plugin instead, which
does the same in its dev server and before a build:
// vite.config.ts
import ferrovue from "ferrovue/vite";
export default { plugins: [ferrovue({ config: "config/ferrovue.json" })] };
root is the project root the configuration's paths are relative to (Vite's working directory by
default), and config the configuration file in it, as --config takes it (ferrovue.config.json
by default).
Every error carries a stable code, as error[FV1805]: components/Card.vue:4:17: …, documented in
the error_codes guide.
--format json writes the same errors as JSON with the exit status unchanged; the
quick start
has a VS Code problem matcher that puts them in the Problems panel.
use ;
// `new` takes the required props; each optional one has a setter.
let props = new.note;
// The markup alone:
let html: String = html.into_string;
// …or as an island the client hydrates: wrapped in
// <div data-island="Greeting" data-props="{…}">…</div>
let island = island.into_string;
// (`island()` exists for a component that renders from its props alone: the client rebuilds it from
// `data-props`. One that also takes slots, the route, stores, translations, teleports, provided
// values or the page head has `html()`, and the page's own app mounts it; see examples/fullstack.)
// …or straight into a buffer you already hold:
let mut page = Stringfrom;
render;
On the client, mountIslands(islands) from ferrovue, with islands from ferrovue/islands
(written by the Vite plugin), hydrates every island on the page; see
islands and hydration.
With routes configured, the generated route_table::router() builds the router once. Resolve
each request's location with router.at(path) and pass the result to the components that take a
route.
The crate's features:
-
routerandi18n, both on by default: vue-router and vue-i18n. An application with neither routes nor translations can leave them out, and build neitherferrovue-routernorferrovue-i18n:ferrovue = { version = "0.8", default-features = false }. -
axumandactix-web:Htmlis anIntoResponseor aResponder, so a handler responds with the component astext/html; charset=utf-8.into_html(props)andinto_island(props)take the props by value, so a handler that builds them returns the page:async -
stream, whichaxumandactix-webturn on:HtmlStream, a page sent in the order its parts are ready. -
maud:ferrovue::Htmlimplementsmaud::Render, so(greeting::html(&props))goes straight into amaud::html!page. -
dioxus:{greeting::island(&props)}in a Dioxus 0.7rsx!is the island element itself, with exactly the markup Vue hydrates inside it (examples/dioxus). -
ammonia:ferrovue::Sanitised, HTML cleaned by ammonia, forv-html.ferrovue::BasicHtmlandferrovue::InlineHtmlare built in; theescapingguide describes all three.
The crate's feature flags list what each adds.
5. Diagnostics in your editor
ferrovue/volar is a plugin for Vue's language tools, which the Vue (Official) extension for VS Code
and Vue support in other editors run. It compiles the project as you type and underlines what
ferrovue refuses in the open component, with the error's code linked to its documentation. Name it
in the tsconfig.json that includes your components, then run Vue: Restart Vue and TS servers:
The quick start
has the details; examples/fullstack/tsconfig.json enables it.
What a component may use
ferrovue compiles <script setup lang="ts"> components, and components with no script at all
(an icon, a divider), which take no props. Props are declared by type, with
defineProps<{ … }>(), an interface, or a type alias.
| Area | Supported |
|---|---|
| Prop types | string, number (integers, i64, written and computed as JavaScript does, exact within ±2⁵³), Float from ferrovue/types (fractions, f64), boolean, string-literal unions ("sm" | "md"), T | undefined, T | null (an Option written as null), arrays (T[], Array<T>, readonly T[]), interfaces and object type aliases (recursive ones too), dictionaries (Record<string, T>, { [key: string]: T }, ferrovue::Record in Rust, which keeps JavaScript's order of keys), another component's exported Props, TrustedHtml, InlineHtml, Date (a component's own prop, optional or nullable too: ferrovue::JsDate, or chrono::DateTime<Utc> with the chrono feature) |
| Shared types | Interfaces and type aliases imported from .ts files (generated once, into types.rs), from a store's file, or from another component's .vue file; objects of a shared type can be passed between components. A TypeScript enum whose members are literals is the type of its values: a string, or a number |
| Constants | Constants imported from .ts files and enums (imported, or declared in the component), evaluated when the component is compiled: strings, numbers, booleans, null, lists of them, objects of them read field by field (LABELS.save, Tone.Loud, Rank[5]), and lists of objects of them, which become a const in types.rs ({ value, label } items get a type named after the constant, OptionsItem, unless the constant is typed with an interface: OPTIONS: Option[]; a field that is null in some items is `T |
| Props | withDefaults, destructured props with defaults (const { size = "md" } = defineProps<…>()), the props object (const props = defineProps<…>(), then props.label in the template or in script code), optional booleans (Vue casts an absent one to false), defineModel (named, required, with defaults), components with no props. A generic component (<script setup generic="T extends Item">) renders each type parameter as its constraint, which is all the template can rely on |
| Setup | ref/shallowRef, computed (an expression or a single return; one that may throw, through .repeat(), .padStart(), a date, $t or a helper, is evaluated where the render first reads it, once, as Vue does), refs that start empty, typed by their type argument (ref<Row[]>([]), ref<Row>(), ref<string | undefined>(undefined)), plain const/let, .value in script code, helpers with Rust twins, a plain <script> block beside setup. Lifecycle hooks, watch (not immediate), defineEmits, defineSlots, defineOptions, defineExpose, template refs (ref(null), useTemplateRef) and functions are client-only. The template may name them only from event handlers, which the server drops |
| Provide and inject | provide and inject resolved through the component tree, slot content seeing the providers of the component that renders it, as Vue does: keys that are string literals or InjectionKey symbols exported from .ts files; values that are props, refs, computeds, constants or objects of them (an interface's fields, reactive() unwrapping refs); defaults, factory defaults (inject(k, () => …, true)), object defaults built as the key's interface (inject(k, { size: "md" })), and keys holding functions, which only event handlers may call. A page rendered from Rust passes the generated Provides, empty or holding what the client app gives app.provide. See the crate's provide_inject guide |
| Text | {{ }} of strings, integers and booleans; +, -, *, /, %, with integers and fractions as JavaScript computes them; <, >, <=, >= (between numbers, or between strings by UTF-16 code unit), ===, !==; unary -, !; ??, ||, &&, ?:; template literals and String() of strings, numbers, booleans, values that may be absent (undefined or null, as JavaScript writes them) and lists (their items joined by ,); typeof; optional chaining a?.b, a?.[i] and chains that go on past it (a?.b.c, s?.trim().length, list?.at(-1)?.name), with every property and method the value has when present, undefined as a whole once a is absent; .length; .toString(), .toFixed(), .toPrecision() and .toExponential() (with no argument or a literal one), Math.max/min (of a spread list too, Math.max(...xs))/abs/round/floor/ceil/trunc/sqrt/sign, the constants of Math and Number, ** and Math.pow() of an integer and a whole exponent known when compiling, exact within ±2⁵³; Number(), parseInt() (no radix, 10 or 16), parseFloat(); Number.isNaN(), Number.isFinite(), Number.isInteger() and Number.isSafeInteger() of a number, present or optional, and isNaN() and isFinite() of a present number; JSON.stringify() of strings, numbers, booleans and lists of those; encodeURIComponent() and encodeURI(); array literals |
| Strings | .trim(), .trimStart(), .trimEnd(), .trimLeft(), .trimRight(), .toUpperCase(), .toLowerCase(), .includes(), .startsWith(), .endsWith(), .indexOf() and .lastIndexOf(), with a position or without, .slice(), .substring(), .substr(), .at(), .charAt(), .charCodeAt(), .codePointAt(), .concat(), .split() with a limit or without, .replace() and .replaceAll() with string patterns ($&, $`, $', $$ read as JavaScript reads them), .padStart(), .padEnd(), .repeat(); indices and lengths count UTF-16 code units, as JavaScript's do (see the crate's strings guide) |
| Dates | new Date(s) and Date.parse(s) of an ISO string (a date alone, or a date and time with a Z or an offset, as ECMAScript's date time string format specifies), new Date(ms), Date.UTC(…) of one to seven parts, .toISOString(), .toUTCString(), .toJSON(), JSON.stringify(), .getTime(), .valueOf() and the .getUTC…() methods, and <, <=, > and >= between two dates, invalid dates included, as JavaScript computes them, a date's validity tested with Number.isNaN(d.getTime()), and dates chosen with ?:, ?? and ` |
| Conditions | v-if / v-else-if / v-else, ?:, && and ` |
| Lists | v-for over arrays (of strings, numbers, objects or child props), array literals and number ranges (n in 5); with an index; with destructured items ({ id, name } in rows); nested; on <template>. .includes() (a number looked for in a list of numbers whether either is a number or a Float, as JavaScript compares them), .join(), and .filter(), .map(), .some(), .every(), .find(), .findIndex(), .findLast(), .findLastIndex() with an arrow function of the item (a name, or destructured) and its index, whose body is an expression, narrowing inside as outside, or with Boolean; .filter(Boolean) of a list or an array literal whose items may be null or undefined, which keeps the ones present and truthy as a list of the values ([name, title, city].filter(Boolean).join(" · ")); Boolean(x); .slice(); an item by its index, list[i] with an integer i and list.at(i) (from the end when negative), undefined outside the list and narrowed as other optional values are (v-if="rows[0]"); .indexOf() and .lastIndexOf() of a string, number or boolean, compared with === (NaN found nowhere, -0 found as 0); .concat() of lists of one item type (numbers whether number or Float); .reduce() and .reduceRight() with an initial value, into a string, a number or a boolean; .toReversed(), and .toSorted() by String(x) as JavaScript sorts without a comparator ([10, 9, 1] to [1, 10, 9]) or by a comparator (a, b) => a - b, a.k - b.k of integers or ?: over < and > of integers or strings, stable as JavaScript's; [...list].sort(…) and [...list].reverse(); spread in array literals ([...a, x, ...b]) of one item type; chained and nested; a computed list read by v-for, .join(), .length, .includes(), kept by computed, or handed to a child |
| Dictionaries | v-for="(value, key, index) in r" over a Record<string, T>, and ([key, value], index) in Object.entries(r), in JavaScript's order: keys that are array indices first, in numeric order, then the others as the props give them; Object.keys(r), Object.values(r), Object.entries(r).length; handed to a child |
| Attributes | static and bound attributes, boolean attributes, :hidden, data-* and aria-*, v-bind objects |
class |
strings, arrays, objects ({ active: on }, computed keys), cond && "x", cond ? "x" : null, merged with a static class |
style |
objects (camelCase or kebab-case keys, --custom properties), arrays of objects, strings, merged with a static style, and v-show; later values override earlier ones as in Vue. A global <style> block is allowed |
| Scoped styles | <style scoped>: the id on every element, on child components' roots (a root that is itself a component, fragments, recursion and inheritAttrs: false as Vue renders them) and, from a component with :slotted() rules, on the slot content it is given, forwarded slots included; inside <Transition>, <KeepAlive>, <Teleport> and v-if; on <RouterLink> and what it holds, as vue-router renders them |
v-bind() in <style> |
variables bound to props, setup values, computeds and constants, quoted expressions included (v-bind('size + "px"')), in a scoped or global <style>: written into the root's style after its own and what a parent passes, on each root of a fragment, and handed to a root that is a component, as Vue's server writes them; undefined and null as initial, an empty string as a space; named as plugin-vue names them for production or development (isProduction) |
| CSS modules | <style module> and <style module="name">, read as $style.x, name.x, $style['x-y'] and through useCssModule(), each class a constant in the generated code, when cssModules mirrors Vite's css.modules.generateScopedName: computed as postcss-modules computes it in Vite ([local], [name], [hash:base64:5], [path] and the rest), composed classes, @values and keyframes included |
| Components | imported child components, v-bind of a child's own Props, listeners in any form (@x, v-on="obj", @[name]), which the server skips, v-model on a child's defineModel, recursion; props named in kebab-case or camelCase; defineAsyncComponent(() => import("./X.vue")) (or { loader: … }), rendered as the component it loads, which Vue's server renderer waits for |
<component :is> |
over a closed set of choices, compiled to a match: an imported component (:is="Card"), an HTML element's name (is="h2"), a computed or ?: choosing among them, a prop typed as a union of string literals (as: "h1" | "h2"), and an object of imported components (or element names) declared in setup or imported from a .ts file, read by such a prop (ICONS[name]); props, fallthrough attributes, slots and scope ids reach each choice as they reach a static child; also inside <KeepAlive> and <Transition>. See the crate's generated_code guide |
| Escape hatches | <ClientOnly> from ferrovue/client, whose default slot the server never renders, so anything may go in it; components listed in twins, rendered by Rust functions of yours. See the crate's errors_and_limits guide |
| Fallthrough attributes | what a parent passes a child beyond its props (static and bound attributes, class, style, data-*, aria-*, booleans, undefined): onto its single root, merged with the root's own class and style and replacing its other attributes where they stand, as Vue's mergeProps merges them; none for two roots; on through a root that is a component, or a <RouterLink>; with inheritAttrs: false, onto the elements or components that bind v-bind="$attrs" or a useAttrs() binding, before or after their own; beside scope ids. Listeners are dropped, as Vue's server drops them. See the crate's generated_code guide |
| Slots | default and named slots, fallbacks, $slots.name tests and the same through useSlots(), scoped slots (<slot :item="x"> and #item="{ item }" or v-slot="props"), whose props a parent can hand to its own children |
| Forms | v-model on text inputs, checkboxes, radios, <select> and <textarea> (renders the initial state) |
| Built-ins | <Transition>, <TransitionGroup>, <KeepAlive>, <Suspense> (synchronous content), <Teleport> (to any target, nested, disabled: see below), v-text, v-once, v-pre, v-memo, custom directives listed in clientDirectives |
| Vue Router | <RouterLink> (resolved by name or imported) with a string to or { name, params, query, hash } / { path, query, hash }, active-class, exact-active-class, aria-current-value, replace; vue-router's own encoding and active-link matching, nested routes included (a parent link is active on its children's pages, exact only on its own); a history base; optional parameters (:id?); routes built from a folder of pages, which the Vite plugin also gives the client as ferrovue/routes. useRoute() and $route: path, fullPath, hash, name, params, and query (a value written once, without =, or repeated, exactly as vue-router parses it; typeof route.query.q === "string" narrows one to a single string). <RouterView>, at the top and in nested route components: each takes the page it shows as a slot |
| Pinia | option stores with a typed state, and setup stores (defineStore(id, () => { … })) whose returned refs are typed by ref<T>() or their initial literal; read through useX() or storeToRefs, in the template or in computed; getters that are an expression of the state, and a setup store's computeds, which may read each other |
| vue-i18n | $t and useI18n()'s t and locale: named and list values, plurals by vue-i18n's rule, literals, linked messages with upper/lower/capitalize, nested and flat keys, fallback locales, a missing key shown as itself. Messages are parsed at build time by vue-i18n's own compiler |
v-html |
only on a TrustedHtml prop (import type { TrustedHtml } from "ferrovue/types"), or an InlineHtml one (inline tags only, ferrovue::InlineHtml on the server, with no trustedHtml needed), the only HTML it may write inside a <p>; never on a table's structure, a <select> or <optgroup>, or an SVG or MathML element that holds no HTML. See the crate's escaping guide |
| Page head | useHead (and useServerHead) from @unhead/vue 3: title, titleTemplate, base, meta, link, script, style, noscript, htmlAttrs and bodyAttrs, with values the server computes (props, setup bindings, getters such as () => props.title, list.map(x => ({ … })) for a list of tags), key, tagPosition, tagPriority and tagDuplicateStrategy; useSeoMeta (and useServerSeoMeta) of string, number, boolean and null values. Collected into a ferrovue::Head in the order Vue's server runs them, and written as unhead's renderSSRHead writes them: its tag order, deduplication, title template and escaping. See the crate's head guide |
Anything else is refused at compile time, with an error that names the file, line and column,
quotes the line and puts a caret under the construct. The
errors_and_limits
guide lists what is refused and why, and
error_codes every code.
Guides
The crate's guide, ferrovue::guide on docs.rs, covers each topic in depth:
- Quick start and
the generated code:
what the compiler writes, and a component's
Props,render,htmlandisland - Props: TypeScript types in Rust, dictionaries, shared types and constants
- Slots, scoped slots from Rust included
- Scoped styles:
<style scoped>ids that match@vitejs/plugin-vue,v-bind()in<style>, CSS modules - Routing, translating with vue-i18n, Pinia and provide and inject
- Teleports and
the page head (
useHead,useSeoMeta) - Islands and hydration: islands, lazy hydration, a page hydrated whole, navigating between pages
- Streaming with holes, web frameworks (axum, actix-web) and Dioxus
- Numbers, strings and dates and prices: JavaScript's semantics in Rust
- Escaping and security
- Testing against Vue: your own components held to Vue with fixtures and two calls
- Errors and limits,
with helpers,
<ClientOnly>and Rust twins, and every error code - Upgrading a project from 0.8 to 0.9 and from 0.7 to 0.8
examples/fullstack
is a complete app to copy: an axum server, a Vite client, islands, Pinia, vue-router and streaming,
with a test that it hydrates. CONTRIBUTING.md
describes the repository and how to work on it.
Performance
Time to render one component to an HTML string: Vue's renderToString on Node, and the Rust that
ferrovue generated from the same .vue file, given the same props. Each side checks its output
against crates/ferrovue/benches/expected/<scenario>.html before anything is timed.
| Scenario | What renders | Output | Vue renderToString |
ferrovue | Speed-up |
|---|---|---|---|---|---|
small |
Nav: two <RouterLink>s resolved against the current route |
206 B | 36.41 µs | 0.989 µs | 36.8× |
list |
Lists: 1,000 words, 1,000 numbers, 100 groups of 10 members |
102 KiB | 355.8 µs | 46.16 µs | 7.7× |
tree |
Tree: a recursive component, binary tree 8 levels deep (255 nodes) |
9.5 KiB | 522.2 µs | 3.86 µs | 135.2× |
page |
Dashboard: 22 Panels with named slots, a Text, 20 Frames holding loops |
8.0 KiB | 181.3 µs | 5.06 µs | 35.8× |
Mean times per render. Vue: tinybench's mean of one awaited renderToString(app), 5 s per
scenario after a 1 s warm-up, the production build, each component with its SSR build's
ssrRender, the app, Pinia and (for small) a vue-router at /users/me created outside the loop.
ferrovue: criterion's mean point estimate (3 s warm-up, 100 samples) of a new String and one call
of the generated render, release profile, the props, router and Route built outside the loop.
Measured on an AMD Ryzen 5 3600XT (6 cores / 12 threads, up to 3.8 GHz) with 62 GiB of RAM, running
Linux 7.2.8-2-cachyos (CachyOS, x86_64), Node 26.10.0, Vue 3.5.43 and rustc 1.99.0, lightly loaded
(a load average of about 1): treat the numbers as indicative. Most of Vue's time goes on component
instances and their virtual nodes, of which tree has 255 and list one: per byte written, Vue
renders list some fourteen times faster than tree, while ferrovue writes the two at a similar
rate.
pnpm bench runs both halves (pnpm bench:rust, then pnpm bench:js), and pnpm bench:record
rewrites benches/expected/*.html from Vue after a scenario changes. CI runs both in a short mode
to check they build, run and agree on the HTML, and holds the instructions each scenario runs to
within 5% of a committed baseline, and the time ferrovue and cargo build take on a project of 500
components to a budget; TESTING.md
describes both.
Versions
Byte-identical output depends on Vue's own SSR compiler, so ferrovue compiles with an exact
@vue/compiler-sfc (currently 3.5.44), and its conformance fixtures are recorded from one version
of Vue and of each integration. Your project may use any later patch of the same minor:
| Peer | Supported | Recorded from |
|---|---|---|
vue |
~3.5.44 (3.5.44 and later 3.5 patches) |
3.5.44 |
vue-router (optional) |
~5.4.0 |
5.4.0 |
pinia (optional) |
~4.1.0 |
4.1.0 |
vue-i18n (optional) |
~11.4.15 |
11.4.15 |
@unhead/vue (optional) |
~3.4.3 |
3.4.3 |
A new minor of any of them (Vue 3.6, vue-router 5.5, …) needs a ferrovue release that re-records the fixtures from it. A weekly CI job re-records them from the newest patch each range allows and hydrates the committed HTML with it, so a patch that changes what Vue writes or hydrates is caught before it reaches you; a ferrovue patch follows when one does.
Deprecations
What ferrovue stops supporting is deprecated for at least one minor release and removed in the next
major release. A deprecated crate item carries #[deprecated]; a deprecated configuration key warns
with warning[FV1117] and a command-line option with warning[FV1118], naming the replacement, and the JSON Schema marks a deprecated key for editors.
Each release's CHANGELOG lists what it deprecates under "Deprecated".
Licence
Licensed under either of Apache License, Version 2.0 or MIT licence, at your option. Unless you explicitly state otherwise, any contribution you intentionally submit for inclusion in this work, as defined in the Apache-2.0 licence, is dual-licensed as above, without any additional terms or conditions.