ferrovue 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,29 @@
1
1
  # ferrovue
2
2
 
3
3
  [![crates.io](https://img.shields.io/crates/v/ferrovue.svg)](https://crates.io/crates/ferrovue)
4
- [![docs.rs](https://img.shields.io/docsrs/ferrovue)](https://docs.rs/ferrovue)
5
4
  [![npm](https://img.shields.io/npm/v/ferrovue.svg)](https://www.npmjs.com/package/ferrovue)
5
+ [![docs.rs](https://img.shields.io/docsrs/ferrovue)](https://docs.rs/ferrovue)
6
6
  [![CI](https://github.com/Kinsmir/ferrovue/actions/workflows/ci.yml/badge.svg)](https://github.com/Kinsmir/ferrovue/actions/workflows/ci.yml)
7
7
  [![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](#licence)
8
8
 
9
- Vue single-file components compiled to Rust render functions. With them, a Rust server renders a
10
- component's HTML with no JavaScript at run time, and the browser hydrates the result with the same
11
- `.vue` file.
9
+ Vue single-file components compiled to Rust render functions. A Rust server renders a component's
10
+ HTML with no JavaScript runtime, and the browser hydrates that HTML with the same `.vue` file.
11
+
12
+ - **No Node on the server.** The compiler runs at build time; the server is your Rust binary, and a
13
+ render is a function call that writes into a `String`, 7.7 to 135 times faster than Vue's
14
+ `renderToString` in the benchmarks (see [Performance](#performance)).
15
+ - **Byte-identical to Vue.** The generated Rust writes exactly the bytes Vue's `renderToString`
16
+ writes for the same props, so Vue hydrates the page without a mismatch. Every supported construct
17
+ is held to fixtures recorded from Vue.
18
+ - **Exact or refused.** What ferrovue cannot reproduce exactly is a compile error that names the
19
+ file, line and construct. A component that compiles renders as Vue renders it.
20
+
21
+ ferrovue is at 0.8, before 1.0: a minor release may change the configuration, the generated code or
22
+ the crates' API. The [CHANGELOG](https://github.com/Kinsmir/ferrovue/blob/main/CHANGELOG.md) says
23
+ what changed, and the guide's upgrade pages
24
+ ([0.8 to 0.9](https://docs.rs/ferrovue/latest/ferrovue/guide/upgrading_to_0_9/index.html),
25
+ [0.7 to 0.8](https://docs.rs/ferrovue/latest/ferrovue/guide/upgrading_to_0_8/index.html)) say how
26
+ to move a project.
12
27
 
13
28
  ```text
14
29
  Button.vue ──(ferrovue compiler, Node)──▶ button.rs ──(your Rust server)──▶ HTML ──▶ Vue hydrates it
@@ -17,23 +32,19 @@ component's HTML with no JavaScript at run time, and the browser hydrates the re
17
32
  ferrovue doesn't interpret templates. It takes what `@vue/compiler-sfc` compiles a template to for
18
33
  server rendering (straight-line `_push` calls plus a few `@vue/server-renderer` helpers) and
19
34
  translates that small, closed vocabulary into Rust that writes **the same bytes**. Vue still decides
20
- element structure, attribute order, whitespace and fragment markers. If a single byte differed, the
21
- browser would report a hydration mismatch, so anything outside that vocabulary is a compile error
22
- that names the component and the construct. ferrovue never guesses.
23
-
24
- The project has two halves:
35
+ element structure, attribute order, whitespace and fragment markers.
25
36
 
26
37
  | | What it is | Published as |
27
38
  |---|---|---|
28
- | `packages/ferrovue` | The compiler and the `ferrovue` command (TypeScript, runs on Node) | npm: [`ferrovue`](https://www.npmjs.com/package/ferrovue) |
29
- | `crates/ferrovue` | The runtime the generated Rust calls: escaping, slots, islands, `<RouterLink>` matching. The router, vue-i18n and the escaping and number primitives they share are crates of their own (`crates/ferrovue-router`, `ferrovue-i18n`, `ferrovue-core`), which `ferrovue` re-exports | crates.io: [`ferrovue`](https://crates.io/crates/ferrovue) ([API docs](https://docs.rs/ferrovue)) |
39
+ | `packages/ferrovue` | The compiler, the `ferrovue` command, the Vite plugin and the browser client (TypeScript) | npm: [`ferrovue`](https://www.npmjs.com/package/ferrovue) |
40
+ | `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`](https://crates.io/crates/ferrovue) ([API docs](https://docs.rs/ferrovue)) |
30
41
 
31
42
  ## Quick start
32
43
 
33
44
  ### 1. Install both halves
34
45
 
35
46
  ```sh
36
- pnpm add -D ferrovue # or npm / yarn
47
+ pnpm add -D ferrovue
37
48
  cargo add ferrovue serde --features serde/derive
38
49
  cargo add --dev serde_json # the generated conformance helper uses it under cfg(test)
39
50
  ```
@@ -43,11 +54,12 @@ let-chains, so the crate that includes it needs **edition 2024**.
43
54
 
44
55
  ### 2. Configure
45
56
 
46
- `ferrovue.config.json` at your project root:
57
+ `pnpm ferrovue init` writes a starter `ferrovue.config.json` and `components/Hello.vue`. Or write
58
+ the configuration at your project root yourself:
47
59
 
48
60
  ```json
49
61
  {
50
- "$schema": "https://cdn.jsdelivr.net/npm/ferrovue@0.7.0/schema.json",
62
+ "$schema": "https://cdn.jsdelivr.net/npm/ferrovue@0.8.0/schema.json",
51
63
  "components": "client/components",
52
64
  "out": "src/generated"
53
65
  }
@@ -67,14 +79,15 @@ read `$schema`. Offline, the copy in the installed package works as well:
67
79
  | `router` | no | Instead of `routes`: `{ routes, base?, linkActiveClass?, linkExactActiveClass? }`, matching `createWebHistory(base)` and `createRouter`'s options |
68
80
  | `stores` | no | Directory of Pinia option stores whose state components may read |
69
81
  | `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` |
82
+ | `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`](https://docs.rs/ferrovue/latest/ferrovue/guide/dates_and_prices/index.html#date-props) |
70
83
  | `helpers` | no | `{ module, functions }`: functions a template may call, each mapped to a Rust twin |
71
- | `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 [Escape hatches](#escape-hatches) |
84
+ | `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](https://docs.rs/ferrovue/latest/ferrovue/guide/errors_and_limits/index.html#rust-twins) |
72
85
  | `i18n` | no | vue-i18n: `{ messages, locale?, fallbackLocale? }`, the directory of locale files (`en.json`, `nl.json`), the default locale and the fallbacks |
73
86
  | `clientDirectives` | no | Custom directives with no server output (no `getSSRProps`), by name without `v-`: `["focus"]` |
74
- | `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](#scoped-styles) |
87
+ | `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`](https://docs.rs/ferrovue/latest/ferrovue/guide/scoped_styles/index.html) |
75
88
  | `viteRoot` | no | Vite's root, relative to this file's directory, from which a component's path is hashed (default `.`) |
76
- | `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](#scoped-styles) |
77
- | `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](#scoped-styles) |
89
+ | `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`](https://docs.rs/ferrovue/latest/ferrovue/guide/scoped_styles/index.html) |
90
+ | `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`](https://docs.rs/ferrovue/latest/ferrovue/guide/scoped_styles/index.html) |
78
91
  | `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`) |
79
92
 
80
93
  ### 3. Write a component
@@ -93,7 +106,6 @@ defineProps<{ name: string; unread: number; note?: string }>();
93
106
  ### 4. Generate and render
94
107
 
95
108
  ```sh
96
- pnpm ferrovue init # a starter ferrovue.config.json and components/Hello.vue
97
109
  pnpm ferrovue # writes src/generated/{greeting.rs, mod.rs}
98
110
  pnpm ferrovue --check # in CI: exits 1 if the committed modules are stale
99
111
  pnpm ferrovue --check --diff # …and shows what differs, as `diff -u` does
@@ -103,8 +115,8 @@ pnpm ferrovue --watch # regenerates when components or configs change
103
115
 
104
116
  `-c, --config <path>` reads another configuration file. `--watch` regenerates on a change to what a
105
117
  run reads (the configuration, the components, stores, locale messages, pages or routes file, and
106
- the `.ts` files components import types from), not on any other file of the project. A Vite project
107
- can use the plugin instead, which does the same in its dev server and before a build:
118
+ the `.ts` files components import types from). A Vite project can use the plugin instead, which
119
+ does the same in its dev server and before a build:
108
120
 
109
121
  ```ts
110
122
  // vite.config.ts
@@ -117,12 +129,11 @@ export default { plugins: [ferrovue({ config: "config/ferrovue.json" })] };
117
129
  default), and `config` the configuration file in it, as `--config` takes it (`ferrovue.config.json`
118
130
  by default).
119
131
 
120
- Every error carries a stable code, as `error[FV0602]: components/Card.vue:4:17: …`, documented in
121
- the crate's [`error_codes`](https://docs.rs/ferrovue/latest/ferrovue/guide/error_codes/index.html)
122
- guide. `--format json` writes the same errors as JSON with the exit status unchanged, and a VS Code
123
- problem matcher for `tasks.json` puts them in the Problems panel: both are in the
124
- [quick start](https://docs.rs/ferrovue/latest/ferrovue/guide/quick_start/index.html#editor-and-ci-diagnostics).
125
- In the editor, the `ferrovue/volar` plugin shows them as you type (step 5).
132
+ Every error carries a stable code, as `error[FV1805]: components/Card.vue:4:17: …`, documented in
133
+ the [`error_codes`](https://docs.rs/ferrovue/latest/ferrovue/guide/error_codes/index.html) guide.
134
+ `--format json` writes the same errors as JSON with the exit status unchanged; the
135
+ [quick start](https://docs.rs/ferrovue/latest/ferrovue/guide/quick_start/index.html#editor-and-ci-diagnostics)
136
+ has a VS Code problem matcher that puts them in the Problems panel.
126
137
 
127
138
  ```rust
128
139
  #[rustfmt::skip]
@@ -149,42 +160,41 @@ let mut page = String::from("<!doctype html><body>");
149
160
  greeting::render(&mut page, &props);
150
161
  ```
151
162
 
163
+ On the client, `mountIslands(islands)` from `ferrovue`, with `islands` from `ferrovue/islands`
164
+ (written by the Vite plugin), hydrates every island on the page; see
165
+ [islands and hydration](https://docs.rs/ferrovue/latest/ferrovue/guide/islands_and_hydration/index.html).
152
166
  With `routes` configured, the generated `route_table::router()` builds the router once. Resolve
153
167
  each request's location with `router.at(path)` and pass the result to the components that take a
154
168
  route.
155
169
 
156
- The router and vue-i18n are features of the crate, `router` and `i18n`, both on by default. An
157
- application with neither routes nor translations can leave them out, and build neither
158
- `ferrovue-router` nor `ferrovue-i18n`: `ferrovue = { version = "0.7", default-features = false }`.
159
-
160
- With the `maud` feature, `ferrovue::Html` implements `maud::Render`, so `(greeting::html(&props))` can go
161
- straight into a `maud::html!` page.
162
-
163
- For `v-html`, `ferrovue::BasicHtml` is built in: `BasicHtml::new(untrusted)` escapes everything but
164
- `<b>`, `<i>`, `<em>`, `<strong>`, `<code>`, `<br>`, `<p>`, `<ul>`, `<ol>` and `<li>` written with no
165
- attributes, and balances them; `BasicHtml::from_text(text)` turns plain text into paragraphs.
166
- `ferrovue::InlineHtml` does the same with `<b>`, `<i>`, `<em>`, `<strong>`, `<code>` and `<br>`
167
- only: it is what a prop typed `InlineHtml` from `ferrovue/types` is on the server, and the HTML
168
- `v-html` may write inside a `<p>`.
169
- With the `ammonia` feature, `ferrovue::Sanitised` is HTML cleaned by [ammonia](https://docs.rs/ammonia):
170
- name it as `trustedHtml` and build a `v-html` prop with `Sanitised::new(untrusted)`, or
171
- `Sanitised::with(&builder, untrusted)` for a policy of your own. The server renders the cleaned
172
- string, and the island's `data-props` carries that same string to the client.
170
+ The crate's features:
173
171
 
174
- With the `axum` feature, `Html` is an `IntoResponse`, and with `actix-web` a `Responder`: a handler
175
- responds with the component, as `text/html; charset=utf-8`. `into_html(props)` and
176
- `into_island(props)` take the props by value, so a handler that builds them returns the page:
172
+ - `router` and `i18n`, both on by default: vue-router and vue-i18n. An application with neither
173
+ routes nor translations can leave them out, and build neither `ferrovue-router` nor
174
+ `ferrovue-i18n`: `ferrovue = { version = "0.8", default-features = false }`.
175
+ - `axum` and `actix-web`: `Html` is an `IntoResponse` or a `Responder`, so a handler responds with
176
+ the component as `text/html; charset=utf-8`. `into_html(props)` and `into_island(props)` take the
177
+ props by value, so a handler that builds them returns the page:
177
178
 
178
- ```rust
179
- async fn hello(Path(name): Path<String>) -> impl IntoResponse {
180
- greeting::into_html(greeting::Props::new(name, 0))
181
- }
182
- ```
183
-
184
- With the `dioxus` feature, a component goes into a Dioxus 0.7 page (`dioxus-ssr` or fullstack):
185
- `{greeting::island(&props)}` in `rsx!` is the island element itself, with exactly the markup Vue
186
- hydrates inside it. See `examples/dioxus` and the crate guide's
187
- [Dioxus page](https://github.com/Kinsmir/ferrovue/blob/main/crates/ferrovue/docs/guide/dioxus.md).
179
+ ```rust
180
+ async fn hello(Path(name): Path<String>) -> impl IntoResponse {
181
+ greeting::into_html(greeting::Props::new(name, 0))
182
+ }
183
+ ```
184
+
185
+ - `stream`, which `axum` and `actix-web` turn on: `HtmlStream`, a page sent in the order its parts
186
+ are ready.
187
+ - `maud`: `ferrovue::Html` implements `maud::Render`, so `(greeting::html(&props))` goes straight
188
+ into a `maud::html!` page.
189
+ - `dioxus`: `{greeting::island(&props)}` in a Dioxus 0.7 `rsx!` is the island element itself, with
190
+ exactly the markup Vue hydrates inside it (`examples/dioxus`).
191
+ - `ammonia`: `ferrovue::Sanitised`, HTML cleaned by [ammonia](https://docs.rs/ammonia), for
192
+ `v-html`. `ferrovue::BasicHtml` and `ferrovue::InlineHtml` are built in; the
193
+ [`escaping`](https://docs.rs/ferrovue/latest/ferrovue/guide/escaping/index.html) guide describes
194
+ all three.
195
+
196
+ The crate's [feature flags](https://docs.rs/ferrovue/latest/ferrovue/index.html#feature-flags) list
197
+ what each adds.
188
198
 
189
199
  ### 5. Diagnostics in your editor
190
200
 
@@ -211,17 +221,17 @@ ferrovue compiles `<script setup lang="ts">` components, and components with no
211
221
 
212
222
  | Area | Supported |
213
223
  |---|---|
214
- | 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` |
224
+ | 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) |
215
225
  | 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 |
216
- | Constants | Constants imported from `.ts` files and `enum`s (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 | null`). See [Constants](#constants) |
226
+ | Constants | Constants imported from `.ts` files and `enum`s (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 | null`). See the crate's [`props`](https://docs.rs/ferrovue/latest/ferrovue/guide/props/index.html#constants) guide |
217
227
  | 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 |
218
228
  | 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 |
219
229
  | 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, `computed`s, 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`](https://docs.rs/ferrovue/latest/ferrovue/guide/provide_inject/index.html) guide |
220
- | 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; optional chaining `a?.b`; `.length`; `String()`, `.toString()`, `.toFixed()`, `Math.max`/`min`/`abs`/`round`/`floor`/`ceil`/`trunc`; `Number()`, `parseInt()` (no radix, 10 or 16), `parseFloat()`; `JSON.stringify()` of strings, numbers, booleans and lists of those; array literals |
221
- | Strings | `.trim()`, `.trimStart()`, `.trimEnd()`, `.toUpperCase()`, `.toLowerCase()`, `.includes()`, `.startsWith()`, `.endsWith()`, `.indexOf()`, `.lastIndexOf()`, `.slice()`, `.substring()`, `.at()`, `.charAt()`, `.split()`, `.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 [Strings](#strings)) |
222
- | 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)`, `.toISOString()`, `.getTime()`, `.valueOf()` and the `.getUTC…()` methods, invalid dates included, as JavaScript computes them. Prices and formatted dates are written in Rust and passed as strings, or filled into a `$t` message. See the crate's [`dates_and_prices`](https://docs.rs/ferrovue/latest/ferrovue/guide/dates_and_prices/index.html) guide |
223
- | Conditions | `v-if` / `v-else-if` / `v-else`, `?:`, `&&` and `||`, with optional and nullable values narrowed as TypeScript narrows them: `v-if="user"`, `user !== undefined`, `user !== null`, `user != null`, and `!user`, `user === undefined` or `user === null` for the `v-else` |
224
- | 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()` 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()`; chained and nested; a computed list read by `v-for`, `.join()`, `.length`, `.includes()`, kept by `computed`, or handed to a child |
230
+ | 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 |
231
+ | 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`](https://docs.rs/ferrovue/latest/ferrovue/guide/strings/index.html) guide) |
232
+ | 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 `||` (a date is always truthy, an invalid one too), of a date built in the template or of a `Date` prop, which an island's `data-props` carries as its ISO string and the client turns back into a `Date` before hydrating, and which may be handed to a child's `Date` prop. Prices and formatted dates are written in Rust and passed as strings, or filled into a `$t` message. See the crate's [`dates_and_prices`](https://docs.rs/ferrovue/latest/ferrovue/guide/dates_and_prices/index.html) guide |
233
+ | Conditions | `v-if` / `v-else-if` / `v-else`, `?:`, `&&` and `||`, with optional and nullable values narrowed as TypeScript narrows them: `v-if="user"`, `user !== undefined`, `user !== null`, `user != null`, `typeof user !== "undefined"`, and `!user`, `user === undefined` or `user === null` for the `v-else` |
234
+ | 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 |
225
235
  | 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 |
226
236
  | Attributes | static and bound attributes, boolean attributes, `:hidden`, `data-*` and `aria-*`, `v-bind` objects |
227
237
  | `class` | strings, arrays, objects (`{ active: on }`, computed keys), `cond && "x"`, `cond ? "x" : null`, merged with a static `class` |
@@ -231,7 +241,7 @@ ferrovue compiles `<script setup lang="ts">` components, and components with no
231
241
  | 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, `@value`s and keyframes included |
232
242
  | 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 |
233
243
  | `<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`](https://docs.rs/ferrovue/latest/ferrovue/guide/generated_code/index.html#component-is) guide |
234
- | 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 [Escape hatches](#escape-hatches) |
244
+ | 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`](https://docs.rs/ferrovue/latest/ferrovue/guide/errors_and_limits/index.html#client-only-content) guide |
235
245
  | 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`](https://docs.rs/ferrovue/latest/ferrovue/guide/generated_code/index.html#fallthrough-attributes) guide |
236
246
  | 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 |
237
247
  | Forms | `v-model` on text inputs, checkboxes, radios, `<select>` and `<textarea>` (renders the initial state) |
@@ -240,506 +250,63 @@ ferrovue compiles `<script setup lang="ts">` components, and components with no
240
250
  | 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 |
241
251
  | 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 |
242
252
  | `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`](https://docs.rs/ferrovue/latest/ferrovue/guide/escaping/index.html#where-v-html-may-go) guide |
243
- | 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 [Page head](#page-head) |
244
-
245
- Refused at compile time, each with an error that names the construct:
246
-
247
- - `<style module>` without `cssModules` in the configuration; a CSS module in another file, one a
248
- preprocessor compiles, or one that imports from another file; `v-bind()` in `<style>` of a value
249
- that is not a string or a number
250
- - `<RouterView>` in a component with `<style scoped>`, which would give the page that component's
251
- id; and, since vue-router renders a link from virtual nodes, a `<slot>` inside a `<RouterLink>`
252
- that takes scope ids, or an element inside one in slot content given a `:slotted()` id
253
- - `<component :is>` over an open set (a `string`, a `Component`, a key that is not a literal union),
254
- over a prop that may be absent, or naming a key its object lacks or a tag that is not an HTML
255
- element; `v-html` or `v-text` on it; and, inside an element it chooses (which Vue renders from
256
- virtual nodes), a `<slot>` with fallback content, `v-show`, and `v-model` on a `<select>`
257
- - in provide and inject: a key that is neither a string literal nor an `InjectionKey` symbol exported
258
- from a `.ts` file, `inject(key)!`, a value that may be `null` or `undefined`, values of different
259
- types (or a ref and a plain value) under one key, setup that assigns to an injected value,
260
- `provide` in a component holding `<RouterView>`, a string key injected inside a Rust twin's slot,
261
- and `provide(…)` not imported from `vue`
262
- - `v-show`, and `v-model` on a `<select>`, in the content of a `<RouterLink>` or of a Rust twin's
263
- slot, which Vue renders from virtual nodes
264
- - in the page head: options given to `useHead`, `useHeadSafe`, `templateParams`, a `titleTemplate`
265
- function, event handlers (`onload`), a `class` or `style` that may be `null` (on which unhead's
266
- renderer throws), and an object or array given to a `useSeoMeta` key
267
- - the Options API (a `<script>` without `setup`), and a type parameter of a generic component with
268
- no constraint (`generic="T"`)
269
- - constants that are not literals (`Date.now()`, a function), an object constant read whole or by a
270
- key chosen at run time (`LABELS[key]`), and `new` of anything but `Date` (`new Map()`)
271
- - the current time (`new Date()`, `Date.now()`), `new Date(year, month, …)`, a date string outside
272
- ECMAScript's date time string format or with a time and no `Z` or offset, which browsers may read
273
- differently (one from the props panics as it renders), the local getters (`.getFullYear()`,
274
- `.getHours()`), `.toString()` and the other `Date` methods; and, permanently, locale formatting:
275
- `Intl`, `.toLocaleString()`, `.toLocaleDateString()`, `.toLocaleTimeString()`, `.localeCompare()`
276
- and vue-i18n's `$d` and `$n`, whose output differs between Node and each browser
277
- - `<RouterLink custom>`, slot props that are array literals, defaults in destructured slot props, and outlets of one slot that pass different props
278
- - custom directives not listed in `clientDirectives`
279
- - `watchEffect`, `watch` with `immediate`, `onServerPrefetch`, top-level `await`, and statements in setup that change state
280
- - `route.meta` and `route.matched`
281
- - in a folder of pages: a parameter beside text in one part of a path (`prefix-[id].vue`), repeatable
282
- and optional catch-all parameters, parameter parsers (`[id=int]`), named views (`index@aside.vue`),
283
- `definePage()`, a `<route>` block, and two pages, or a page and a component, with one component name
284
- - Pinia getters that read `this` or return a function
285
- - ordering comparisons between a string and a number, which JavaScript makes numeric
286
- - regular expressions (`.replace(/x/g, …)`, `.split(/,/)`), replacement functions, a search's
287
- starting position or a split's limit (`.includes(x, 3)`, `.split(",", 2)`), `.toLocaleUpperCase()`
288
- and `.toLocaleLowerCase()` (the server's locale is not the browser's), `parseInt` with another radix
289
- or of a number, `.repeat()` by a negative literal
290
- - array methods given anything but `Boolean` or an arrow function whose body is an expression
291
- (`.map(String)`, `x => { return … }`), `.map()` to optional values, methods other than
292
- `.filter(Boolean)` over a list of optional values, a computed list as a slot prop
293
- - a dictionary's field read by name (`r.key`, `r[key]`), which may be absent although TypeScript says
294
- it is not; `Object.entries()` anywhere but as a `v-for`'s source; dictionaries of optional values
295
- - two strings that may each hold half of a surrogate pair compared, searched or joined (see
296
- [Strings](#strings))
297
- - a type that is both `null` and `undefined` (`T | null | undefined`, `x?: T | null`), a default
298
- for a nullable prop, and `=== null` or `=== undefined` of a value that may be either (`a?.b` where
299
- `b` is nullable; `== null` tests both)
300
- - an attribute (`class` and `style` aside) bound to a value that may be neither a string, a number
301
- nor a boolean: a list, an object, or a `route.query` value, which is an array when its key is
302
- repeated. Vue's server renderer leaves the attribute out and hydration then sets it, silently;
303
- join a list (`.join(",")`), or narrow a query value with `typeof route.query.q === "string"`
304
- - any method call without a Rust twin
305
- - a value read from `$attrs` or `useAttrs()` (`$attrs.title`), which has no type; an attribute a
306
- component is passed that would reach a prop of the component (or `<RouterLink>`) its root passes it
307
- on to; `$attrs` in a component that is the root of one that may be handed scope ids, where it would
308
- hold them; an attribute named by a number, or bound to a `$route.query` value; a class merged by
309
- `v-bind` with a string that may equal the class before it, which Vue would write once; attributes
310
- passed to a component whose root is a `<Transition>` or `<KeepAlive>` around a `v-if`, which Vue's
311
- server drops but its client keeps
312
-
313
- Every error names the file, line and column, with the line quoted and a caret under the construct.
314
-
315
- An object prop handed to a child component is cloned. Its strings are `Cow`s, so borrowed ones
316
- cost nothing to copy.
317
-
318
- ### Constants
319
-
320
- A constant a component imports from a `.ts` file, or an `enum`, is evaluated when the component is
321
- compiled, as `types.rs` holds the shared types:
322
-
323
- ```ts
324
- // components/catalog.ts
325
- export const LABELS = { title: "Catalogue", empty: "Nothing yet" } as const;
326
- export const SIZES = ["sm", "md", "lg"] as const;
327
- export const SORTS = [
328
- { key: "name", label: "By name" },
329
- { key: "price", label: "By price" },
330
- ] as const;
331
- export enum Tone { Calm = "calm", Loud = "loud" }
332
- ```
333
-
334
- `{{ LABELS.title }}` and `v-if="tone === Tone.Loud"` are written into the page as the strings they
335
- are, and `v-for="s in SIZES"` walks an array literal. A list of objects becomes a `const` beside
336
- the shared types, with a type for its items (here `SortsItem`, or the interface the constant is
337
- declared with):
338
-
339
- ```rust
340
- pub const SORTS: &[SortsItem<'static>] = &[
341
- SortsItem { key: Cow::Borrowed("name"), label: Cow::Borrowed("By name") },
342
- SortsItem { key: Cow::Borrowed("price"), label: Cow::Borrowed("By price") },
343
- ];
344
- ```
345
-
346
- A constant is a literal, a list, an object, or another constant of the same file. One that is
347
- anything else (`Date.now()`, a function) is refused where the template reads it, naming the file
348
- and line it is declared on.
349
-
350
- ### Scoped styles
351
-
352
- A `<style scoped>` component's elements carry `data-v-<id>`, and its CSS is rewritten by the client
353
- build to select them. The server has to write the id the client build chose, which is not something
354
- the browser checks when it hydrates: a wrong id hydrates cleanly and leaves the styles unapplied. So
355
- ferrovue computes it as `@vitejs/plugin-vue` does (the first 8 hex digits of a SHA-256 of the
356
- `.vue` file's path from Vite's root, followed by its source unless only the path is hashed), and the
357
- two must be configured alike:
358
-
359
- | `@vitejs/plugin-vue` | `ferrovue.config.json` |
360
- |---|---|
361
- | `vite build` with the default options | `"scopeId": "filepath-source"` (the default) |
362
- | `features: { componentIdGenerator: "filepath" }` (any mode), or the dev server | `"scopeId": "filepath"` |
363
-
364
- Set `viteRoot` when Vite's root is not the directory holding `ferrovue.config.json`. The Vite
365
- plugin (`ferrovue/vite`) compares the two when a component has scoped styles: a build in which they
366
- differ fails, and the dev server warns.
367
-
368
- `v-bind()` in `<style>` sets CSS variables on the component's root, which Vue's server writes into
369
- its `style` and ferrovue writes there too. Their names follow the same id, and whether plugin-vue
370
- compiles for production (`vite build`) or development (the dev server): set `isProduction` to match,
371
- `true` by default. CSS modules (`<style module>`) take their class names from Vite's
372
- `css.modules.generateScopedName`: set it to a pattern, and `cssModules` in `ferrovue.config.json` to
373
- the same, and ferrovue computes each class as postcss-modules does in Vite. The Vite plugin compares
374
- both, and fails a build in which they differ. See the crate's
375
- [`scoped_styles`](https://docs.rs/ferrovue/latest/ferrovue/guide/scoped_styles/index.html) guide.
376
-
377
- The default is the plugin's production behaviour because the production build is the one readers
378
- get: with every option left alone, its styles apply. But plugin-vue hashes the path alone in its dev
379
- server, so with the default a page rendered during development carries other ids than the dev
380
- client and shows unstyled. The recommended setup is `componentIdGenerator: "filepath"` with
381
- `"scopeId": "filepath"`, as [`examples/fullstack`](https://github.com/Kinsmir/ferrovue/tree/main/examples/fullstack) does: the ids are then the
382
- same in development and production, and do not change, nor change the generated Rust, whenever a
383
- component's source does.
384
-
385
- A component a parent may hand ids to has a `render_scoped(…, attrs)` beside `render`, which
386
- generated parents call (for one a parent also passes attributes to, `attrs` is an `fv::Attrs`
387
- holding them and the ids). A component whose outlets pass a slot scope id (`:slotted()`) takes slot
388
- content that is given it: `Slot::slotted` for a slot, a third `&str` parameter for a scoped slot's
389
- closure. `render`, and content from Rust, need none of it: markup written from Rust carries no ids.
390
-
391
- Where Vue's own server render gives other ids than its client render, ferrovue writes the server's:
392
- a `:slotted()` component's slot fallback, which only the client gives the slot scope id, and a
393
- component with `inheritAttrs: false` that is another component's root, whose root only the client
394
- gives the ids that other component inherits.
395
-
396
- ### Strings
397
-
398
- String methods count as JavaScript counts: in UTF-16 code units, so `"🦀".length` is 2 and
399
- `"🦀 crab".slice(3)` is `"crab"`. Strings are ordered by code unit too, which puts every character
400
- from U+E000 to U+FFFF after one beyond U+FFFF. Each runtime routine is held to vectors recorded from
401
- JavaScript (`crates/*/tests/vectors/`).
402
-
403
- A JavaScript string can hold half of a surrogate pair (`"🦀".slice(0, 1)`, `.charAt(1)`,
404
- `.split("")`), and a Rust string cannot. ferrovue writes U+FFFD in its place, which is exactly what
405
- the page carries anyway: a server sends Vue's string as UTF-8, and UTF-8 writes each half as U+FFFD
406
- (`res.end`, `Buffer.from` and `TextEncoder` all do). Its length is the same. Where the half itself
407
- would decide the result, ferrovue refuses at compile time: two strings that may each hold a half
408
- compared or ordered, side by side (`a.slice(0, 1) + b.slice(1)`, a `.join("")`, a class object's
409
- names), one searched for in another (`.includes(a.charAt(0))`), repeated or padded where halves
410
- would join, ordered against anything but a literal below U+D800, or written by `JSON.stringify`
411
- (which escapes it) or into a `<RouterLink>` (whose encoding throws). Three corners remain, by design:
412
-
413
- - a string holding U+FFFD in the props compares equal to a half, or finds one, where JavaScript
414
- would tell them apart;
415
- - a half handed to a child component or a helper is U+FFFD there, which matters only if that
416
- component then compares or joins it as above;
417
- - Vue itself cannot hydrate such text cleanly: the browser reads U+FFFD where its own render holds
418
- the half, and reports a mismatch.
419
-
420
- Where JavaScript throws, the generated code panics, as Vue's render rejects: a negative or infinite
421
- `.repeat()` count, and a `.repeat()`, `.padStart()` or `.padEnd()` past the longest string V8 makes
422
- (2²⁹ − 24 code units).
423
-
424
- ### Dictionaries
425
-
426
- A `Record<string, T>` prop is a `ferrovue::Record`, built from pairs
427
- (`[("b", 1), ("10", 2)].into_iter().collect()`) or read from JSON. It holds its keys in the order a
428
- JavaScript object does, array indices (`"0"` to `"4294967294"`) first in numeric order, so `v-for`
429
- walks them as Vue does, and it is written back as JSON in that order, which the browser reads back
430
- the same.
431
-
432
- ### Translating
433
-
434
- With `i18n` configured, the generated `i18n` module holds every locale's messages. Build an `I18n` per
435
- request, in the reader's locale, and pass it to the components that translate:
436
-
437
- ```rust
438
- let i18n = generated::i18n::i18n("nl"); // falls back as configured
439
- page::render(&mut out, &props, &i18n);
440
- ```
441
-
442
- ### Escape hatches
443
-
444
- Some components will never compile: a component library's render functions, code that measures the
445
- page in `onMounted`. Two ways keep one of them from blocking the page around it.
446
-
447
- **`<ClientOnly>`**: the server writes its `#fallback` slot (or a comment, without one), and the
448
- browser hydrates that same fallback, then swaps in the default slot once mounted. Both sides render
449
- the fallback first, so this is exact by construction. ferrovue does not compile the default slot at
450
- all: any component, import or expression may go there.
451
-
452
- ```vue
453
- <script setup lang="ts">
454
- import { ClientOnly } from "ferrovue/client";
455
- import { VDataTable } from "vuetify/components";
456
-
457
- defineProps<{ rows: { name: string }[] }>();
458
- </script>
459
-
460
- <template>
461
- <ClientOnly>
462
- <VDataTable :items="rows" />
463
- <template #fallback><p class="loading">Loading the table…</p></template>
464
- </ClientOnly>
465
- </template>
466
- ```
467
-
468
- The swap needs Vue running on that part of the page, so put `<ClientOnly>` inside an island (or an
469
- app the client hydrates whole); elsewhere the fallback stays. `<ClientOnly>` takes no attributes.
470
-
471
- **Rust twins**: a component the server should render, written by you in Rust. List it under
472
- `twins` with the props a template may pass it and the slots it may fill:
473
-
474
- ```json
475
- {
476
- "twins": {
477
- "VBtn": {
478
- "rust": "crate::ui::v_btn",
479
- "props": { "label": "string", "size": "int?", "block": "bool" },
480
- "slots": ["default"]
481
- }
482
- }
483
- }
484
- ```
485
-
486
- A template then uses `<VBtn>` (or `<v-btn>`), imported from anywhere or registered globally, and the
487
- generated parent calls `crate::ui::v_btn` with the props, the slots (each a `ferrovue::Slot`) and
488
- `&ferrovue::Attrs`: the attributes it is given beyond its props and the scope ids its root takes.
489
- `twins.rs` beside the components holds `VBtnProps`, `VBtnSlots` and the signature your function must
490
- have. Prop types are those of helpers (`string`, `int?`, …); an absent `bool` is `false`, and a
491
- bare `block` is `true`, as Vue casts them.
492
-
493
- **Exactness is your responsibility.** ferrovue cannot check that `v_btn` writes the bytes Vue's
494
- server renderer writes for `VBtn`: its guarantee rests on your code here, as it does for helpers. Prove it with fixtures of the components that use the twin, held to Vue by the
495
- conformance suite (see [Testing against Vue](#testing-against-vue)). Slot content reaches the twin as a component's render function sees it
496
- (`<!--v-if-->` for an absent `v-if`, as Vue writes virtual nodes). The twin decides where it goes and
497
- whether fragment markers surround it, as the component does. `examples/fullstack` has a
498
- twin (`src/ui.rs`) and its fixtures (`fixtures/`).
499
-
500
- ### Teleports
501
-
502
- A component that renders a `<Teleport>` takes a `ferrovue::Teleports`. After rendering the page body,
503
- write what was teleported where each target is, as Vue's `ssrContext.teleports` is written:
504
-
505
- ```rust
506
- let teleports = ferrovue::Teleports::new();
507
- page::render(&mut body, &props, &teleports);
508
- let modals = teleports.get("#modals").unwrap_or_default(); // inside <div id="modals">
509
- ```
510
-
511
- As Vue recommends, teleport to a dedicated element such as `#modals`: the browser hydrates
512
- a target from its first node, and `body` also holds the app.
513
-
514
- ### Page head
515
-
516
- A component that calls `useHead` or `useSeoMeta` from `@unhead/vue` takes a `ferrovue::Head`, which
517
- collects the calls of the whole tree in the order Vue's server runs them. After rendering the body,
518
- write the head as unhead's server renderer writes it:
519
-
520
- ```rust
521
- let head = ferrovue::Head::new(); // createHead(): lang="en", charset and viewport; or Head::without_defaults()
522
- book_page::render(&mut body, &props, &head);
523
- let tags = head.render(); // head_tags, body_tags_open, body_tags, html_attrs, body_attrs
524
- let page = format!("<html{}><head>{}</head><body{}>{}{}{}</body></html>",
525
- tags.html_attrs, tags.head_tags, tags.body_attrs, tags.body_tags_open, body, tags.body_tags);
526
- ```
527
-
528
- On the client the head is unhead's: give the page's app its client head,
529
- `mountPage(layout, islands, { plugins: [createHead()] })` with `createHead` from
530
- `@unhead/vue/client`, and it takes over the tags the server wrote. The crate's
531
- [`head`](https://docs.rs/ferrovue/latest/ferrovue/guide/head/index.html) guide has the details.
532
-
533
- ### Scoped slots from Rust
534
-
535
- A scoped slot's `Slots` field takes a closure that receives the props its outlet passes, as a
536
- generated `…SlotProps` struct borrowed for the render. The closure returns whether it wrote content;
537
- returning `false` shows the slot's fallback, as empty content does in Vue.
538
-
539
- ```rust
540
- let row = |out: &mut String, p: &data_list::RowSlotProps<'_>| {
541
- ferrovue::escape_into(out, p.label);
542
- true
543
- };
544
- data_list::render(&mut page, &props, data_list::Slots { row: Some(&row), ..Default::default() });
545
- ```
546
-
547
- ### Hydrating islands
548
-
549
- `mountIslands` from `ferrovue` hydrates every `data-island` element on the page with the
550
- component of that name, from the props the server wrote. With the Vite plugin, `ferrovue/islands`
551
- gives it every island there is, so nothing has to be listed by hand:
552
-
553
- ```ts
554
- import { hydrateState, mountIslands } from "ferrovue";
555
- import islands from "ferrovue/islands"; // written by `ferrovue()` from `ferrovue/vite`
556
-
557
- hydrateState(pinia);
558
- await mountIslands(islands, { pinia, router }); // one Pinia and one router for every island
559
- ```
560
-
561
- The islands are exactly the components that have an `island()`: those that render from their props
562
- alone, as the compiler finds while it generates them. That is the only set the server can write
563
- `data-island` for, so it needs no folder convention or list to keep in step, and making a component
564
- an island is a matter of the server calling its `island()`. Each is keyed by its `NAME`, the file
565
- name without `.vue`, which is what `data-island` carries.
566
-
567
- Each entry is a lazy `import()`, so the bundler gives every island a chunk of its own and a page
568
- fetches the code of the islands it holds and no more. `mountIslands` takes such loaders and
569
- components alike, so a hand-written island can join them: `mountIslands({ ...islands, Chart })`.
570
- An island whose name is not among them is left as the server rendered it, with a warning naming it
571
- (`onError` to handle it otherwise), as is one whose component fails to load.
572
-
573
- An island can wait to hydrate, and to fetch its code, until it is needed. `hydrate` on the
574
- server's `island()` writes a `data-hydrate` attribute on the wrapper, which `mountIslands` honours
575
- with Vue's lazy hydration strategies:
576
-
577
- ```rust
578
- use ferrovue::Hydrate;
579
-
580
- reviews::island(&props).hydrate(Hydrate::Visible); // scrolled into view
581
- footer::island(&props).hydrate(Hydrate::visible_with("200px")); // within 200px of the view
582
- chart::island(&props).hydrate(Hydrate::Idle); // the browser is idle
583
- counter::island(&props).hydrate(Hydrate::Interaction); // pointerenter, click or focus
584
- menu::island(&props).hydrate(Hydrate::InteractionOn(&["keydown"]));
585
- sidebar::island(&props).hydrate(Hydrate::media("(min-width: 60rem)"));
586
- ```
587
-
588
- The island's markup is unchanged, so nothing about exactness changes. An event that woke an island
589
- waiting for interaction is dispatched again once it has hydrated, so the first click is not lost.
590
- An island without `data-hydrate` hydrates at once, as before.
591
-
592
- An island's `<style>` goes into its chunk too, and Vite links it only once the island's script has
593
- loaded, after the server's markup is on screen. Link those stylesheets from the page up front: a
594
- server reading Vite's manifest takes the `css` of the entry's `dynamicImports` as well as its own
595
- (`examples/fullstack/src/assets.rs`).
596
-
597
- An application whose own navigation layer leaves the page can keep `<RouterLink>` for its `href`s
598
- and active classes with `linkRouter(routes, { navigate })` from `ferrovue/link-router`: a router
599
- over the routes file (or the default export of `ferrovue/routes`, with a folder of pages) whose
600
- every navigation after the first goes to `navigate`. The routing guide
601
- (`ferrovue::guide::routing`) explains each part of it.
602
-
603
- ### Hydrating a whole page
604
-
605
- A page built from a layout whose slots hold components, each rendered from its own props, can be
606
- hydrated as one app. `ferrovue::Page` takes each slot's parts as `Part`s, made from the same
607
- `html()` values that write the markup, renders the layout, and writes what it rendered as a
608
- `<script type="application/json">`; `mountPage` reads it back, loads the components it names and
609
- hydrates the layout with each slot's parts:
610
-
611
- ```rust
612
- let mut page = ferrovue::Page::new();
613
- let content = page.slot("default", books.iter().map(|b| Part::new(pick::NAME, pick::html(b))));
614
- let reviews = page.hole("reviews"); // filled later, while the page streams
615
- let record = page.render_to(&mut out, picks::html(&props, picks::Slots {
616
- default: Some(content.slot()),
617
- reviews: Some(reviews.slot()),
618
- }, &route));
619
- ```
620
-
621
- ```ts
622
- import { mountPage } from "ferrovue";
623
- import islands from "ferrovue/islands";
624
-
625
- await mountPage(() => import("./Picks.vue"), islands, { pinia, router });
626
- ```
627
-
628
- The record repeats the text of every prop, so a page hydrated this way is larger than one of
629
- islands; the guide's `islands_and_hydration` page compares the two, and `examples/fullstack` serves
630
- a page of each kind.
631
-
632
- An application that goes to the next page without a reload shows it from the record in the fetched
633
- document, with the same app:
634
-
635
- ```ts
636
- import { hydrateState, readPage, renderPage } from "ferrovue";
637
-
638
- const next = readPage(await (await fetch(href)).text()); // no DOMParser: works in a worker too
639
- const pinia = createPinia();
640
- hydrateState(pinia, { text: next.state });
641
- const router = linkRouter(routes, { navigate, location: href }); // at the next page's location
642
- current = await renderPage(() => import("./Picks.vue"), islands, next.record, { pinia, router, previous: current });
643
- ```
644
-
645
- `renderPage` mounts the next page's app in place of the container before it unmounts `previous`, so
646
- a router both use stays started. `renderRecordedPage` from `ferrovue/testing` checks that a page
647
- rendered from its record on the client shows what the server wrote, and `conformanceSuite` checks
648
- the same of every fixture.
649
-
650
- ### Hydrating Pinia state
651
-
652
- ```rust
653
- ferrovue::state_script_into(&mut page, "__pinia", &stores); // a <script type="application/json">
654
- ```
655
-
656
- ```ts
657
- import { hydrateState } from "ferrovue";
658
- hydrateState(pinia); // before app.mount(): every store starts from what the server rendered
659
- ```
660
-
661
- ### Fixtures in an application's vitest run
662
-
663
- `fixtureApp` from `ferrovue/testing` installs Pinia for a fixture's `$stores`, vue-router for its
664
- routes and vue-i18n for its `i18n` option, importing each itself. A component's `useBasket()`,
665
- `useRoute()` or `useI18n()` reads the instance its own import of the package gives it, so the two
666
- must be one module instance. vitest loads `ferrovue/testing` from `node_modules` with Node, and
667
- runs the application's code through Vite; where a peer goes through Vite too (listed in
668
- `server.deps.inline`, aliased, linked from elsewhere, or installed twice), the fixture installs a
669
- different instance from the one the component reads, and the render fails with Pinia's "there was
670
- no active Pinia" or an undefined route. Pass the application's own modules in the options:
671
-
672
- ```ts
673
- const app = await fixtureApp(component, readFixture(json), routes, {
674
- pinia: await import("pinia"),
675
- vueRouter: await import("vue-router"),
676
- vueI18n: await import("vue-i18n"),
677
- });
678
- ```
679
-
680
- Without them, `fixtureApp` imports the peers itself, as before. Another way is to have vitest run
681
- ferrovue through Vite as well, which puts it on the application's module graph; this also covers
682
- `vue` itself, which `fixtureApp` always imports:
683
-
684
- ```ts
685
- // vitest.config.ts
686
- export default defineConfig({ test: { server: { deps: { inline: ["ferrovue"] } } } });
687
- ```
688
-
689
- ### Streaming with holes
690
-
691
- `ferrovue::hole()` is a slot whose content you write later. Render a layout with holes,
692
- `split_holes` the output, and stream the pieces with each hole's content between them, in whatever
693
- order the content is ready. With the `stream` feature (which `axum` and `actix-web` turn on),
694
- `ferrovue::HtmlStream` is that streamed body, from the render and a future for each hole's content:
695
-
696
- ```rust
697
- let body = ferrovue::HtmlStream::new(page).hole(async move { reviews_of(&id).await });
698
- ```
699
-
700
- ### Testing against Vue
701
-
702
- An application holds its own components to Vue as ferrovue's suite does, with a fixtures directory
703
- and two calls. A fixture is a component's props as JSON (plus `$slots`, `$route`, `$stores` and
704
- `$locale` where it takes them) beside the HTML Vue rendered for them:
705
-
706
- ```text
707
- fixtures/Reviews/three.json {"reviews":[{"reader":"Ada","stars":5,"text":"The spice must flow."}]}
708
- fixtures/Reviews/three.html recorded from Vue with FERROVUE_FIXTURES_WRITE=1
709
- ```
710
-
711
- ```ts
712
- // test/conformance.test.ts, run by vitest with environment: "happy-dom"
713
- import { join } from "node:path";
714
- import type { Component } from "vue";
715
- import { conformanceSuite } from "ferrovue/testing";
716
-
717
- await conformanceSuite({
718
- config: join(import.meta.dirname, "../ferrovue.config.json"),
719
- components: import.meta.glob<Component>("../client/components/*.vue", { eager: true, import: "default" }),
720
- pinia: await import("pinia"),
721
- vueRouter: await import("vue-router"),
722
- });
723
- ```
724
-
725
- ```rust
726
- ferrovue::conformance!("fixtures", generated::render_json, at_least = 24);
727
- ```
728
-
729
- The Vue half fails on a component with no fixtures, a fixture naming no component, a project with
730
- no components, generated Rust that differs from what `ferrovue` writes now, a fixture Vue renders
731
- differently from its `.html`, and a recorded `.html` that hydrates with any warning;
732
- `FERROVUE_FIXTURES_WRITE=1` records the `.html` files instead. The Rust half renders every fixture through the generated `render_json`,
733
- compares the bytes, and fails when fewer than `at_least` fixtures are found. The crate's
734
- [`testing`](https://docs.rs/ferrovue/latest/ferrovue/guide/testing/index.html) guide has the
735
- details; `examples/fullstack` uses both.
253
+ | 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`](https://docs.rs/ferrovue/latest/ferrovue/guide/head/index.html) guide |
254
+
255
+ Anything else is refused at compile time, with an error that names the file, line and column,
256
+ quotes the line and puts a caret under the construct. The
257
+ [`errors_and_limits`](https://docs.rs/ferrovue/latest/ferrovue/guide/errors_and_limits/index.html#refused-at-compile-time)
258
+ guide lists what is refused and why, and
259
+ [`error_codes`](https://docs.rs/ferrovue/latest/ferrovue/guide/error_codes/index.html) every code.
260
+
261
+ ## Guides
262
+
263
+ The crate's guide, `ferrovue::guide` on docs.rs, covers each topic in depth:
264
+
265
+ - [Quick start](https://docs.rs/ferrovue/latest/ferrovue/guide/quick_start/index.html) and
266
+ [the generated code](https://docs.rs/ferrovue/latest/ferrovue/guide/generated_code/index.html):
267
+ what the compiler writes, and a component's `Props`, `render`, `html` and `island`
268
+ - [Props](https://docs.rs/ferrovue/latest/ferrovue/guide/props/index.html): TypeScript types in
269
+ Rust, dictionaries, shared types and [constants](https://docs.rs/ferrovue/latest/ferrovue/guide/props/index.html#constants)
270
+ - [Slots](https://docs.rs/ferrovue/latest/ferrovue/guide/slots/index.html), scoped slots from Rust
271
+ included
272
+ - [Scoped styles](https://docs.rs/ferrovue/latest/ferrovue/guide/scoped_styles/index.html):
273
+ `<style scoped>` ids that match `@vitejs/plugin-vue`, `v-bind()` in `<style>`, CSS modules
274
+ - [Routing](https://docs.rs/ferrovue/latest/ferrovue/guide/routing/index.html),
275
+ [translating with vue-i18n](https://docs.rs/ferrovue/latest/ferrovue/guide/i18n/index.html),
276
+ [Pinia](https://docs.rs/ferrovue/latest/ferrovue/guide/pinia/index.html) and
277
+ [provide and inject](https://docs.rs/ferrovue/latest/ferrovue/guide/provide_inject/index.html)
278
+ - [Teleports](https://docs.rs/ferrovue/latest/ferrovue/guide/teleports/index.html) and
279
+ [the page head](https://docs.rs/ferrovue/latest/ferrovue/guide/head/index.html) (`useHead`,
280
+ `useSeoMeta`)
281
+ - [Islands and hydration](https://docs.rs/ferrovue/latest/ferrovue/guide/islands_and_hydration/index.html):
282
+ islands, lazy hydration, a page hydrated whole, navigating between pages
283
+ - [Streaming](https://docs.rs/ferrovue/latest/ferrovue/guide/streaming/index.html) with holes,
284
+ [web frameworks](https://docs.rs/ferrovue/latest/ferrovue/guide/web_frameworks/index.html) (axum,
285
+ actix-web) and [Dioxus](https://docs.rs/ferrovue/latest/ferrovue/guide/dioxus/index.html)
286
+ - [Numbers](https://docs.rs/ferrovue/latest/ferrovue/guide/numbers/index.html),
287
+ [strings](https://docs.rs/ferrovue/latest/ferrovue/guide/strings/index.html) and
288
+ [dates and prices](https://docs.rs/ferrovue/latest/ferrovue/guide/dates_and_prices/index.html):
289
+ JavaScript's semantics in Rust
290
+ - [Escaping](https://docs.rs/ferrovue/latest/ferrovue/guide/escaping/index.html) and
291
+ [security](https://docs.rs/ferrovue/latest/ferrovue/guide/security/index.html)
292
+ - [Testing against Vue](https://docs.rs/ferrovue/latest/ferrovue/guide/testing/index.html): your
293
+ own components held to Vue with fixtures and two calls
294
+ - [Errors and limits](https://docs.rs/ferrovue/latest/ferrovue/guide/errors_and_limits/index.html),
295
+ with helpers, `<ClientOnly>` and Rust twins, and every
296
+ [error code](https://docs.rs/ferrovue/latest/ferrovue/guide/error_codes/index.html)
297
+ - Upgrading a project [from 0.8 to 0.9](https://docs.rs/ferrovue/latest/ferrovue/guide/upgrading_to_0_9/index.html)
298
+ and [from 0.7 to 0.8](https://docs.rs/ferrovue/latest/ferrovue/guide/upgrading_to_0_8/index.html)
299
+
300
+ [`examples/fullstack`](https://github.com/Kinsmir/ferrovue/blob/main/examples/fullstack/README.md)
301
+ is a complete app to copy: an axum server, a Vite client, islands, Pinia, vue-router and streaming,
302
+ with a test that it hydrates. [CONTRIBUTING.md](https://github.com/Kinsmir/ferrovue/blob/main/CONTRIBUTING.md)
303
+ describes the repository and how to work on it.
736
304
 
737
305
  ## Performance
738
306
 
739
- Time to render one component to an HTML string. On the left is Vue's `renderToString` on Node, and
740
- on the right is the Rust that ferrovue generated from the same `.vue` file. Both get the same props,
741
- and both write the same bytes: each side checks its output against
742
- `crates/ferrovue/benches/expected/<scenario>.html` before anything is timed.
307
+ Time to render one component to an HTML string: Vue's `renderToString` on Node, and the Rust that
308
+ ferrovue generated from the same `.vue` file, given the same props. Each side checks its output
309
+ against `crates/ferrovue/benches/expected/<scenario>.html` before anything is timed.
743
310
 
744
311
  | Scenario | What renders | Output | Vue `renderToString` | ferrovue | Speed-up |
745
312
  |---|---|---|---|---|---|
@@ -748,41 +315,24 @@ and both write the same bytes: each side checks its output against
748
315
  | `tree` | `Tree`: a recursive component, binary tree 8 levels deep (255 nodes) | 9.5 KiB | 522.2 µs | 3.86 µs | 135.2× |
749
316
  | `page` | `Dashboard`: 22 `Panel`s with named slots, a `Text`, 20 `Frame`s holding loops | 8.0 KiB | 181.3 µs | 5.06 µs | 35.8× |
750
317
 
751
- These are mean times per render. The Vue column is tinybench's mean, with 5 s per scenario after
752
- a 1 s warm-up. The ferrovue column is criterion's mean point estimate, using its defaults of a 3 s
753
- warm-up and 100 samples.
754
-
755
- What each side measures:
756
-
757
- - **Vue**: one awaited `renderToString(app)`. Vue is the production build (`NODE_ENV=production`).
758
- Each component uses the `ssrRender` its SSR build would have. The app is created once outside
759
- the loop, along with its Pinia instance and, for `small`, a vue-router already at `/users/me`.
760
- - **ferrovue**: a new `String` plus one call of the generated `render`, built with the release
761
- profile. The props, the router and the `Route` are built once outside the loop.
318
+ Mean times per render. Vue: tinybench's mean of one awaited `renderToString(app)`, 5 s per
319
+ scenario after a 1 s warm-up, the production build, each component with its SSR build's
320
+ `ssrRender`, the app, Pinia and (for `small`) a vue-router at `/users/me` created outside the loop.
321
+ ferrovue: criterion's mean point estimate (3 s warm-up, 100 samples) of a new `String` and one call
322
+ of the generated `render`, release profile, the props, router and `Route` built outside the loop.
762
323
 
763
324
  Measured on an AMD Ryzen 5 3600XT (6 cores / 12 threads, up to 3.8 GHz) with 62 GiB of RAM, running
764
- Linux 7.2.8-2-cachyos (CachyOS, x86_64). Software: Node 26.10.0, Vue 3.5.43, rustc 1.99.0. The
765
- machine was lightly loaded (a load average of about 1) but not idle, so treat the numbers as
766
- indicative.
767
-
768
- The speed-up says as much about Vue as about ferrovue. Most of Vue's time goes on component
325
+ Linux 7.2.8-2-cachyos (CachyOS, x86_64), Node 26.10.0, Vue 3.5.43 and rustc 1.99.0, lightly loaded
326
+ (a load average of about 1): treat the numbers as indicative. Most of Vue's time goes on component
769
327
  instances and their virtual nodes, of which `tree` has 255 and `list` one: per byte written, Vue
770
328
  renders `list` some fourteen times faster than `tree`, while ferrovue writes the two at a similar
771
329
  rate.
772
330
 
773
- To reproduce:
774
-
775
- ```sh
776
- pnpm bench # both halves: pnpm bench:rust, then pnpm bench:js
777
- pnpm bench:rust # cargo bench -p ferrovue --bench render (crates/ferrovue/benches/render.rs)
778
- pnpm bench:js # Vue's renderToString with tinybench (packages/ferrovue/bench/ssr.bench.ts)
779
- pnpm bench:record # rewrite benches/expected/*.html from Vue, after changing a scenario
780
- ```
781
-
782
- The benchmarks are not part of `pnpm test`. CI runs both in a short mode only to check that they
783
- still build, run and agree on the HTML. It never compares their timings. What CI does hold to a
784
- budget is the number of instructions each scenario runs, within 5% of a committed baseline, and
785
- the time ferrovue and `cargo build` take on a project of 500 components; [TESTING.md](https://github.com/Kinsmir/ferrovue/blob/main/TESTING.md#performance-budgets)
331
+ `pnpm bench` runs both halves (`pnpm bench:rust`, then `pnpm bench:js`), and `pnpm bench:record`
332
+ rewrites `benches/expected/*.html` from Vue after a scenario changes. CI runs both in a short mode
333
+ to check they build, run and agree on the HTML, and holds the instructions each scenario runs to
334
+ within 5% of a committed baseline, and the time ferrovue and `cargo build` take on a project of 500
335
+ components to a budget; [TESTING.md](https://github.com/Kinsmir/ferrovue/blob/main/TESTING.md#performance-budgets)
786
336
  describes both.
787
337
 
788
338
  ## Versions
@@ -806,109 +356,10 @@ before it reaches you; a ferrovue patch follows when one does.
806
356
 
807
357
  ### Deprecations
808
358
 
809
- Something ferrovue stops supporting is deprecated first, for at least one minor release, and
810
- removed in the next major release. A deprecated item of the crates carries `#[deprecated]`, so the
811
- Rust compiler warns where it is used. A deprecated configuration key warns with
812
- `warning[FV1117]` and a deprecated command-line option with `warning[FV1118]`, each naming what
813
- replaces it, and the JSON Schema marks the key deprecated for editors. Each release's CHANGELOG lists
814
- what it deprecates under "Deprecated".
815
-
816
- ## Repository layout
817
-
818
- ```text
819
- crates/ferrovue/ the Rust runtime crate, which generated code calls
820
- src/ a module per part (`Html`, slots, class and style, the state script,
821
- strings, teleports, the page head, fallthrough attributes, `BasicHtml`
822
- and `InlineHtml`,
823
- `Sanitised`, the fixture check `conformance!` writes), each module's
824
- unit tests beside it in <module>/tests.rs; lib.rs re-exports them all,
825
- and the three crates below
826
- docs/guide/ the crate guide, `ferrovue::guide` on docs.rs
827
- tests/conformance/ components, fixtures, recorded HTML, generated Rust
828
- tests/literal_props/ components compiled with `"builders": false`
829
- tests/sanitised/ a component whose `TrustedHtml` is `ferrovue::Sanitised`
830
- tests/vectors/ vectors recorded from JavaScript and Vue
831
- tests/properties.rs property-based tests of the runtime
832
- benches/ criterion and instruction-count benchmarks of generated renderers
833
- crates/ferrovue-core/ escaping and JavaScript's numbers, which the other crates share
834
- tests/vectors/ vectors recorded from escapeHtml, String(n), Math and toFixed
835
- crates/ferrovue-router/ vue-router's matching and links (the `router` feature)
836
- tests/vectors/ vectors recorded from vue-router
837
- crates/ferrovue-i18n/ vue-i18n's t() (the `i18n` feature)
838
- crates/ferrovue-contract/ unpublished: Rust using every item of the generated-code contract,
839
- and the version check's message (trybuild)
840
- packages/ferrovue/ the compiler (npm package)
841
- src/index.ts `ferrovue`: the browser API, `mountIslands`, `mountPage`, `readPage`, `renderPage`, `hydrateState` and the types
842
- src/compiler.ts `ferrovue/compiler`: `generate`, `write`
843
- src/context.ts, model.ts `ferrovue.config.json`, and the types and values the compiler passes around
844
- src/schema.ts the configuration's keys, types and defaults, from which `schema.json` is written
845
- src/component.ts, script.ts, typescript.ts, constants.ts
846
- a `.vue` file read, <script setup>, TypeScript types, constants and enums
847
- src/template.ts, children.ts, slots.ts, loops.ts, vhtml.ts
848
- the compiled template: statements, child components, slots, v-for,
849
- where `v-html` may go
850
- src/expr.ts, strings.ts, numbers.ts, narrowing.ts, calls.ts, lists.ts, parens.ts
851
- expressions: operators, strings, numbers, narrowing, calls, lists,
852
- the parentheses Rust needs
853
- src/attrs.ts, classes.ts, styles.ts, fallthrough.ts
854
- attributes, class and style, attributes a parent passes on
855
- src/dynamic.ts `<component :is>` over a closed set of choices
856
- src/plugin.ts the plugin interface (see CONTRIBUTING.md)
857
- src/plugins/ vue-router, Pinia, vue-i18n, scoped styles, <Teleport>, shared types,
858
- <ClientOnly>, Rust twins, provide and inject, the page head
859
- src/rust.ts, emitter.ts the Rust source written out
860
- src/cli.ts, args.ts, vite.ts
861
- the `ferrovue` command, its options, and the Vite plugin
862
- src/diff.ts the diff `ferrovue --check --diff` prints for a stale file
863
- src/errors.ts, diagnostics.ts
864
- every error code with its title, and errors as the CLI, JSON and Vite show them
865
- src/volar.ts, editor.ts `ferrovue/volar`: errors in the editor, from a plugin for Vue's language tools
866
- src/client.ts browser-side helpers: `mountIslands`, `mountPage`, `readPage`, `renderPage`, `hydrateState`, `<ClientOnly>`
867
- src/islands.ts `ferrovue/islands`, which the Vite plugin writes: every island, loaded lazily
868
- src/link-router.ts `ferrovue/link-router`: `<RouterLink>` while the application navigates on its own
869
- src/routes.ts a routes file as vue-router's route records
870
- src/file-routes.ts the routes vue-router's file-based routing builds from a folder of pages
871
- src/page-routes.ts `ferrovue/routes`, which the Vite plugin writes from the pages
872
- src/testing.ts `ferrovue/testing`: utilities for a project's own conformance suite
873
- src/fixture.ts, settle.ts `fixtureApp` and `readFixture`, and waiting for async components to settle
874
- src/conformance.ts `conformanceSuite`, which `ferrovue/testing` exports
875
- src/ssr.ts `attachSsrRender`, which `ferrovue/testing` exports
876
- src/hydration.ts `hydrateRecordedPage` and `renderRecordedPage`, which `ferrovue/testing` exports
877
- src/client-render.ts the DOM of a client render as it is compared with the server's markup
878
- src/types.ts `ferrovue/types`: `TrustedHtml`, `InlineHtml`, `Float`
879
- test/ compiler, CLI, router, vector, island, Vite and conformance tests
880
- api/ the package's API report, an `.api.md` per export and `cli.txt` (`pnpm api:report`)
881
- schema.json the JSON Schema of `ferrovue.config.json` (`pnpm schema:generate`)
882
- browser/ conformance fixtures and recorded pages hydrated in real browsers (`pnpm test:browser`)
883
- bench/ Vue renderToString benchmarks, the other half of Performance
884
- fuzz/ the randomised differential tester (`pnpm fuzz`)
885
- examples/greeting/ the smallest setup: one component rendered from Rust
886
- examples/fullstack/ axum + Vite: islands (some hydrating later), a page hydrated whole,
887
- Pinia state, routes from a folder of pages, the page head, streaming,
888
- <ClientOnly>, a Rust twin, and a conformance suite
889
- examples/dioxus/ a Dioxus page, rendered with dioxus-ssr, with an island in it
890
- fuzz/ cargo-fuzz targets for the runtime's escaping and JSON (see TESTING.md)
891
- scripts/ferrovue-in.ts the `ferrovue` command run in a project of this repository (`pnpm conformance:check`, CI)
892
- scripts/inspect.ts Vue's SSR code and render of a component beside the Rust ferrovue generates for it
893
- scripts/release.ts the release version bump (see RELEASING.md)
894
- scripts/api-report.ts the npm package's API report, written and checked (`pnpm api:report`, `pnpm api:check`)
895
- scripts/traceability.test.ts the README's "What a component may use" rows, traced to conformance components in TESTING.md
896
- ```
897
-
898
- ## Development
899
-
900
- ```sh
901
- pnpm install
902
- pnpm test # compiler, CLI and the Vue half of the conformance suite
903
- pnpm typecheck
904
- cargo test --all-features
905
- ```
906
-
907
- [TESTING.md](https://github.com/Kinsmir/ferrovue/blob/main/TESTING.md) explains how the suite fits together and how to add a case,
908
- [CONTRIBUTING.md](https://github.com/Kinsmir/ferrovue/blob/main/CONTRIBUTING.md) how to add support for a Vue construct, and
909
- [RELEASING.md](https://github.com/Kinsmir/ferrovue/blob/main/RELEASING.md) how a release is cut.
910
-
911
- [`examples/fullstack`](https://github.com/Kinsmir/ferrovue/blob/main/examples/fullstack/README.md) is a complete app to copy: an axum server, a Vite client, islands, Pinia, vue-router and streaming, with a test that it hydrates.
359
+ What ferrovue stops supporting is deprecated for at least one minor release and removed in the next
360
+ major release. A deprecated crate item carries `#[deprecated]`; a deprecated configuration key warns
361
+ with `warning[FV1117]` and a command-line option with `warning[FV1118]`, naming the replacement, and the JSON Schema marks a deprecated key for editors.
362
+ Each release's CHANGELOG lists what it deprecates under "Deprecated".
912
363
 
913
364
  ## Licence
914
365