ferrovue 0.8.0 → 0.10.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.
Files changed (86) hide show
  1. package/README.md +162 -697
  2. package/dist/args.js +1 -1
  3. package/dist/attrs.d.ts +2 -0
  4. package/dist/attrs.js +12 -4
  5. package/dist/blocks.d.ts +32 -0
  6. package/dist/blocks.js +476 -0
  7. package/dist/calls.d.ts +4 -0
  8. package/dist/calls.js +152 -15
  9. package/dist/children.js +18 -3
  10. package/dist/cli.js +2 -6
  11. package/dist/client-render.d.ts +4 -3
  12. package/dist/client-render.js +17 -4
  13. package/dist/client.d.ts +19 -0
  14. package/dist/client.js +51 -25
  15. package/dist/compiler.d.ts +14 -4
  16. package/dist/compiler.js +187 -35
  17. package/dist/component.js +12 -5
  18. package/dist/conformance.d.ts +1 -1
  19. package/dist/constants.d.ts +26 -3
  20. package/dist/constants.js +176 -20
  21. package/dist/context.d.ts +22 -1
  22. package/dist/context.js +12 -3
  23. package/dist/diagnostics.d.ts +11 -1
  24. package/dist/diagnostics.js +33 -4
  25. package/dist/dynamic.js +7 -2
  26. package/dist/editor.d.ts +2 -2
  27. package/dist/editor.js +14 -9
  28. package/dist/emitter.d.ts +1 -0
  29. package/dist/emitter.js +12 -0
  30. package/dist/errors.d.ts +519 -13
  31. package/dist/errors.js +1440 -322
  32. package/dist/expr.d.ts +11 -0
  33. package/dist/expr.js +273 -59
  34. package/dist/fixture.d.ts +4 -1
  35. package/dist/fixture.js +11 -3
  36. package/dist/functions.d.ts +14 -0
  37. package/dist/functions.js +172 -0
  38. package/dist/hydration.js +1 -1
  39. package/dist/index.d.ts +2 -1
  40. package/dist/index.js +2 -1
  41. package/dist/inputs.d.ts +2 -2
  42. package/dist/inputs.js +1 -1
  43. package/dist/lists.d.ts +14 -1
  44. package/dist/lists.js +581 -40
  45. package/dist/loads.d.ts +27 -0
  46. package/dist/loads.js +81 -0
  47. package/dist/loops.js +10 -3
  48. package/dist/model.d.ts +51 -3
  49. package/dist/model.js +162 -6
  50. package/dist/narrowing.d.ts +17 -0
  51. package/dist/narrowing.js +125 -12
  52. package/dist/numbers.d.ts +4 -0
  53. package/dist/numbers.js +53 -1
  54. package/dist/page-json.d.ts +1 -0
  55. package/dist/page-json.js +14 -0
  56. package/dist/plugin.d.ts +5 -1
  57. package/dist/plugins/client-only.js +1 -1
  58. package/dist/plugins/dates.d.ts +6 -0
  59. package/dist/plugins/dates.js +81 -5
  60. package/dist/plugins/head.js +2 -1
  61. package/dist/plugins/index.js +2 -1
  62. package/dist/plugins/loaders.d.ts +35 -0
  63. package/dist/plugins/loaders.js +349 -0
  64. package/dist/plugins/provide.js +5 -4
  65. package/dist/plugins/router-link.js +2 -2
  66. package/dist/plugins/router.js +4 -2
  67. package/dist/plugins/stores.js +7 -7
  68. package/dist/plugins/twins.js +1 -1
  69. package/dist/resolve.d.ts +32 -0
  70. package/dist/resolve.js +246 -0
  71. package/dist/rust.d.ts +1 -1
  72. package/dist/rust.js +39 -9
  73. package/dist/schema.d.ts +1 -1
  74. package/dist/schema.js +1 -0
  75. package/dist/script.d.ts +1 -1
  76. package/dist/script.js +64 -54
  77. package/dist/settle.js +8 -0
  78. package/dist/strings.d.ts +9 -1
  79. package/dist/strings.js +112 -22
  80. package/dist/template.js +162 -137
  81. package/dist/typescript.d.ts +12 -5
  82. package/dist/typescript.js +64 -34
  83. package/dist/vite.js +3 -3
  84. package/dist/volar.js +1 -1
  85. package/package.json +1 -1
  86. package/schema.json +6 -0
package/README.md CHANGED
@@ -1,14 +1,22 @@
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.
12
20
 
13
21
  ```text
14
22
  Button.vue ──(ferrovue compiler, Node)──▶ button.rs ──(your Rust server)──▶ HTML ──▶ Vue hydrates it
@@ -17,23 +25,19 @@ component's HTML with no JavaScript at run time, and the browser hydrates the re
17
25
  ferrovue doesn't interpret templates. It takes what `@vue/compiler-sfc` compiles a template to for
18
26
  server rendering (straight-line `_push` calls plus a few `@vue/server-renderer` helpers) and
19
27
  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:
28
+ element structure, attribute order, whitespace and fragment markers.
25
29
 
26
30
  | | What it is | Published as |
27
31
  |---|---|---|
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)) |
32
+ | `packages/ferrovue` | The compiler, the `ferrovue` command, the Vite plugin and the browser client (TypeScript) | npm: [`ferrovue`](https://www.npmjs.com/package/ferrovue) |
33
+ | `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
34
 
31
35
  ## Quick start
32
36
 
33
37
  ### 1. Install both halves
34
38
 
35
39
  ```sh
36
- pnpm add -D ferrovue # or npm / yarn
40
+ pnpm add -D ferrovue
37
41
  cargo add ferrovue serde --features serde/derive
38
42
  cargo add --dev serde_json # the generated conformance helper uses it under cfg(test)
39
43
  ```
@@ -43,11 +47,12 @@ let-chains, so the crate that includes it needs **edition 2024**.
43
47
 
44
48
  ### 2. Configure
45
49
 
46
- `ferrovue.config.json` at your project root:
50
+ `pnpm ferrovue init` writes a starter `ferrovue.config.json` and `components/Hello.vue`. Or write
51
+ the configuration at your project root yourself:
47
52
 
48
53
  ```json
49
54
  {
50
- "$schema": "https://cdn.jsdelivr.net/npm/ferrovue@0.7.0/schema.json",
55
+ "$schema": "https://cdn.jsdelivr.net/npm/ferrovue@0.8.0/schema.json",
51
56
  "components": "client/components",
52
57
  "out": "src/generated"
53
58
  }
@@ -67,14 +72,15 @@ read `$schema`. Offline, the copy in the installed package works as well:
67
72
  | `router` | no | Instead of `routes`: `{ routes, base?, linkActiveClass?, linkExactActiveClass? }`, matching `createWebHistory(base)` and `createRouter`'s options |
68
73
  | `stores` | no | Directory of Pinia option stores whose state components may read |
69
74
  | `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` |
75
+ | `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
76
  | `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) |
77
+ | `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
78
  | `i18n` | no | vue-i18n: `{ messages, locale?, fallbackLocale? }`, the directory of locale files (`en.json`, `nl.json`), the default locale and the fallbacks |
73
79
  | `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) |
80
+ | `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
81
  | `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) |
82
+ | `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) |
83
+ | `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
84
  | `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
85
 
80
86
  ### 3. Write a component
@@ -93,7 +99,6 @@ defineProps<{ name: string; unread: number; note?: string }>();
93
99
  ### 4. Generate and render
94
100
 
95
101
  ```sh
96
- pnpm ferrovue init # a starter ferrovue.config.json and components/Hello.vue
97
102
  pnpm ferrovue # writes src/generated/{greeting.rs, mod.rs}
98
103
  pnpm ferrovue --check # in CI: exits 1 if the committed modules are stale
99
104
  pnpm ferrovue --check --diff # …and shows what differs, as `diff -u` does
@@ -102,9 +107,10 @@ pnpm ferrovue --watch # regenerates when components or configs change
102
107
  ```
103
108
 
104
109
  `-c, --config <path>` reads another configuration file. `--watch` regenerates on a change to what a
105
- 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:
110
+ run reads (the configuration, the components, stores, locale messages, pages or routes file, the
111
+ `.ts` files components import types from, and the tsconfig files their aliases come from). A Vite
112
+ project can use the plugin instead, which does the same in its dev server and before a build, and
113
+ also resolves imports through the build's `resolve.alias`:
108
114
 
109
115
  ```ts
110
116
  // vite.config.ts
@@ -117,12 +123,13 @@ export default { plugins: [ferrovue({ config: "config/ferrovue.json" })] };
117
123
  default), and `config` the configuration file in it, as `--config` takes it (`ferrovue.config.json`
118
124
  by default).
119
125
 
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).
126
+ Every error carries a stable code, as `error[FV1805]: components/Card.vue:4:17: …`, documented in
127
+ the [`error_codes`](https://docs.rs/ferrovue/latest/ferrovue/guide/error_codes/index.html) guide.
128
+ A run reports every error it finds, each with a `= help:` line where the way around it is short,
129
+ then a line that counts them, and writes nothing while any remains. `--format json` writes the same
130
+ errors as JSON with the exit status unchanged; the
131
+ [quick start](https://docs.rs/ferrovue/latest/ferrovue/guide/quick_start/index.html#editor-and-ci-diagnostics)
132
+ has a VS Code problem matcher that puts them in the Problems panel.
126
133
 
127
134
  ```rust
128
135
  #[rustfmt::skip]
@@ -149,42 +156,41 @@ let mut page = String::from("<!doctype html><body>");
149
156
  greeting::render(&mut page, &props);
150
157
  ```
151
158
 
159
+ On the client, `mountIslands(islands)` from `ferrovue`, with `islands` from `ferrovue/islands`
160
+ (written by the Vite plugin), hydrates every island on the page; see
161
+ [islands and hydration](https://docs.rs/ferrovue/latest/ferrovue/guide/islands_and_hydration/index.html).
152
162
  With `routes` configured, the generated `route_table::router()` builds the router once. Resolve
153
163
  each request's location with `router.at(path)` and pass the result to the components that take a
154
164
  route.
155
165
 
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.
166
+ The crate's features:
162
167
 
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.
173
-
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:
177
-
178
- ```rust
179
- async fn hello(Path(name): Path<String>) -> impl IntoResponse {
180
- greeting::into_html(greeting::Props::new(name, 0))
181
- }
182
- ```
168
+ - `router` and `i18n`, both on by default: vue-router and vue-i18n. An application with neither
169
+ routes nor translations can leave them out, and build neither `ferrovue-router` nor
170
+ `ferrovue-i18n`: `ferrovue = { version = "0.8", default-features = false }`.
171
+ - `axum` and `actix-web`: `Html` is an `IntoResponse` or a `Responder`, so a handler responds with
172
+ the component as `text/html; charset=utf-8`. `into_html(props)` and `into_island(props)` take the
173
+ props by value, so a handler that builds them returns the page:
183
174
 
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).
175
+ ```rust
176
+ async fn hello(Path(name): Path<String>) -> impl IntoResponse {
177
+ greeting::into_html(greeting::Props::new(name, 0))
178
+ }
179
+ ```
180
+
181
+ - `stream`, which `axum` and `actix-web` turn on: `HtmlStream`, a page sent in the order its parts
182
+ are ready.
183
+ - `maud`: `ferrovue::Html` implements `maud::Render`, so `(greeting::html(&props))` goes straight
184
+ into a `maud::html!` page.
185
+ - `dioxus`: `{greeting::island(&props)}` in a Dioxus 0.7 `rsx!` is the island element itself, with
186
+ exactly the markup Vue hydrates inside it (`examples/dioxus`).
187
+ - `ammonia`: `ferrovue::Sanitised`, HTML cleaned by [ammonia](https://docs.rs/ammonia), for
188
+ `v-html`. `ferrovue::BasicHtml` and `ferrovue::InlineHtml` are built in; the
189
+ [`escaping`](https://docs.rs/ferrovue/latest/ferrovue/guide/escaping/index.html) guide describes
190
+ all three.
191
+
192
+ The crate's [feature flags](https://docs.rs/ferrovue/latest/ferrovue/index.html#feature-flags) list
193
+ what each adds.
188
194
 
189
195
  ### 5. Diagnostics in your editor
190
196
 
@@ -211,18 +217,21 @@ ferrovue compiles `<script setup lang="ts">` components, and components with no
211
217
 
212
218
  | Area | Supported |
213
219
  |---|---|
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` |
215
- | 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) |
220
+ | 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) |
221
+ | Shared types | Interfaces and type aliases imported from `.ts` files (generated once, into `types.rs`; imported by a relative path or through an alias the project declares, in its tsconfig.json's `compilerOptions.paths` or, with the Vite plugin, Vite's `resolve.alias`: `@/lib/format`), 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 |
222
+ | Constants | Constants imported from `.ts` files, object constants declared in `<script setup>`, 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`); lookup tables: an object constant (`as const`, `Record<Size, T>` or untyped, keys computed from enum members too) read by a key whose type is a union of string or integer literals or an `enum` (a prop, a `computed`, a `v-for` item's field, a `?:` of literals), compiled to a `match` over the members, read on by field (`THEME[tone].fg`) in text, attributes, `:class` and `:style`, with `undefined` for a member the object lacks (`ICONS[kind] ?? "none"`, `ICONS[kind]?.label`, `v-if="ICONS[kind]"`). See the crate's [`props`](https://docs.rs/ferrovue/latest/ferrovue/guide/props/index.html#constants) guide |
217
223
  | 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
- | 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 |
224
+ | Setup | `ref`/`shallowRef`, `computed` (an expression, or a body of statements: see Statements; 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, functions (see Functions), a plain `<script>` block beside setup. Lifecycle hooks, `watch` (not `immediate`), `defineEmits`, `defineSlots`, `defineOptions`, `defineExpose` and template refs (`ref(null)`, `useTemplateRef`) are client-only, as is a function only an event handler calls. The template may name them only from event handlers, which the server drops |
225
+ | Statements | in a `computed`'s body and in a list method's callback (`.map()`, `.filter()`, `.find()`, `.some()`, `.reduce()`, a `.toSorted()` comparator and the others): `const`, and `let` never assigned again, of any value the template may compute, destructured too (`const { name, posts: count } = author`); `if` / `else if` / `else` and early `return`, narrowing what follows as TypeScript narrows it (`if (!user) return ""; return user.name`, `if (!a \|\| !a.title) return "-"`); `switch` over a string or a number, with stacked `case`s, `default` and `break`. Each `const` is evaluated once, where JavaScript evaluates it, and only on the path that reaches it. Loops, assignments, `try`, `throw`, `var` and a path that ends without `return` are refused. See the crate's [`script_logic`](https://docs.rs/ferrovue/latest/ferrovue/guide/script_logic/index.html) guide |
226
+ | Functions | functions the template or a `computed` calls: declared in `<script setup>` (`function f(a: T): R { … }`, `const f = (a: T) => …`) or exported from a `.ts` file the component imports, whose bodies hold what a `computed`'s may (see Statements) and read props, refs, `computed`s, constants and other such functions (a `.ts` file's own constants, functions and imports); parameters by name, optional (`x?: T`), with defaults (`x = 1`, also after an earlier parameter: ``plural = `${word}s` ``) and as object patterns. Each call is translated where it is written, its arguments evaluated once and in order, with the types they have there, so one function serves an integer and a `Float`. A function imported from a `.ts` file needs no `helpers` entry. Recursion, `...rest`, `async` and generators are refused. See the crate's [`script_logic`](https://docs.rs/ferrovue/latest/ferrovue/guide/script_logic/index.html#functions) guide |
227
+ | Loaders | A top-level `await` in `<script setup>` of a loader declared with `defineLoader`, whose arguments are strings, numbers and booleans and whose result is any type a prop may have: the compiler writes a `Loaders` trait the Rust app implements, and the render stops where Vue's would until the result is in, starting siblings' loaders together. The results travel in the page for the client to hydrate from, inside a `<Suspense>` `mountPage` adds. See the crate's [`loaders`](https://docs.rs/ferrovue/latest/ferrovue/guide/loaders/index.html) guide |
219
228
  | 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 |
225
- | 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 |
229
+ | 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 `-`, `!`; `??`, `\|\|`, `&&`, `?:` (whose branches may differ in type where the value is only written out: in `{{ }}`, a string-valued attribute and a template literal, each branch is written as JavaScript writes it, `{{ n > 0 ? n : "none" }}`); 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 |
230
+ | 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) |
231
+ | 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 |
232
+ | 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` |
233
+ | 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 or a body of statements (see Statements), 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; `.flat()` of lists of lists, to a literal depth or `Infinity`, and `.flatMap()` with an arrow function that gives a list (flattened one level) or a value (kept as `.map()` keeps it); chained and nested; a computed list read by `v-for`, `.join()`, `.length`, `.includes()`, kept by `computed`, or handed to a child |
234
+ | 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)` (of a record of lists too, which `.flat()` joins), and `Object.entries(r)` read by the list methods, whose arrow functions destructure each pair (`([key, value]) => …`, `([, value]) => …`, `([key, { id }]) => …`) or read its parts (`pair[0]`, `pair[1]`), and walked by `v-for="[key, value] in Object.entries(r).filter(…)"`, kept by `computed` and sorted by a comparator of either part; handed to a child |
226
235
  | Attributes | static and bound attributes, boolean attributes, `:hidden`, `data-*` and `aria-*`, `v-bind` objects |
227
236
  | `class` | strings, arrays, objects (`{ active: on }`, computed keys), `cond && "x"`, `cond ? "x" : null`, merged with a static `class` |
228
237
  | `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 |
@@ -231,7 +240,7 @@ ferrovue compiles `<script setup lang="ts">` components, and components with no
231
240
  | 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
241
  | 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
242
  | `<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) |
243
+ | 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
244
  | 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
245
  | 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
246
  | Forms | `v-model` on text inputs, checkboxes, radios, `<select>` and `<textarea>` (renders the initial state) |
@@ -240,506 +249,69 @@ ferrovue compiles `<script setup lang="ts">` components, and components with no
240
249
  | 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
250
  | 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
251
  | `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.
252
+ | 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 |
253
+
254
+ Anything else is refused at compile time, with an error that names the file, line and column,
255
+ quotes the line and puts a caret under the construct. The
256
+ [`errors_and_limits`](https://docs.rs/ferrovue/latest/ferrovue/guide/errors_and_limits/index.html#refused-at-compile-time)
257
+ guide lists what is refused and why, and
258
+ [`error_codes`](https://docs.rs/ferrovue/latest/ferrovue/guide/error_codes/index.html) every code.
259
+
260
+ ## Guides
261
+
262
+ The crate's guide, `ferrovue::guide` on docs.rs, covers each topic in depth:
263
+
264
+ - [Quick start](https://docs.rs/ferrovue/latest/ferrovue/guide/quick_start/index.html) and
265
+ [the generated code](https://docs.rs/ferrovue/latest/ferrovue/guide/generated_code/index.html):
266
+ what the compiler writes, and a component's `Props`, `render`, `html` and `island`
267
+ - [Props](https://docs.rs/ferrovue/latest/ferrovue/guide/props/index.html): TypeScript types in
268
+ Rust, dictionaries, shared types and [constants](https://docs.rs/ferrovue/latest/ferrovue/guide/props/index.html#constants)
269
+ - [Slots](https://docs.rs/ferrovue/latest/ferrovue/guide/slots/index.html), scoped slots from Rust
270
+ included
271
+ - [Scoped styles](https://docs.rs/ferrovue/latest/ferrovue/guide/scoped_styles/index.html):
272
+ `<style scoped>` ids that match `@vitejs/plugin-vue`, `v-bind()` in `<style>`, CSS modules
273
+ - [Routing](https://docs.rs/ferrovue/latest/ferrovue/guide/routing/index.html),
274
+ [translating with vue-i18n](https://docs.rs/ferrovue/latest/ferrovue/guide/i18n/index.html),
275
+ [Pinia](https://docs.rs/ferrovue/latest/ferrovue/guide/pinia/index.html),
276
+ [provide and inject](https://docs.rs/ferrovue/latest/ferrovue/guide/provide_inject/index.html) and
277
+ [loaders](https://docs.rs/ferrovue/latest/ferrovue/guide/loaders/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
+ - [Porting an existing app](https://docs.rs/ferrovue/latest/ferrovue/guide/porting/index.html)
298
+ from a Node server that renders with Vue, and the
299
+ [support policy](https://docs.rs/ferrovue/latest/ferrovue/guide/support/index.html): supported
300
+ versions, fixes, deprecations and where to report a problem
301
+ - Upgrading a project [from 0.9 to 0.10](https://docs.rs/ferrovue/latest/ferrovue/guide/upgrading_to_0_10/index.html),
302
+ [from 0.8 to 0.9](https://docs.rs/ferrovue/latest/ferrovue/guide/upgrading_to_0_9/index.html)
303
+ and [from 0.7 to 0.8](https://docs.rs/ferrovue/latest/ferrovue/guide/upgrading_to_0_8/index.html)
304
+
305
+ [`examples/fullstack`](https://github.com/Kinsmir/ferrovue/blob/main/examples/fullstack/README.md)
306
+ is a complete app to copy: an axum server, a Vite client, islands, Pinia, vue-router and streaming,
307
+ with a test that it hydrates. [CONTRIBUTING.md](https://github.com/Kinsmir/ferrovue/blob/main/CONTRIBUTING.md)
308
+ describes the repository and how to work on it.
736
309
 
737
310
  ## Performance
738
311
 
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.
312
+ Time to render one component to an HTML string: Vue's `renderToString` on Node, and the Rust that
313
+ ferrovue generated from the same `.vue` file, given the same props. Each side checks its output
314
+ against `crates/ferrovue/benches/expected/<scenario>.html` before anything is timed.
743
315
 
744
316
  | Scenario | What renders | Output | Vue `renderToString` | ferrovue | Speed-up |
745
317
  |---|---|---|---|---|---|
@@ -748,41 +320,24 @@ and both write the same bytes: each side checks its output against
748
320
  | `tree` | `Tree`: a recursive component, binary tree 8 levels deep (255 nodes) | 9.5 KiB | 522.2 µs | 3.86 µs | 135.2× |
749
321
  | `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
322
 
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.
323
+ Mean times per render. Vue: tinybench's mean of one awaited `renderToString(app)`, 5 s per
324
+ scenario after a 1 s warm-up, the production build, each component with its SSR build's
325
+ `ssrRender`, the app, Pinia and (for `small`) a vue-router at `/users/me` created outside the loop.
326
+ ferrovue: criterion's mean point estimate (3 s warm-up, 100 samples) of a new `String` and one call
327
+ of the generated `render`, release profile, the props, router and `Route` built outside the loop.
762
328
 
763
329
  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
330
+ Linux 7.2.8-2-cachyos (CachyOS, x86_64), Node 26.10.0, Vue 3.5.43 and rustc 1.99.0, lightly loaded
331
+ (a load average of about 1): treat the numbers as indicative. Most of Vue's time goes on component
769
332
  instances and their virtual nodes, of which `tree` has 255 and `list` one: per byte written, Vue
770
333
  renders `list` some fourteen times faster than `tree`, while ferrovue writes the two at a similar
771
334
  rate.
772
335
 
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)
336
+ `pnpm bench` runs both halves (`pnpm bench:rust`, then `pnpm bench:js`), and `pnpm bench:record`
337
+ rewrites `benches/expected/*.html` from Vue after a scenario changes. CI runs both in a short mode
338
+ to check they build, run and agree on the HTML, and holds the instructions each scenario runs to
339
+ within 5% of a committed baseline, and the time ferrovue and `cargo build` take on a project of 500
340
+ components to a budget; [TESTING.md](https://github.com/Kinsmir/ferrovue/blob/main/TESTING.md#performance-budgets)
786
341
  describes both.
787
342
 
788
343
  ## Versions
@@ -804,111 +359,21 @@ the fixtures from it. A weekly CI job re-records them from the newest patch each
804
359
  hydrates the committed HTML with it, so a patch that changes what Vue writes or hydrates is caught
805
360
  before it reaches you; a ferrovue patch follows when one does.
806
361
 
807
- ### Deprecations
808
-
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
- ```
362
+ ### Upgrading
897
363
 
898
- ## Development
364
+ Until 1.0, versions are 0.MINOR.PATCH: raising MINOR (0.9.2 to 0.10.0) may change the
365
+ configuration, the generated code and the crates' API, and raising PATCH (0.10.0 to 0.10.1) breaks
366
+ none of them. The npm package and the crates share one version, so upgrade them together and
367
+ generate your components again. The
368
+ [CHANGELOG](https://github.com/Kinsmir/ferrovue/blob/main/CHANGELOG.md) lists what each release
369
+ changes, and the [guides](#guides) have an upgrade page for each MINOR release.
899
370
 
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.
371
+ ### Deprecations
910
372
 
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.
373
+ What ferrovue stops supporting is deprecated for at least one minor release before it is removed:
374
+ in a later minor release before 1.0, and only in a major release from 1.0. A deprecated crate item carries `#[deprecated]`; a deprecated configuration key warns
375
+ with `warning[FV1117]` and a command-line option with `warning[FV1118]`, naming the replacement, and the JSON Schema marks a deprecated key for editors.
376
+ Each release's CHANGELOG lists what it deprecates under "Deprecated".
912
377
 
913
378
  ## Licence
914
379