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