ferrovue 0.5.0 → 0.7.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 +306 -62
- package/dist/args.d.ts +28 -0
- package/dist/args.js +69 -0
- package/dist/attrs.d.ts +2 -1
- package/dist/attrs.js +10 -4
- package/dist/calls.js +5 -2
- package/dist/children.d.ts +2 -2
- package/dist/children.js +84 -16
- package/dist/classes.d.ts +4 -0
- package/dist/classes.js +23 -1
- package/dist/cli.js +80 -81
- package/dist/client-render.d.ts +23 -0
- package/dist/client-render.js +143 -0
- package/dist/client.d.ts +74 -10
- package/dist/client.js +373 -42
- package/dist/compiler.d.ts +9 -5
- package/dist/compiler.js +80 -18
- package/dist/component.d.ts +7 -3
- package/dist/component.js +38 -27
- package/dist/conformance.d.ts +85 -0
- package/dist/conformance.js +327 -0
- package/dist/constants.js +4 -2
- package/dist/context.d.ts +66 -10
- package/dist/context.js +176 -5
- package/dist/diagnostics.d.ts +6 -4
- package/dist/diagnostics.js +7 -3
- package/dist/dynamic.d.ts +18 -0
- package/dist/dynamic.js +576 -0
- package/dist/editor.d.ts +18 -0
- package/dist/editor.js +59 -0
- package/dist/errors.d.ts +213 -4
- package/dist/errors.js +129 -4
- package/dist/expr.d.ts +2 -0
- package/dist/expr.js +91 -12
- package/dist/fallthrough.d.ts +1 -0
- package/dist/fallthrough.js +10 -2
- package/dist/file-routes.d.ts +33 -0
- package/dist/file-routes.js +224 -0
- package/dist/files.d.ts +13 -0
- package/dist/files.js +49 -0
- package/dist/fixture.d.ts +16 -0
- package/dist/fixture.js +22 -7
- package/dist/hydration.d.ts +12 -1
- package/dist/hydration.js +47 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/inputs.d.ts +24 -0
- package/dist/inputs.js +40 -0
- package/dist/link-router.d.ts +7 -3
- package/dist/link-router.js +8 -4
- package/dist/lists.d.ts +16 -1
- package/dist/lists.js +78 -4
- package/dist/loops.js +4 -4
- package/dist/model.d.ts +20 -1
- package/dist/model.js +46 -1
- package/dist/numbers.js +3 -2
- package/dist/page-routes.d.ts +10 -0
- package/dist/page-routes.js +10 -0
- package/dist/paths.d.ts +5 -0
- package/dist/paths.js +7 -0
- package/dist/plugin.d.ts +15 -1
- package/dist/plugin.js +3 -0
- package/dist/plugins/client-only.js +3 -0
- package/dist/plugins/css-modules.d.ts +39 -0
- package/dist/plugins/css-modules.js +135 -0
- package/dist/plugins/head.d.ts +15 -0
- package/dist/plugins/head.js +385 -0
- package/dist/plugins/i18n.js +5 -12
- package/dist/plugins/index.js +4 -1
- package/dist/plugins/provide.d.ts +44 -0
- package/dist/plugins/provide.js +530 -0
- package/dist/plugins/router-link.js +7 -3
- package/dist/plugins/router.d.ts +4 -1
- package/dist/plugins/router.js +58 -11
- package/dist/plugins/scoped.d.ts +10 -0
- package/dist/plugins/scoped.js +17 -4
- package/dist/plugins/shared-types.js +2 -1
- package/dist/plugins/stores.js +26 -15
- package/dist/routes.d.ts +5 -2
- package/dist/routes.js +8 -2
- package/dist/rust.d.ts +9 -1
- package/dist/rust.js +83 -32
- package/dist/schema.d.ts +25 -0
- package/dist/schema.js +73 -0
- package/dist/script.js +31 -8
- package/dist/settle.d.ts +2 -0
- package/dist/settle.js +5 -0
- package/dist/slots.js +20 -4
- package/dist/ssr.d.ts +3 -0
- package/dist/ssr.js +67 -0
- package/dist/strings.d.ts +3 -0
- package/dist/strings.js +16 -2
- package/dist/styles.d.ts +5 -1
- package/dist/styles.js +131 -4
- package/dist/template.js +92 -11
- package/dist/testing.d.ts +4 -4
- package/dist/testing.js +3 -30
- package/dist/types.d.ts +5 -0
- package/dist/typescript.d.ts +11 -1
- package/dist/typescript.js +131 -36
- package/dist/vhtml.d.ts +4 -0
- package/dist/vhtml.js +63 -0
- package/dist/vite.d.ts +25 -11
- package/dist/vite.js +166 -51
- package/dist/volar.d.ts +15 -0
- package/dist/volar.js +87 -0
- package/package.json +41 -7
- package/schema.json +333 -0
package/README.md
CHANGED
|
@@ -38,8 +38,8 @@ cargo add ferrovue serde --features serde/derive
|
|
|
38
38
|
cargo add --dev serde_json # the generated conformance helper uses it under cfg(test)
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
ferrovue
|
|
42
|
-
crate that includes it needs **edition 2024**.
|
|
41
|
+
ferrovue needs Rust **1.89** or later (CI tests that and the latest stable). Generated code uses
|
|
42
|
+
let-chains, so the crate that includes it needs **edition 2024**.
|
|
43
43
|
|
|
44
44
|
### 2. Configure
|
|
45
45
|
|
|
@@ -47,25 +47,34 @@ crate that includes it needs **edition 2024**.
|
|
|
47
47
|
|
|
48
48
|
```json
|
|
49
49
|
{
|
|
50
|
+
"$schema": "https://cdn.jsdelivr.net/npm/ferrovue@0.7.0/schema.json",
|
|
50
51
|
"components": "client/components",
|
|
51
52
|
"out": "src/generated"
|
|
52
53
|
}
|
|
53
54
|
```
|
|
54
55
|
|
|
56
|
+
`$schema` points an editor at the configuration's JSON Schema, published with each version of the
|
|
57
|
+
npm package, so VS Code and other JSON editors complete the keys, describe them on hover and flag a
|
|
58
|
+
misspelt one. Name the version of `ferrovue` you use; `ferrovue init` writes it. ferrovue does not
|
|
59
|
+
read `$schema`. Offline, the copy in the installed package works as well:
|
|
60
|
+
`"$schema": "./node_modules/ferrovue/schema.json"`.
|
|
61
|
+
|
|
55
62
|
| Key | Required | Meaning |
|
|
56
63
|
|---|---|---|
|
|
57
64
|
| `components` | yes | Directory of `.vue` files to compile |
|
|
58
|
-
| `out` | yes | Directory the Rust modules are written to. **
|
|
59
|
-
| `routes` | no | JSON file listing the app's routes: each a vue-router path, or `{ "path", "name", "children" }`. Needed for `<RouterLink>`, `<RouterView>` and `useRoute()` |
|
|
65
|
+
| `out` | yes | Directory the Rust modules are written to. **Every module ferrovue wrote there is replaced**, and one no component produces any more is removed; a `.rs` file without ferrovue's `// @generated` header is left alone, and `--check` reports it |
|
|
66
|
+
| `routes` | no | JSON file listing the app's routes: each a vue-router path, or `{ "path", "name", "children" }`. Or `{ "pages": "client/pages" }`: a folder of pages, whose file names give the routes as vue-router's file-based routing reads them (`index.vue`, `[id].vue`, `[[id]].vue`, `[...path].vue`, `(group)` folders, a `name.vue` beside `name/` as its layout), each page compiled as a component. Needed for `<RouterLink>`, `<RouterView>` and `useRoute()` |
|
|
60
67
|
| `router` | no | Instead of `routes`: `{ routes, base?, linkActiveClass?, linkExactActiveClass? }`, matching `createWebHistory(base)` and `createRouter`'s options |
|
|
61
68
|
| `stores` | no | Directory of Pinia option stores whose state components may read |
|
|
62
|
-
| `trustedHtml` | no | Rust type of a `TrustedHtml` prop,
|
|
69
|
+
| `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` |
|
|
63
70
|
| `helpers` | no | `{ module, functions }`: functions a template may call, each mapped to a Rust twin |
|
|
64
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) |
|
|
65
72
|
| `i18n` | no | vue-i18n: `{ messages, locale?, fallbackLocale? }`, the directory of locale files (`en.json`, `nl.json`), the default locale and the fallbacks |
|
|
66
73
|
| `clientDirectives` | no | Custom directives with no server output (no `getSSRProps`), by name without `v-`: `["focus"]` |
|
|
67
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) |
|
|
68
75
|
| `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) |
|
|
69
78
|
| `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`) |
|
|
70
79
|
|
|
71
80
|
### 3. Write a component
|
|
@@ -84,19 +93,36 @@ defineProps<{ name: string; unread: number; note?: string }>();
|
|
|
84
93
|
### 4. Generate and render
|
|
85
94
|
|
|
86
95
|
```sh
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
pnpm ferrovue init # a starter ferrovue.config.json and components/Hello.vue
|
|
97
|
+
pnpm ferrovue # writes src/generated/{greeting.rs, mod.rs}
|
|
98
|
+
pnpm ferrovue --check # in CI: exits 1 if the committed modules are stale
|
|
99
|
+
pnpm ferrovue --check --diff # …and shows what differs, as `diff -u` does
|
|
100
|
+
pnpm ferrovue --check --format json # errors as JSON, for an editor or CI annotations
|
|
101
|
+
pnpm ferrovue --watch # regenerates when components or configs change
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`-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:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// vite.config.ts
|
|
111
|
+
import ferrovue from "ferrovue/vite";
|
|
112
|
+
|
|
113
|
+
export default { plugins: [ferrovue({ config: "config/ferrovue.json" })] };
|
|
93
114
|
```
|
|
94
115
|
|
|
116
|
+
`root` is the project root the configuration's paths are relative to (Vite's working directory by
|
|
117
|
+
default), and `config` the configuration file in it, as `--config` takes it (`ferrovue.config.json`
|
|
118
|
+
by default).
|
|
119
|
+
|
|
95
120
|
Every error carries a stable code, as `error[FV0602]: components/Card.vue:4:17: …`, documented in
|
|
96
121
|
the crate's [`error_codes`](https://docs.rs/ferrovue/latest/ferrovue/guide/error_codes/index.html)
|
|
97
122
|
guide. `--format json` writes the same errors as JSON with the exit status unchanged, and a VS Code
|
|
98
123
|
problem matcher for `tasks.json` puts them in the Problems panel: both are in the
|
|
99
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).
|
|
100
126
|
|
|
101
127
|
```rust
|
|
102
128
|
#[rustfmt::skip]
|
|
@@ -115,8 +141,8 @@ let html: String = greeting::html(&props).into_string();
|
|
|
115
141
|
let island = greeting::island(&props).into_string();
|
|
116
142
|
|
|
117
143
|
// (`island()` exists for a component that renders from its props alone: the client rebuilds it from
|
|
118
|
-
// `data-props`. One that also takes slots, the route, stores, translations
|
|
119
|
-
// and the page's own app mounts it; see examples/fullstack.)
|
|
144
|
+
// `data-props`. One that also takes slots, the route, stores, translations, teleports, provided
|
|
145
|
+
// values or the page head has `html()`, and the page's own app mounts it; see examples/fullstack.)
|
|
120
146
|
|
|
121
147
|
// …or straight into a buffer you already hold:
|
|
122
148
|
let mut page = String::from("<!doctype html><body>");
|
|
@@ -129,11 +155,22 @@ route.
|
|
|
129
155
|
|
|
130
156
|
The router and vue-i18n are features of the crate, `router` and `i18n`, both on by default. An
|
|
131
157
|
application with neither routes nor translations can leave them out, and build neither
|
|
132
|
-
`ferrovue-router` nor `ferrovue-i18n`: `ferrovue = { version = "0.
|
|
158
|
+
`ferrovue-router` nor `ferrovue-i18n`: `ferrovue = { version = "0.7", default-features = false }`.
|
|
133
159
|
|
|
134
160
|
With the `maud` feature, `ferrovue::Html` implements `maud::Render`, so `(greeting::html(&props))` can go
|
|
135
161
|
straight into a `maud::html!` page.
|
|
136
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.
|
|
173
|
+
|
|
137
174
|
With the `axum` feature, `Html` is an `IntoResponse`, and with `actix-web` a `Responder`: a handler
|
|
138
175
|
responds with the component, as `text/html; charset=utf-8`. `into_html(props)` and
|
|
139
176
|
`into_island(props)` take the props by value, so a handler that builds them returns the page:
|
|
@@ -147,7 +184,24 @@ async fn hello(Path(name): Path<String>) -> impl IntoResponse {
|
|
|
147
184
|
With the `dioxus` feature, a component goes into a Dioxus 0.7 page (`dioxus-ssr` or fullstack):
|
|
148
185
|
`{greeting::island(&props)}` in `rsx!` is the island element itself, with exactly the markup Vue
|
|
149
186
|
hydrates inside it. See `examples/dioxus` and the crate guide's
|
|
150
|
-
[Dioxus page](crates/ferrovue/docs/guide/dioxus.md).
|
|
187
|
+
[Dioxus page](https://github.com/Kinsmir/ferrovue/blob/main/crates/ferrovue/docs/guide/dioxus.md).
|
|
188
|
+
|
|
189
|
+
### 5. Diagnostics in your editor
|
|
190
|
+
|
|
191
|
+
`ferrovue/volar` is a plugin for Vue's language tools, which the Vue (Official) extension for VS Code
|
|
192
|
+
and Vue support in other editors run. It compiles the project as you type and underlines what
|
|
193
|
+
ferrovue refuses in the open component, with the error's code linked to its documentation. Name it
|
|
194
|
+
in the `tsconfig.json` that includes your components, then run **Vue: Restart Vue and TS servers**:
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{
|
|
198
|
+
"include": ["components/**/*.vue", "components/**/*.ts"],
|
|
199
|
+
"vueCompilerOptions": { "plugins": ["ferrovue/volar"] }
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The [quick start](https://docs.rs/ferrovue/latest/ferrovue/guide/quick_start/index.html#diagnostics-in-your-editor)
|
|
204
|
+
has the details; `examples/fullstack/tsconfig.json` enables it.
|
|
151
205
|
|
|
152
206
|
## What a component may use
|
|
153
207
|
|
|
@@ -157,38 +211,58 @@ ferrovue compiles `<script setup lang="ts">` components, and components with no
|
|
|
157
211
|
|
|
158
212
|
| Area | Supported |
|
|
159
213
|
|---|---|
|
|
160
|
-
| 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` |
|
|
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` |
|
|
161
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 |
|
|
162
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) |
|
|
163
217
|
| 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 |
|
|
164
|
-
| Setup | `ref`/`shallowRef`, `computed` (an expression or a single `return`), 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`,
|
|
218
|
+
| Setup | `ref`/`shallowRef`, `computed` (an expression or a single `return`), 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
|
+
| 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 |
|
|
165
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 |
|
|
166
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)) |
|
|
167
222
|
| 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` |
|
|
168
|
-
| 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; `.slice()`; chained and nested; a computed list read by `v-for`, `.join()`, `.length`, `.includes()`, kept by `computed`, or handed to a child |
|
|
223
|
+
| 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 |
|
|
169
224
|
| 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 |
|
|
170
225
|
| Attributes | static and bound attributes, boolean attributes, `:hidden`, `data-*` and `aria-*`, `v-bind` objects |
|
|
171
226
|
| `class` | strings, arrays, objects (`{ active: on }`, computed keys), `cond && "x"`, `cond ? "x" : null`, merged with a static `class` |
|
|
172
227
|
| `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 |
|
|
173
228
|
| Scoped styles | `<style scoped>`: the id on every element, on child components' roots (a root that is itself a component, fragments, recursion and `inheritAttrs: false` as Vue renders them) and, from a component with `:slotted()` rules, on the slot content it is given, forwarded slots included; inside `<Transition>`, `<KeepAlive>`, `<Teleport>` and `v-if`; on `<RouterLink>` and what it holds, as vue-router renders them |
|
|
174
|
-
|
|
|
229
|
+
| `v-bind()` in `<style>` | variables bound to props, setup values, `computed`s and constants, quoted expressions included (`v-bind('size + "px"')`), in a scoped or global `<style>`: written into the root's `style` after its own and what a parent passes, on each root of a fragment, and handed to a root that is a component, as Vue's server writes them; `undefined` and `null` as `initial`, an empty string as a space; named as plugin-vue names them for production or development (`isProduction`) |
|
|
230
|
+
| 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 |
|
|
231
|
+
| 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 |
|
|
232
|
+
| `<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 |
|
|
175
233
|
| 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) |
|
|
176
234
|
| 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 |
|
|
177
235
|
| 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 |
|
|
178
236
|
| Forms | `v-model` on text inputs, checkboxes, radios, `<select>` and `<textarea>` (renders the initial state) |
|
|
179
237
|
| Built-ins | `<Transition>`, `<TransitionGroup>`, `<KeepAlive>`, `<Suspense>` (synchronous content), `<Teleport>` (to any target, nested, disabled: see below), `v-text`, `v-once`, `v-pre`, `v-memo`, custom directives listed in `clientDirectives` |
|
|
180
|
-
| Vue Router | `<RouterLink>` (resolved by name or imported) with a string `to` or `{ name, params, query, hash }` / `{ path, query, hash }`, `active-class`, `exact-active-class`, `aria-current-value`, `replace`; vue-router's own encoding and active-link matching, nested routes included (a parent link is active on its children's pages, exact only on its own); a history base
|
|
238
|
+
| Vue Router | `<RouterLink>` (resolved by name or imported) with a string `to` or `{ name, params, query, hash }` / `{ path, query, hash }`, `active-class`, `exact-active-class`, `aria-current-value`, `replace`; vue-router's own encoding and active-link matching, nested routes included (a parent link is active on its children's pages, exact only on its own); a history base; optional parameters (`:id?`); routes built from a folder of pages, which the Vite plugin also gives the client as `ferrovue/routes`. `useRoute()` and `$route`: `path`, `fullPath`, `hash`, `name`, `params`, and `query` (a value written once, without `=`, or repeated, exactly as vue-router parses it; `typeof route.query.q === "string"` narrows one to a single string). `<RouterView>`, at the top and in nested route components: each takes the page it shows as a slot |
|
|
181
239
|
| 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 |
|
|
182
240
|
| 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 |
|
|
183
|
-
| `v-html` | only on a `TrustedHtml` prop (`import type { TrustedHtml } from "ferrovue/types"`) |
|
|
241
|
+
| `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 |
|
|
242
|
+
| 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) |
|
|
184
243
|
|
|
185
244
|
Refused at compile time, each with an error that names the construct:
|
|
186
245
|
|
|
187
|
-
- `<style module
|
|
246
|
+
- `<style module>` without `cssModules` in the configuration; a CSS module in another file, one a
|
|
247
|
+
preprocessor compiles, or one that imports from another file; `v-bind()` in `<style>` of a value
|
|
248
|
+
that is not a string or a number
|
|
188
249
|
- `<RouterView>` in a component with `<style scoped>`, which would give the page that component's
|
|
189
250
|
id; and, since vue-router renders a link from virtual nodes, a `<slot>` inside a `<RouterLink>`
|
|
190
251
|
that takes scope ids, or an element inside one in slot content given a `:slotted()` id
|
|
191
|
-
- `<component :is>`
|
|
252
|
+
- `<component :is>` over an open set (a `string`, a `Component`, a key that is not a literal union),
|
|
253
|
+
over a prop that may be absent, or naming a key its object lacks or a tag that is not an HTML
|
|
254
|
+
element; `v-html` or `v-text` on it; and, inside an element it chooses (which Vue renders from
|
|
255
|
+
virtual nodes), a `<slot>` with fallback content, `v-show`, and `v-model` on a `<select>`
|
|
256
|
+
- in provide and inject: a key that is neither a string literal nor an `InjectionKey` symbol exported
|
|
257
|
+
from a `.ts` file, `inject(key)!`, a value that may be `null` or `undefined`, values of different
|
|
258
|
+
types (or a ref and a plain value) under one key, setup that assigns to an injected value,
|
|
259
|
+
`provide` in a component holding `<RouterView>`, a string key injected inside a Rust twin's slot,
|
|
260
|
+
and `provide(…)` not imported from `vue`
|
|
261
|
+
- `v-show`, and `v-model` on a `<select>`, in the content of a `<RouterLink>` or of a Rust twin's
|
|
262
|
+
slot, which Vue renders from virtual nodes
|
|
263
|
+
- in the page head: options given to `useHead`, `useHeadSafe`, `templateParams`, a `titleTemplate`
|
|
264
|
+
function, event handlers (`onload`), a `class` or `style` that may be `null` (on which unhead's
|
|
265
|
+
renderer throws), and an object or array given to a `useSeoMeta` key
|
|
192
266
|
- the Options API (a `<script>` without `setup`), and a type parameter of a generic component with
|
|
193
267
|
no constraint (`generic="T"`)
|
|
194
268
|
- constants that are not literals (`Date.now()`, a function), an object constant read whole or by a
|
|
@@ -197,14 +271,18 @@ Refused at compile time, each with an error that names the construct:
|
|
|
197
271
|
- custom directives not listed in `clientDirectives`
|
|
198
272
|
- `watchEffect`, `watch` with `immediate`, `onServerPrefetch`, top-level `await`, and statements in setup that change state
|
|
199
273
|
- `route.meta` and `route.matched`
|
|
274
|
+
- in a folder of pages: a parameter beside text in one part of a path (`prefix-[id].vue`), repeatable
|
|
275
|
+
and optional catch-all parameters, parameter parsers (`[id=int]`), named views (`index@aside.vue`),
|
|
276
|
+
`definePage()`, a `<route>` block, and two pages, or a page and a component, with one component name
|
|
200
277
|
- Pinia getters that read `this` or return a function
|
|
201
278
|
- ordering comparisons between a string and a number, which JavaScript makes numeric
|
|
202
279
|
- regular expressions (`.replace(/x/g, …)`, `.split(/,/)`), replacement functions, a search's
|
|
203
280
|
starting position or a split's limit (`.includes(x, 3)`, `.split(",", 2)`), `.toLocaleUpperCase()`
|
|
204
281
|
and `.toLocaleLowerCase()` (the server's locale is not the browser's), `parseInt` with another radix
|
|
205
282
|
or of a number, `.repeat()` by a negative literal
|
|
206
|
-
- array methods given anything but an arrow function whose body is an expression
|
|
207
|
-
`x => { return … }`), `.map()` to optional values,
|
|
283
|
+
- array methods given anything but `Boolean` or an arrow function whose body is an expression
|
|
284
|
+
(`.map(String)`, `x => { return … }`), `.map()` to optional values, methods other than
|
|
285
|
+
`.filter(Boolean)` over a list of optional values, a computed list as a slot prop
|
|
208
286
|
- a dictionary's field read by name (`r.key`, `r[key]`), which may be absent although TypeScript says
|
|
209
287
|
it is not; `Object.entries()` anywhere but as a `v-for`'s source; dictionaries of optional values
|
|
210
288
|
- two strings that may each hold half of a surrogate pair compared, searched or joined (see
|
|
@@ -251,7 +329,7 @@ are, and `v-for="s in SIZES"` walks an array literal. A list of objects becomes
|
|
|
251
329
|
the shared types, with a type for its items (here `SortsItem`, or the interface the constant is
|
|
252
330
|
declared with):
|
|
253
331
|
|
|
254
|
-
```rust
|
|
332
|
+
```rust
|
|
255
333
|
pub const SORTS: &[SortsItem<'static>] = &[
|
|
256
334
|
SortsItem { key: Cow::Borrowed("name"), label: Cow::Borrowed("By name") },
|
|
257
335
|
SortsItem { key: Cow::Borrowed("price"), label: Cow::Borrowed("By price") },
|
|
@@ -280,11 +358,20 @@ Set `viteRoot` when Vite's root is not the directory holding `ferrovue.config.js
|
|
|
280
358
|
plugin (`ferrovue/vite`) compares the two when a component has scoped styles: a build in which they
|
|
281
359
|
differ fails, and the dev server warns.
|
|
282
360
|
|
|
361
|
+
`v-bind()` in `<style>` sets CSS variables on the component's root, which Vue's server writes into
|
|
362
|
+
its `style` and ferrovue writes there too. Their names follow the same id, and whether plugin-vue
|
|
363
|
+
compiles for production (`vite build`) or development (the dev server): set `isProduction` to match,
|
|
364
|
+
`true` by default. CSS modules (`<style module>`) take their class names from Vite's
|
|
365
|
+
`css.modules.generateScopedName`: set it to a pattern, and `cssModules` in `ferrovue.config.json` to
|
|
366
|
+
the same, and ferrovue computes each class as postcss-modules does in Vite. The Vite plugin compares
|
|
367
|
+
both, and fails a build in which they differ. See the crate's
|
|
368
|
+
[`scoped_styles`](https://docs.rs/ferrovue/latest/ferrovue/guide/scoped_styles/index.html) guide.
|
|
369
|
+
|
|
283
370
|
The default is the plugin's production behaviour because the production build is the one readers
|
|
284
371
|
get: with every option left alone, its styles apply. But plugin-vue hashes the path alone in its dev
|
|
285
372
|
server, so with the default a page rendered during development carries other ids than the dev
|
|
286
373
|
client and shows unstyled. The recommended setup is `componentIdGenerator: "filepath"` with
|
|
287
|
-
`"scopeId": "filepath"`, as [`examples/fullstack`](examples/fullstack) does: the ids are then the
|
|
374
|
+
`"scopeId": "filepath"`, as [`examples/fullstack`](https://github.com/Kinsmir/ferrovue/tree/main/examples/fullstack) does: the ids are then the
|
|
288
375
|
same in development and production, and do not change, nor change the generated Rust, whenever a
|
|
289
376
|
component's source does.
|
|
290
377
|
|
|
@@ -347,8 +434,8 @@ page::render(&mut out, &props, &i18n);
|
|
|
347
434
|
|
|
348
435
|
### Escape hatches
|
|
349
436
|
|
|
350
|
-
Some components will never compile: a component library's render functions,
|
|
351
|
-
|
|
437
|
+
Some components will never compile: a component library's render functions, code that measures the
|
|
438
|
+
page in `onMounted`. Two ways keep one of them from blocking the page around it.
|
|
352
439
|
|
|
353
440
|
**`<ClientOnly>`**: the server writes its `#fallback` slot (or a comment, without one), and the
|
|
354
441
|
browser hydrates that same fallback, then swaps in the default slot once mounted. Both sides render
|
|
@@ -397,13 +484,11 @@ have. Prop types are those of helpers (`string`, `int?`, …); an absent `bool`
|
|
|
397
484
|
bare `block` is `true`, as Vue casts them.
|
|
398
485
|
|
|
399
486
|
**Exactness is your responsibility.** ferrovue cannot check that `v_btn` writes the bytes Vue's
|
|
400
|
-
server renderer writes for `VBtn`: its guarantee rests on your code here, as it does for helpers. Prove it with fixtures
|
|
401
|
-
|
|
402
|
-
`render_json`, and compare. Slot content reaches the twin as a component's render function sees it
|
|
487
|
+
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
|
|
488
|
+
conformance suite (see [Testing against Vue](#testing-against-vue)). Slot content reaches the twin as a component's render function sees it
|
|
403
489
|
(`<!--v-if-->` for an absent `v-if`, as Vue writes virtual nodes). The twin decides where it goes and
|
|
404
490
|
whether fragment markers surround it, as the component does. `examples/fullstack` has a
|
|
405
|
-
twin (`src/ui.rs`)
|
|
406
|
-
half (`src/fixtures.rs`).
|
|
491
|
+
twin (`src/ui.rs`) and its fixtures (`fixtures/`).
|
|
407
492
|
|
|
408
493
|
### Teleports
|
|
409
494
|
|
|
@@ -419,6 +504,25 @@ let modals = teleports.get("#modals").unwrap_or_default(); // inside <div id="mo
|
|
|
419
504
|
As Vue recommends, teleport to a dedicated element such as `#modals`: the browser hydrates
|
|
420
505
|
a target from its first node, and `body` also holds the app.
|
|
421
506
|
|
|
507
|
+
### Page head
|
|
508
|
+
|
|
509
|
+
A component that calls `useHead` or `useSeoMeta` from `@unhead/vue` takes a `ferrovue::Head`, which
|
|
510
|
+
collects the calls of the whole tree in the order Vue's server runs them. After rendering the body,
|
|
511
|
+
write the head as unhead's server renderer writes it:
|
|
512
|
+
|
|
513
|
+
```rust
|
|
514
|
+
let head = ferrovue::Head::new(); // createHead(): lang="en", charset and viewport; or Head::without_defaults()
|
|
515
|
+
book_page::render(&mut body, &props, &head);
|
|
516
|
+
let tags = head.render(); // head_tags, body_tags_open, body_tags, html_attrs, body_attrs
|
|
517
|
+
let page = format!("<html{}><head>{}</head><body{}>{}{}{}</body></html>",
|
|
518
|
+
tags.html_attrs, tags.head_tags, tags.body_attrs, tags.body_tags_open, body, tags.body_tags);
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
On the client the head is unhead's: give the page's app its client head,
|
|
522
|
+
`mountPage(layout, islands, { plugins: [createHead()] })` with `createHead` from
|
|
523
|
+
`@unhead/vue/client`, and it takes over the tags the server wrote. The crate's
|
|
524
|
+
[`head`](https://docs.rs/ferrovue/latest/ferrovue/guide/head/index.html) guide has the details.
|
|
525
|
+
|
|
422
526
|
### Scoped slots from Rust
|
|
423
527
|
|
|
424
528
|
A scoped slot's `Slots` field takes a closure that receives the props its outlet passes, as a
|
|
@@ -459,6 +563,25 @@ components alike, so a hand-written island can join them: `mountIslands({ ...isl
|
|
|
459
563
|
An island whose name is not among them is left as the server rendered it, with a warning naming it
|
|
460
564
|
(`onError` to handle it otherwise), as is one whose component fails to load.
|
|
461
565
|
|
|
566
|
+
An island can wait to hydrate, and to fetch its code, until it is needed. `hydrate` on the
|
|
567
|
+
server's `island()` writes a `data-hydrate` attribute on the wrapper, which `mountIslands` honours
|
|
568
|
+
with Vue's lazy hydration strategies:
|
|
569
|
+
|
|
570
|
+
```rust
|
|
571
|
+
use ferrovue::Hydrate;
|
|
572
|
+
|
|
573
|
+
reviews::island(&props).hydrate(Hydrate::Visible); // scrolled into view
|
|
574
|
+
footer::island(&props).hydrate(Hydrate::visible_with("200px")); // within 200px of the view
|
|
575
|
+
chart::island(&props).hydrate(Hydrate::Idle); // the browser is idle
|
|
576
|
+
counter::island(&props).hydrate(Hydrate::Interaction); // pointerenter, click or focus
|
|
577
|
+
menu::island(&props).hydrate(Hydrate::InteractionOn(&["keydown"]));
|
|
578
|
+
sidebar::island(&props).hydrate(Hydrate::media("(min-width: 60rem)"));
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
The island's markup is unchanged, so nothing about exactness changes. An event that woke an island
|
|
582
|
+
waiting for interaction is dispatched again once it has hydrated, so the first click is not lost.
|
|
583
|
+
An island without `data-hydrate` hydrates at once, as before.
|
|
584
|
+
|
|
462
585
|
An island's `<style>` goes into its chunk too, and Vite links it only once the island's script has
|
|
463
586
|
loaded, after the server's markup is on screen. Link those stylesheets from the page up front: a
|
|
464
587
|
server reading Vite's manifest takes the `css` of the entry's `dynamicImports` as well as its own
|
|
@@ -466,7 +589,8 @@ server reading Vite's manifest takes the `css` of the entry's `dynamicImports` a
|
|
|
466
589
|
|
|
467
590
|
An application whose own navigation layer leaves the page can keep `<RouterLink>` for its `href`s
|
|
468
591
|
and active classes with `linkRouter(routes, { navigate })` from `ferrovue/link-router`: a router
|
|
469
|
-
over the routes file
|
|
592
|
+
over the routes file (or the default export of `ferrovue/routes`, with a folder of pages) whose
|
|
593
|
+
every navigation after the first goes to `navigate`. The routing guide
|
|
470
594
|
(`ferrovue::guide::routing`) explains each part of it.
|
|
471
595
|
|
|
472
596
|
### Hydrating a whole page
|
|
@@ -498,6 +622,24 @@ The record repeats the text of every prop, so a page hydrated this way is larger
|
|
|
498
622
|
islands; the guide's `islands_and_hydration` page compares the two, and `examples/fullstack` serves
|
|
499
623
|
a page of each kind.
|
|
500
624
|
|
|
625
|
+
An application that goes to the next page without a reload shows it from the record in the fetched
|
|
626
|
+
document, with the same app:
|
|
627
|
+
|
|
628
|
+
```ts
|
|
629
|
+
import { hydrateState, readPage, renderPage } from "ferrovue";
|
|
630
|
+
|
|
631
|
+
const next = readPage(await (await fetch(href)).text()); // no DOMParser: works in a worker too
|
|
632
|
+
const pinia = createPinia();
|
|
633
|
+
hydrateState(pinia, { text: next.state });
|
|
634
|
+
const router = linkRouter(routes, { navigate, location: href }); // at the next page's location
|
|
635
|
+
current = await renderPage(() => import("./Picks.vue"), islands, next.record, { pinia, router, previous: current });
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
`renderPage` mounts the next page's app in place of the container before it unmounts `previous`, so
|
|
639
|
+
a router both use stays started. `renderRecordedPage` from `ferrovue/testing` checks that a page
|
|
640
|
+
rendered from its record on the client shows what the server wrote, and `conformanceSuite` checks
|
|
641
|
+
the same of every fixture.
|
|
642
|
+
|
|
501
643
|
### Hydrating Pinia state
|
|
502
644
|
|
|
503
645
|
```rust
|
|
@@ -509,17 +651,82 @@ import { hydrateState } from "ferrovue";
|
|
|
509
651
|
hydrateState(pinia); // before app.mount(): every store starts from what the server rendered
|
|
510
652
|
```
|
|
511
653
|
|
|
654
|
+
### Fixtures in an application's vitest run
|
|
655
|
+
|
|
656
|
+
`fixtureApp` from `ferrovue/testing` installs Pinia for a fixture's `$stores`, vue-router for its
|
|
657
|
+
routes and vue-i18n for its `i18n` option, importing each itself. A component's `useBasket()`,
|
|
658
|
+
`useRoute()` or `useI18n()` reads the instance its own import of the package gives it, so the two
|
|
659
|
+
must be one module instance. vitest loads `ferrovue/testing` from `node_modules` with Node, and
|
|
660
|
+
runs the application's code through Vite; where a peer goes through Vite too (listed in
|
|
661
|
+
`server.deps.inline`, aliased, linked from elsewhere, or installed twice), the fixture installs a
|
|
662
|
+
different instance from the one the component reads, and the render fails with Pinia's "there was
|
|
663
|
+
no active Pinia" or an undefined route. Pass the application's own modules in the options:
|
|
664
|
+
|
|
665
|
+
```ts
|
|
666
|
+
const app = await fixtureApp(component, readFixture(json), routes, {
|
|
667
|
+
pinia: await import("pinia"),
|
|
668
|
+
vueRouter: await import("vue-router"),
|
|
669
|
+
vueI18n: await import("vue-i18n"),
|
|
670
|
+
});
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
Without them, `fixtureApp` imports the peers itself, as before. Another way is to have vitest run
|
|
674
|
+
ferrovue through Vite as well, which puts it on the application's module graph; this also covers
|
|
675
|
+
`vue` itself, which `fixtureApp` always imports:
|
|
676
|
+
|
|
677
|
+
```ts
|
|
678
|
+
// vitest.config.ts
|
|
679
|
+
export default defineConfig({ test: { server: { deps: { inline: ["ferrovue"] } } } });
|
|
680
|
+
```
|
|
681
|
+
|
|
512
682
|
### Streaming with holes
|
|
513
683
|
|
|
514
684
|
`ferrovue::hole()` is a slot whose content you write later. Render a layout with holes,
|
|
515
685
|
`split_holes` the output, and stream the pieces with each hole's content between them, in whatever
|
|
516
|
-
order the content is ready. With the `axum`
|
|
517
|
-
streamed
|
|
686
|
+
order the content is ready. With the `stream` feature (which `axum` and `actix-web` turn on),
|
|
687
|
+
`ferrovue::HtmlStream` is that streamed body, from the render and a future for each hole's content:
|
|
518
688
|
|
|
519
689
|
```rust
|
|
520
690
|
let body = ferrovue::HtmlStream::new(page).hole(async move { reviews_of(&id).await });
|
|
521
691
|
```
|
|
522
692
|
|
|
693
|
+
### Testing against Vue
|
|
694
|
+
|
|
695
|
+
An application holds its own components to Vue as ferrovue's suite does, with a fixtures directory
|
|
696
|
+
and two calls. A fixture is a component's props as JSON (plus `$slots`, `$route`, `$stores` and
|
|
697
|
+
`$locale` where it takes them) beside the HTML Vue rendered for them:
|
|
698
|
+
|
|
699
|
+
```text
|
|
700
|
+
fixtures/Reviews/three.json {"reviews":[{"reader":"Ada","stars":5,"text":"The spice must flow."}]}
|
|
701
|
+
fixtures/Reviews/three.html recorded from Vue with FERROVUE_FIXTURES_WRITE=1
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
```ts
|
|
705
|
+
// test/conformance.test.ts, run by vitest with environment: "happy-dom"
|
|
706
|
+
import { join } from "node:path";
|
|
707
|
+
import type { Component } from "vue";
|
|
708
|
+
import { conformanceSuite } from "ferrovue/testing";
|
|
709
|
+
|
|
710
|
+
await conformanceSuite({
|
|
711
|
+
config: join(import.meta.dirname, "../ferrovue.config.json"),
|
|
712
|
+
components: import.meta.glob<Component>("../client/components/*.vue", { eager: true, import: "default" }),
|
|
713
|
+
pinia: await import("pinia"),
|
|
714
|
+
vueRouter: await import("vue-router"),
|
|
715
|
+
});
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
```rust
|
|
719
|
+
ferrovue::conformance!("fixtures", generated::render_json, at_least = 24);
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
The Vue half fails on a component with no fixtures, a fixture naming no component, a project with
|
|
723
|
+
no components, generated Rust that differs from what `ferrovue` writes now, a fixture Vue renders
|
|
724
|
+
differently from its `.html`, and a recorded `.html` that hydrates with any warning;
|
|
725
|
+
`FERROVUE_FIXTURES_WRITE=1` records the `.html` files instead. The Rust half renders every fixture through the generated `render_json`,
|
|
726
|
+
compares the bytes, and fails when fewer than `at_least` fixtures are found. The crate's
|
|
727
|
+
[`testing`](https://docs.rs/ferrovue/latest/ferrovue/guide/testing/index.html) guide has the
|
|
728
|
+
details; `examples/fullstack` uses both.
|
|
729
|
+
|
|
523
730
|
## Performance
|
|
524
731
|
|
|
525
732
|
Time to render one component to an HTML string. On the left is Vue's `renderToString` on Node, and
|
|
@@ -529,10 +736,10 @@ and both write the same bytes: each side checks its output against
|
|
|
529
736
|
|
|
530
737
|
| Scenario | What renders | Output | Vue `renderToString` | ferrovue | Speed-up |
|
|
531
738
|
|---|---|---|---|---|---|
|
|
532
|
-
| `small` | `Nav`: two `<RouterLink>`s resolved against the current route | 206 B | 41
|
|
533
|
-
| `list` | `Lists`: 1,000 words, 1,000 numbers, 100 groups of 10 members | 102 KiB |
|
|
534
|
-
| `tree` | `Tree`: a recursive component, binary tree 8 levels deep (255 nodes) | 9.5 KiB |
|
|
535
|
-
| `page` | `Dashboard`: 22 `Panel`s with named slots, a `Text`, 20 `Frame`s holding loops | 8.0 KiB |
|
|
739
|
+
| `small` | `Nav`: two `<RouterLink>`s resolved against the current route | 206 B | 36.41 µs | 0.989 µs | 36.8× |
|
|
740
|
+
| `list` | `Lists`: 1,000 words, 1,000 numbers, 100 groups of 10 members | 102 KiB | 355.8 µs | 46.16 µs | 7.7× |
|
|
741
|
+
| `tree` | `Tree`: a recursive component, binary tree 8 levels deep (255 nodes) | 9.5 KiB | 522.2 µs | 3.86 µs | 135.2× |
|
|
742
|
+
| `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× |
|
|
536
743
|
|
|
537
744
|
These are mean times per render. The Vue column is tinybench's mean, with 5 s per scenario after
|
|
538
745
|
a 1 s warm-up. The ferrovue column is criterion's mean point estimate, using its defaults of a 3 s
|
|
@@ -580,21 +787,36 @@ of Vue and of each integration. Your project may use any later patch of the same
|
|
|
580
787
|
| `vue-router` (optional) | `~5.3.1` | 5.3.1 |
|
|
581
788
|
| `pinia` (optional) | `~4.0.3` | 4.0.3 |
|
|
582
789
|
| `vue-i18n` (optional) | `~11.4.13` | 11.4.13 |
|
|
790
|
+
| `@unhead/vue` (optional) | `~3.4.2` | 3.4.2 |
|
|
583
791
|
|
|
584
792
|
A new minor of any of them (Vue 3.6, vue-router 5.4, …) needs a ferrovue release that re-records
|
|
585
793
|
the fixtures from it. A weekly CI job re-records them from the newest patch each range allows and
|
|
586
794
|
hydrates the committed HTML with it, so a patch that changes what Vue writes or hydrates is caught
|
|
587
795
|
before it reaches you; a ferrovue patch follows when one does.
|
|
588
796
|
|
|
797
|
+
### Deprecations
|
|
798
|
+
|
|
799
|
+
Something ferrovue stops supporting is deprecated first, for at least one minor release, and
|
|
800
|
+
removed in the next major release. A deprecated item of the crates carries `#[deprecated]`, so the
|
|
801
|
+
Rust compiler warns where it is used. A deprecated configuration key warns with
|
|
802
|
+
`warning[FV1117]` and a deprecated command-line option with `warning[FV1118]`, each naming what
|
|
803
|
+
replaces it, and the JSON Schema marks the key deprecated for editors. Each release's CHANGELOG lists
|
|
804
|
+
what it deprecates under "Deprecated".
|
|
805
|
+
|
|
589
806
|
## Repository layout
|
|
590
807
|
|
|
591
808
|
```text
|
|
592
809
|
crates/ferrovue/ the Rust runtime crate, which generated code calls
|
|
593
810
|
src/ a module per part (`Html`, slots, class and style, the state script,
|
|
594
|
-
strings, teleports, fallthrough attributes
|
|
595
|
-
|
|
596
|
-
|
|
811
|
+
strings, teleports, the page head, fallthrough attributes, `BasicHtml`
|
|
812
|
+
and `InlineHtml`,
|
|
813
|
+
`Sanitised`, the fixture check `conformance!` writes), each module's
|
|
814
|
+
unit tests beside it in <module>/tests.rs; lib.rs re-exports them all,
|
|
815
|
+
and the three crates below
|
|
816
|
+
docs/guide/ the crate guide, `ferrovue::guide` on docs.rs
|
|
597
817
|
tests/conformance/ components, fixtures, recorded HTML, generated Rust
|
|
818
|
+
tests/literal_props/ components compiled with `"builders": false`
|
|
819
|
+
tests/sanitised/ a component whose `TrustedHtml` is `ferrovue::Sanitised`
|
|
598
820
|
tests/vectors/ vectors recorded from JavaScript and Vue
|
|
599
821
|
tests/properties.rs property-based tests of the runtime
|
|
600
822
|
benches/ criterion benchmarks of generated renderers (see Performance)
|
|
@@ -603,40 +825,62 @@ crates/ferrovue-core/ escaping and JavaScript's numbers, which the other
|
|
|
603
825
|
crates/ferrovue-router/ vue-router's matching and links (the `router` feature)
|
|
604
826
|
tests/vectors/ vectors recorded from vue-router
|
|
605
827
|
crates/ferrovue-i18n/ vue-i18n's t() (the `i18n` feature)
|
|
828
|
+
crates/ferrovue-contract/ unpublished: Rust using every item of the generated-code contract,
|
|
829
|
+
and the version check's message (trybuild)
|
|
606
830
|
packages/ferrovue/ the compiler (npm package)
|
|
607
|
-
src/index.ts `ferrovue`: the browser API, `mountIslands`, `mountPage`, `hydrateState` and the types
|
|
831
|
+
src/index.ts `ferrovue`: the browser API, `mountIslands`, `mountPage`, `readPage`, `renderPage`, `hydrateState` and the types
|
|
608
832
|
src/compiler.ts `ferrovue/compiler`: `generate`, `write`
|
|
833
|
+
src/context.ts, model.ts `ferrovue.config.json`, and the types and values the compiler passes around
|
|
834
|
+
src/schema.ts the configuration's keys, types and defaults, from which `schema.json` is written
|
|
609
835
|
src/component.ts, script.ts, typescript.ts, constants.ts
|
|
610
836
|
a `.vue` file read, <script setup>, TypeScript types, constants and enums
|
|
611
|
-
src/template.ts, children.ts, slots.ts, loops.ts
|
|
612
|
-
the compiled template: statements, child components, slots, v-for
|
|
613
|
-
|
|
614
|
-
|
|
837
|
+
src/template.ts, children.ts, slots.ts, loops.ts, vhtml.ts
|
|
838
|
+
the compiled template: statements, child components, slots, v-for,
|
|
839
|
+
where `v-html` may go
|
|
840
|
+
src/expr.ts, strings.ts, numbers.ts, narrowing.ts, calls.ts, lists.ts, parens.ts
|
|
841
|
+
expressions: operators, strings, numbers, narrowing, calls, lists,
|
|
842
|
+
the parentheses Rust needs
|
|
615
843
|
src/attrs.ts, classes.ts, styles.ts, fallthrough.ts
|
|
616
844
|
attributes, class and style, attributes a parent passes on
|
|
845
|
+
src/dynamic.ts `<component :is>` over a closed set of choices
|
|
617
846
|
src/plugin.ts the plugin interface (see CONTRIBUTING.md)
|
|
618
847
|
src/plugins/ vue-router, Pinia, vue-i18n, scoped styles, <Teleport>, shared types,
|
|
619
|
-
<ClientOnly>, Rust twins
|
|
848
|
+
<ClientOnly>, Rust twins, provide and inject, the page head
|
|
620
849
|
src/rust.ts, emitter.ts the Rust source written out
|
|
621
|
-
src/cli.ts, vite.ts
|
|
850
|
+
src/cli.ts, args.ts, vite.ts
|
|
851
|
+
the `ferrovue` command, its options, and the Vite plugin
|
|
852
|
+
src/diff.ts the diff `ferrovue --check --diff` prints for a stale file
|
|
622
853
|
src/errors.ts, diagnostics.ts
|
|
623
854
|
every error code with its title, and errors as the CLI, JSON and Vite show them
|
|
624
|
-
src/
|
|
855
|
+
src/volar.ts, editor.ts `ferrovue/volar`: errors in the editor, from a plugin for Vue's language tools
|
|
856
|
+
src/client.ts browser-side helpers: `mountIslands`, `mountPage`, `readPage`, `renderPage`, `hydrateState`, `<ClientOnly>`
|
|
625
857
|
src/islands.ts `ferrovue/islands`, which the Vite plugin writes: every island, loaded lazily
|
|
626
858
|
src/link-router.ts `ferrovue/link-router`: `<RouterLink>` while the application navigates on its own
|
|
627
859
|
src/routes.ts a routes file as vue-router's route records
|
|
628
|
-
src/
|
|
629
|
-
src/
|
|
630
|
-
src/
|
|
860
|
+
src/file-routes.ts the routes vue-router's file-based routing builds from a folder of pages
|
|
861
|
+
src/page-routes.ts `ferrovue/routes`, which the Vite plugin writes from the pages
|
|
862
|
+
src/testing.ts `ferrovue/testing`: utilities for a project's own conformance suite
|
|
863
|
+
src/fixture.ts, settle.ts `fixtureApp` and `readFixture`, and waiting for async components to settle
|
|
864
|
+
src/conformance.ts `conformanceSuite`, which `ferrovue/testing` exports
|
|
865
|
+
src/ssr.ts `attachSsrRender`, which `ferrovue/testing` exports
|
|
866
|
+
src/hydration.ts `hydrateRecordedPage` and `renderRecordedPage`, which `ferrovue/testing` exports
|
|
867
|
+
src/client-render.ts the DOM of a client render as it is compared with the server's markup
|
|
868
|
+
src/types.ts `ferrovue/types`: `TrustedHtml`, `InlineHtml`, `Float`
|
|
631
869
|
test/ compiler, CLI, router, vector, island, Vite and conformance tests
|
|
870
|
+
api/ the package's API report, an `.api.md` per export and `cli.txt` (`pnpm api:report`)
|
|
871
|
+
schema.json the JSON Schema of `ferrovue.config.json` (`pnpm schema:generate`)
|
|
632
872
|
browser/ conformance fixtures and recorded pages hydrated in real browsers (`pnpm test:browser`)
|
|
633
873
|
bench/ Vue renderToString benchmarks, the other half of Performance
|
|
634
874
|
fuzz/ the randomised differential tester (`pnpm fuzz`)
|
|
635
875
|
examples/greeting/ the smallest setup: one component rendered from Rust
|
|
636
|
-
examples/fullstack/ axum + Vite: islands, a page hydrated whole,
|
|
637
|
-
|
|
876
|
+
examples/fullstack/ axum + Vite: islands (some hydrating later), a page hydrated whole,
|
|
877
|
+
Pinia state, routes from a folder of pages, the page head, streaming,
|
|
878
|
+
<ClientOnly>, a Rust twin, and a conformance suite
|
|
638
879
|
examples/dioxus/ a Dioxus page, rendered with dioxus-ssr, with an island in it
|
|
880
|
+
scripts/ferrovue-in.ts the `ferrovue` command run in a project of this repository (`pnpm conformance:check`, CI)
|
|
881
|
+
scripts/inspect.ts Vue's SSR code and render of a component beside the Rust ferrovue generates for it
|
|
639
882
|
scripts/release.ts the release version bump (see RELEASING.md)
|
|
883
|
+
scripts/api-report.ts the npm package's API report, written and checked (`pnpm api:report`, `pnpm api:check`)
|
|
640
884
|
```
|
|
641
885
|
|
|
642
886
|
## Development
|
|
@@ -648,15 +892,15 @@ pnpm typecheck
|
|
|
648
892
|
cargo test --all-features
|
|
649
893
|
```
|
|
650
894
|
|
|
651
|
-
[TESTING.md](TESTING.md) explains how the suite fits together and how to add a case,
|
|
652
|
-
[CONTRIBUTING.md](CONTRIBUTING.md) how to add support for a Vue construct, and
|
|
653
|
-
[RELEASING.md](RELEASING.md) how a release is cut.
|
|
895
|
+
[TESTING.md](https://github.com/Kinsmir/ferrovue/blob/main/TESTING.md) explains how the suite fits together and how to add a case,
|
|
896
|
+
[CONTRIBUTING.md](https://github.com/Kinsmir/ferrovue/blob/main/CONTRIBUTING.md) how to add support for a Vue construct, and
|
|
897
|
+
[RELEASING.md](https://github.com/Kinsmir/ferrovue/blob/main/RELEASING.md) how a release is cut.
|
|
654
898
|
|
|
655
|
-
[`examples/fullstack`](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.
|
|
899
|
+
[`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.
|
|
656
900
|
|
|
657
901
|
## Licence
|
|
658
902
|
|
|
659
|
-
Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or [MIT licence](LICENSE-MIT),
|
|
903
|
+
Licensed under either of [Apache License, Version 2.0](https://github.com/Kinsmir/ferrovue/blob/main/LICENSE-APACHE) or [MIT licence](https://github.com/Kinsmir/ferrovue/blob/main/LICENSE-MIT),
|
|
660
904
|
at your option. Unless you explicitly state otherwise, any contribution you intentionally submit for
|
|
661
905
|
inclusion in this work, as defined in the Apache-2.0 licence, is dual-licensed as above, without any
|
|
662
906
|
additional terms or conditions.
|