ferrovue 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -56,12 +56,15 @@ crate that includes it needs **edition 2024**.
56
56
  |---|---|---|
57
57
  | `components` | yes | Directory of `.vue` files to compile |
58
58
  | `out` | yes | Directory the Rust modules are written to. **Everything in it is replaced.** |
59
- | `routes` | no | JSON file listing the app's routes: each a vue-router path, or `{ "path", "name" }`. Needed for `<RouterLink>`, `<RouterView>` and `useRoute()` |
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()` |
60
60
  | `router` | no | Instead of `routes`: `{ routes, base?, linkActiveClass?, linkExactActiveClass? }`, matching `createWebHistory(base)` and `createRouter`'s options |
61
61
  | `stores` | no | Directory of Pinia option stores whose state components may read |
62
62
  | `trustedHtml` | no | Rust type of a `TrustedHtml` prop, e.g. `crate::html::Sanitised` (needed for `v-html`) |
63
63
  | `helpers` | no | `{ module, functions }`: functions a template may call, each mapped to a Rust twin |
64
+ | `i18n` | no | vue-i18n: `{ messages, locale?, fallbackLocale? }`, the directory of locale files (`en.json`, `nl.json`), the default locale and the fallbacks |
64
65
  | `clientDirectives` | no | Custom directives with no server output (no `getSSRProps`), by name without `v-`: `["focus"]` |
66
+ | `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) |
67
+ | `viteRoot` | no | Vite's root, relative to this file's directory, from which a component's path is hashed (default `.`) |
65
68
 
66
69
  ### 3. Write a component
67
70
 
@@ -89,7 +92,8 @@ mod generated;
89
92
 
90
93
  use generated::greeting::{self, Props};
91
94
 
92
- let props = Props { name: "Ada".into(), unread: 3, note: None };
95
+ // `new` takes the required props; each optional one has a setter.
96
+ let props = Props::new("Ada", 3).note("welcome back");
93
97
 
94
98
  // The markup alone:
95
99
  let html: String = greeting::html(&props).into_string();
@@ -98,6 +102,10 @@ let html: String = greeting::html(&props).into_string();
98
102
  // <div data-island="Greeting" data-props="{…}">…</div>
99
103
  let island = greeting::island(&props).into_string();
100
104
 
105
+ // (`island()` exists for a component that renders from its props alone: the client rebuilds it from
106
+ // `data-props`. One that also takes slots, the route, stores, translations or teleports has `html()`,
107
+ // and the page's own app mounts it — see examples/fullstack.)
108
+
101
109
  // …or straight into a buffer you already hold:
102
110
  let mut page = String::from("<!doctype html><body>");
103
111
  greeting::render(&mut page, &props);
@@ -117,40 +125,153 @@ ferrovue compiles `<script setup lang="ts">` components. Props are declared by t
117
125
 
118
126
  | Area | Supported |
119
127
  |---|---|
120
- | Prop types | `string`, `number` (integers, `i64`), `boolean`, string-literal unions (`"sm" \| "md"`), `T \| undefined`, arrays (`T[]`, `Array<T>`, `readonly T[]`), interfaces and object type aliases (recursive ones too), another component's exported `Props`, `TrustedHtml` |
128
+ | 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`, 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` |
121
129
  | 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 |
122
130
  | Props | `withDefaults`, destructured props with defaults (`const { size = "md" } = defineProps<…>()`), optional booleans (Vue casts an absent one to `false`), `defineModel` (named, required, with defaults), components with no props |
123
131
  | Setup | `ref`/`shallowRef`, `computed` (an expression or a single `return`), 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`, `provide`, template refs (`ref(null)`, `useTemplateRef`) and functions are client-only. The template may name them only from event handlers, which the server drops |
124
- | Text | `{{ }}` of strings, integers and booleans; `+`, `-`, `*`, `%` (literal divisor); `<`, `>`, `<=`, `>=`, `===`, `!==`; unary `-`, `!`; `??`, `\|\|`, `&&`, `?:`; template literals; optional chaining `a?.b`; `.length`; string `.trim()`, `.trimStart()`, `.trimEnd()`, `.toUpperCase()`, `.toLowerCase()`, `.includes()`, `.startsWith()`, `.endsWith()`; list `.includes()`, `.join()`; `String()`, `.toString()`, `Math.max`/`min`/`abs`; array literals |
125
- | Conditions | `v-if` / `v-else-if` / `v-else`, with optional values narrowed inside the branch as TypeScript narrows them |
126
- | 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>` |
132
+ | 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 |
133
+ | 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)) |
134
+ | Conditions | `v-if` / `v-else-if` / `v-else`, `?:`, `&&` and `||`, with optional values narrowed as TypeScript narrows them: `v-if="user"`, `user !== undefined`, and `!user` or `user === undefined` for the `v-else` |
135
+ | 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()`, `.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 |
136
+ | 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 |
127
137
  | Attributes | static and bound attributes, boolean attributes, `:hidden`, `data-*` and `aria-*`, `v-bind` objects |
128
138
  | `class` | strings, arrays, objects (`{ active: on }`, computed keys), `cond && "x"`, `cond ? "x" : null`, merged with a static `class` |
129
139
  | `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 |
140
+ | 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 |
130
141
  | Components | imported child components, `v-bind` of a child's own `Props`, `v-model` on a child's `defineModel`, recursion |
131
142
  | Slots | default and named slots, fallbacks, `$slots.name` tests, scoped slots (`<slot :item="x">` and `#item="{ item }"` or `v-slot="props"`), whose props a parent can hand to its own children |
132
143
  | Forms | `v-model` on text inputs, checkboxes, radios, `<select>` and `<textarea>` (renders the initial state) |
133
- | Built-ins | `<Transition>`, `<TransitionGroup>`, `<KeepAlive>`, `<Suspense>` (synchronous content), `v-text`, `v-once`, `v-pre`, `v-memo`, custom directives listed in `clientDirectives` |
134
- | 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; a history base. `useRoute()` and `$route`: `path`, `hash`, `name`, `params`. `<RouterView>` at the top level |
135
- | Pinia | option stores with a typed `state`, read through `useX()` or `storeToRefs`, in the template or in `computed`; getters that are an expression of the state |
144
+ | 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` |
145
+ | 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. `useRoute()` and `$route`: `path`, `fullPath`, `hash`, `name`, `params`, and `query` (a value written once, without `=`, or repeated, exactly as vue-router parses it). `<RouterView>`, at the top and in nested route components: each takes the page it shows as a slot |
146
+ | 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 |
147
+ | 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 |
136
148
  | `v-html` | only on a `TrustedHtml` prop (`import type { TrustedHtml } from "ferrovue/types"`) |
137
149
 
138
150
  Refused at compile time, each with an error that names the construct:
139
151
 
140
- - `<style scoped>`, `<style module>`, and `v-bind()` in CSS
141
- - `<Teleport>` and `<component :is>`
152
+ - `<style module>`, and `v-bind()` in CSS
153
+ - `<RouterView>` in a component with `<style scoped>`, which would give the page that component's
154
+ id; and, since vue-router renders a link from virtual nodes, a `<slot>` inside a `<RouterLink>`
155
+ that takes scope ids, or an element inside one in slot content given a `:slotted()` id
156
+ - `<component :is>`
142
157
  - `<RouterLink custom>`, slot props that are array literals, defaults in destructured slot props, and outlets of one slot that pass different props
143
158
  - custom directives not listed in `clientDirectives`
144
159
  - `watchEffect`, `watch` with `immediate`, `onServerPrefetch`, top-level `await`, and statements in setup that change state
145
- - `route.query` and `route.fullPath`, and setup stores
160
+ - `route.meta` and `route.matched`
146
161
  - Pinia getters that read `this` or return a function
147
- - `/` and fractions, ordering comparisons of strings
162
+ - ordering comparisons between a string and a number, which JavaScript makes numeric
163
+ - regular expressions (`.replace(/x/g, …)`, `.split(/,/)`), replacement functions, a search's
164
+ starting position or a split's limit (`.includes(x, 3)`, `.split(",", 2)`), `.toLocaleUpperCase()`
165
+ and `.toLocaleLowerCase()` (the server's locale is not the browser's), `parseInt` with another radix
166
+ or of a number, `.repeat()` by a negative literal
167
+ - array methods given anything but an arrow function whose body is an expression (`.filter(Boolean)`,
168
+ `x => { return … }`), `.map()` to optional values, a computed list as a slot prop
169
+ - a dictionary's field read by name (`r.key`, `r[key]`), which may be absent although TypeScript says
170
+ it is not; `Object.entries()` anywhere but as a `v-for`'s source; dictionaries of optional values
171
+ - two strings that may each hold half of a surrogate pair compared, searched or joined (see
172
+ [Strings](#strings))
148
173
  - `null`
149
174
  - any method call without a Rust twin
150
175
 
151
176
  An object prop handed to a child component is cloned. Its strings are `Cow`s, so borrowed ones
152
177
  cost nothing to copy.
153
178
 
179
+ ### Scoped styles
180
+
181
+ A `<style scoped>` component's elements carry `data-v-<id>`, and its CSS is rewritten by the client
182
+ build to select them. The server has to write the id the client build chose, which is not something
183
+ the browser checks when it hydrates: a wrong id hydrates cleanly and leaves the styles unapplied. So
184
+ ferrovue computes it as `@vitejs/plugin-vue` does — the first 8 hex digits of a SHA-256 of the
185
+ `.vue` file's path from Vite's root, followed by its source unless only the path is hashed — and the
186
+ two must be configured alike:
187
+
188
+ | `@vitejs/plugin-vue` | `ferrovue.config.json` |
189
+ |---|---|
190
+ | `vite build` with the default options | `"scopeId": "filepath-source"` (the default) |
191
+ | `features: { componentIdGenerator: "filepath" }` (any mode), or the dev server | `"scopeId": "filepath"` |
192
+
193
+ Set `viteRoot` when Vite's root is not the directory holding `ferrovue.config.json`. The Vite
194
+ plugin (`ferrovue/vite`) compares the two when a component has scoped styles: a build in which they
195
+ differ fails, and the dev server warns.
196
+
197
+ The default is the plugin's production behaviour because the production build is the one readers
198
+ get: with every option left alone, its styles apply. But plugin-vue hashes the path alone in its dev
199
+ server, so with the default a page rendered during development carries other ids than the dev
200
+ client and shows unstyled. The recommended setup is `componentIdGenerator: "filepath"` with
201
+ `"scopeId": "filepath"`, as [`examples/fullstack`](examples/fullstack) does: the ids are then the
202
+ same in development and production, and do not change, nor change the generated Rust, whenever a
203
+ component's source does.
204
+
205
+ A component a parent may hand ids to has a `render_scoped(…, attrs)` beside `render`, which
206
+ generated parents call. A component whose outlets pass a slot scope id (`:slotted()`) takes slot
207
+ content that is given it: `Slot::slotted` for a slot, a third `&str` parameter for a scoped slot's
208
+ closure. `render`, and content from Rust, need none of it: markup written from Rust carries no ids.
209
+
210
+ Where Vue's own server render gives other ids than its client render, ferrovue writes the server's:
211
+ a `:slotted()` component's slot fallback, which only the client gives the slot scope id, and a
212
+ component with `inheritAttrs: false` that is another component's root, whose root only the client
213
+ gives the ids that other component inherits.
214
+
215
+ ### Strings
216
+
217
+ String methods count as JavaScript counts: in UTF-16 code units, so `"🦀".length` is 2 and
218
+ `"🦀 crab".slice(3)` is `"crab"`. Strings are ordered by code unit too, which puts every character
219
+ from U+E000 to U+FFFF after one beyond U+FFFF. Each runtime routine is held to vectors recorded from
220
+ JavaScript (`crates/ferrovue/tests/vectors/`).
221
+
222
+ A JavaScript string can hold half of a surrogate pair — `"🦀".slice(0, 1)`, `.charAt(1)`,
223
+ `.split("")` — and a Rust string cannot. ferrovue writes U+FFFD in its place, which is exactly what
224
+ the page carries anyway: a server sends Vue's string as UTF-8, and UTF-8 writes each half as U+FFFD
225
+ (`res.end`, `Buffer.from` and `TextEncoder` all do). Its length is the same. Where the half itself
226
+ would decide the result, ferrovue refuses at compile time: two strings that may each hold a half
227
+ compared or ordered, side by side (`a.slice(0, 1) + b.slice(1)`, a `.join("")`, a class object's
228
+ names), one searched for in another (`.includes(a.charAt(0))`), repeated or padded where halves
229
+ would join, ordered against anything but a literal below U+D800, or written by `JSON.stringify`
230
+ (which escapes it) or into a `<RouterLink>` (whose encoding throws). Three corners remain, by design:
231
+
232
+ - a string holding U+FFFD in the props compares equal to a half, or finds one, where JavaScript
233
+ would tell them apart;
234
+ - a half handed to a child component or a helper is U+FFFD there, which matters only if that
235
+ component then compares or joins it as above;
236
+ - Vue itself cannot hydrate such text cleanly: the browser reads U+FFFD where its own render holds
237
+ the half, and reports a mismatch.
238
+
239
+ Where JavaScript throws, the generated code panics, as Vue's render rejects: a negative or infinite
240
+ `.repeat()` count, and a `.repeat()`, `.padStart()` or `.padEnd()` past the longest string V8 makes
241
+ (2²⁹ − 24 code units).
242
+
243
+ ### Dictionaries
244
+
245
+ A `Record<string, T>` prop is a `ferrovue::Record`, built from pairs —
246
+ `[("b", 1), ("10", 2)].into_iter().collect()` — or read from JSON. It holds its keys in the order a
247
+ JavaScript object does, array indices (`"0"` to `"4294967294"`) first in numeric order, so `v-for`
248
+ walks them as Vue does, and it is written back as JSON in that order, which the browser reads back
249
+ the same.
250
+
251
+ ### Translating
252
+
253
+ With `i18n` configured, the generated `i18n` module holds every locale's messages. Build an `I18n` per
254
+ request, in the reader's locale, and pass it to the components that translate:
255
+
256
+ ```rust
257
+ let i18n = generated::i18n::i18n("nl"); // falls back as configured
258
+ page::render(&mut out, &props, &i18n);
259
+ ```
260
+
261
+ ### Teleports
262
+
263
+ A component that renders a `<Teleport>` takes a `ferrovue::Teleports`. After rendering the page body,
264
+ write what was teleported where each target is, as Vue's `ssrContext.teleports` is written:
265
+
266
+ ```rust
267
+ let teleports = ferrovue::Teleports::new();
268
+ page::render(&mut body, &props, &teleports);
269
+ let modals = teleports.get("#modals").unwrap_or_default(); // inside <div id="modals">
270
+ ```
271
+
272
+ As Vue recommends, teleport to a dedicated element (`#modals`) rather than `body`: the browser hydrates
273
+ a target from its first node, and `body` also holds the app.
274
+
154
275
  ### Scoped slots from Rust
155
276
 
156
277
  A scoped slot's `Slots` field takes a closure that receives the props its outlet passes, as a
@@ -182,6 +303,54 @@ hydrateState(pinia); // before app.mount(): every store starts from what the ser
182
303
  `split_holes` the output, and stream the pieces with each hole's content between them, in whatever
183
304
  order the content is ready.
184
305
 
306
+ ## Performance
307
+
308
+ Time to render one component to an HTML string. On the left is Vue's `renderToString` on Node, and
309
+ on the right is the Rust that ferrovue generated from the same `.vue` file. Both get the same props,
310
+ and both write the same bytes: each side checks its output against
311
+ `crates/ferrovue/benches/expected/<scenario>.html` before anything is timed.
312
+
313
+ | Scenario | What renders | Output | Vue `renderToString` | ferrovue | Speed-up |
314
+ |---|---|---|---|---|---|
315
+ | `small` | `Nav`: two `<RouterLink>`s resolved against the current route | 206 B | 34.57 µs | 0.950 µs | 36.4× |
316
+ | `list` | `Lists`: 1,000 words, 1,000 numbers, 100 groups of 10 members | 102 KiB | 368.1 µs | 45.95 µs | 8.0× |
317
+ | `tree` | `Tree`: a recursive component, binary tree 8 levels deep (255 nodes) | 9.5 KiB | 484.6 µs | 3.72 µs | 130.2× |
318
+ | `page` | `Dashboard`: 22 `Panel`s with named slots, a `Text`, 20 `Frame`s holding loops | 8.0 KiB | 181.6 µs | 4.45 µs | 40.9× |
319
+
320
+ These are mean times per render. The Vue column is tinybench's mean, with 5 s per scenario after
321
+ a 1 s warm-up. The ferrovue column is criterion's mean point estimate, using its defaults of a 3 s
322
+ warm-up and 100 samples.
323
+
324
+ What each side measures:
325
+
326
+ - **Vue**: one awaited `renderToString(app)`. Vue is the production build (`NODE_ENV=production`).
327
+ Each component uses the `ssrRender` its SSR build would have. The app is created once outside
328
+ the loop, along with its Pinia instance and, for `small`, a vue-router already at `/users/me`.
329
+ - **ferrovue**: a new `String` plus one call of the generated `render`, built with the release
330
+ profile. The props, the router and the `Route` are built once outside the loop.
331
+
332
+ Measured on an AMD Ryzen 5 3600XT (6 cores / 12 threads, up to 3.8 GHz) with 62 GiB of RAM, running
333
+ Linux 7.2.8-2-cachyos (CachyOS, x86_64). Software: Node 26.10.0, Vue 3.5.43, rustc 1.99.0. The
334
+ machine was lightly loaded (a load average of about 1) but not idle, so treat the numbers as
335
+ indicative.
336
+
337
+ The speed-up says as much about Vue as about ferrovue. Most of Vue's time goes on component
338
+ instances and their virtual nodes, of which `tree` has 255 and `list` one: per byte written, Vue
339
+ renders `list` some fourteen times faster than `tree`, while ferrovue writes the two at a similar
340
+ rate.
341
+
342
+ To reproduce:
343
+
344
+ ```sh
345
+ pnpm bench # both halves: pnpm bench:rust, then pnpm bench:js
346
+ pnpm bench:rust # cargo bench -p ferrovue --bench render (crates/ferrovue/benches/render.rs)
347
+ pnpm bench:js # Vue's renderToString with tinybench (packages/ferrovue/bench/ssr.bench.ts)
348
+ pnpm bench:record # rewrite benches/expected/*.html from Vue, after changing a scenario
349
+ ```
350
+
351
+ The benchmarks are not part of `pnpm test`. CI runs both in a short mode only to check that they
352
+ still build, run and agree on the HTML. It never compares their timings.
353
+
185
354
  ## Versions
186
355
 
187
356
  Byte-identical output depends on Vue's own SSR compiler, so the package pins **`vue`,
@@ -193,16 +362,29 @@ ferrovue version.
193
362
 
194
363
  ```text
195
364
  crates/ferrovue/ the Rust runtime crate
196
- src/ runtime + unit tests
365
+ src/ runtime (escaping, JS numbers, router, i18n, teleports) + unit tests
197
366
  tests/conformance/ components, fixtures, recorded HTML, generated Rust
198
- tests/vectors/ string and router vectors shared with the TypeScript tests
367
+ tests/vectors/ vectors recorded from JavaScript, vue-router and vue-i18n
199
368
  tests/properties.rs property-based tests of the runtime
369
+ benches/ criterion benchmarks of generated renderers (see Performance)
200
370
  packages/ferrovue/ the compiler (npm package)
201
- src/compiler.ts .vue → Rust
202
- src/cli.ts the `ferrovue` command
203
- src/client.ts browser-side helpers
371
+ src/compiler.ts the API: `generate`, `write`
372
+ src/script.ts, template.ts, expr.ts, attrs.ts
373
+ <script setup>, the compiled template, expressions, class/style
374
+ src/router.ts, stores.ts, i18n.ts
375
+ vue-router, Pinia and vue-i18n
376
+ src/rust.ts, emitter.ts the Rust source written out
377
+ src/cli.ts, vite.ts the `ferrovue` command and the Vite plugin
378
+ src/client.ts browser-side helpers: `mountIslands`, `hydrateState`
204
379
  src/testing.ts utilities for a project's own conformance suite
205
- test/ compiler, CLI, router, vector and conformance tests
380
+ src/types.ts `ferrovue/types`: `TrustedHtml`, `Float`
381
+ test/ compiler, CLI, router, vector, island, Vite and conformance tests
382
+ browser/ conformance fixtures hydrated in real browsers (`pnpm test:browser`)
383
+ bench/ Vue renderToString benchmarks, the other half of Performance
384
+ fuzz/ the randomised differential tester (`pnpm fuzz`)
385
+ examples/greeting/ the smallest setup: one component rendered from Rust
386
+ examples/fullstack/ axum + Vite: islands, Pinia state, routes and streaming
387
+ scripts/release.ts the release version bump (see RELEASING.md)
206
388
  ```
207
389
 
208
390
  ## Development
@@ -214,7 +396,11 @@ pnpm typecheck
214
396
  cargo test --all-features
215
397
  ```
216
398
 
217
- [TESTING.md](TESTING.md) explains how the suite fits together and how to add a case.
399
+ [TESTING.md](TESTING.md) explains how the suite fits together and how to add a case,
400
+ [CONTRIBUTING.md](CONTRIBUTING.md) how to add support for a Vue construct, and
401
+ [RELEASING.md](RELEASING.md) how a release is cut.
402
+
403
+ [`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.
218
404
 
219
405
  ## Licence
220
406
 
@@ -0,0 +1,51 @@
1
+ import { type N, type Scope, type Val } from "./model.ts";
2
+ import { Emitter } from "./emitter.ts";
3
+ /** `toDisplayString`, escaped. */
4
+ export declare function interpolate(e: Emitter, v: Val): void;
5
+ /** The value half of `key="value"`, escaped. */
6
+ export declare function attrValue(e: Emitter, v: Val): void;
7
+ /** `ssrRenderAttr(key, value)`: absent for null/undefined, `key="value"` for anything else. */
8
+ export declare function renderAttr(e: Emitter, key: string, v: Val): void;
9
+ /** `ssrRenderDynamicAttr(key, value)`: as `renderAttr`, but a boolean attribute is present or
10
+ * absent, and an empty string renders the name alone. */
11
+ export declare function renderDynamicAttr(s: Scope, e: Emitter, key: string, v: Val, n: N): void;
12
+ /** One item of a class list once it is flattened: literal text, known now, or a Rust `&str`
13
+ * expression evaluated at run time — empty when the item contributes nothing. */
14
+ export type ClassItem = {
15
+ lit: string;
16
+ } | {
17
+ code: string;
18
+ };
19
+ /** `normalizeClass`'s view of a class binding, flattened: a string, an array of items, an object
20
+ * of `name: condition`, `cond && "name"`, `cond ? "a" : null`. Every item is trimmed and the empty
21
+ * ones dropped before they are joined with one space, which is what `class_into` does too, so an
22
+ * object's keys become items of their own. */
23
+ export declare function classItems(s: Scope, n: N): ClassItem[];
24
+ /** `ssrRenderClass(value)`: normalised, then escaped. `after` says a class was already written. */
25
+ export declare function renderClass(s: Scope, e: Emitter, n: N, after?: boolean): void;
26
+ export declare const IGNORED_PROPS: Set<string>;
27
+ /** `ssrGetDirectiveProps(_ctx, dir)`: a custom directive's server props, which are none for a
28
+ * directive the configuration declares client-only. `false` when `n` is not such a call. */
29
+ export declare function directiveProps(s: Scope, n: N): boolean;
30
+ /** `ssrRenderAttrs(obj)`, key by key in the object's order, and the inherited scope ids where the
31
+ * root's `_attrs` is merged in. */
32
+ export declare function renderAttrs(s: Scope, e: Emitter, n: N): void;
33
+ /** `mergeProps(a, _attrs, b, …)` as one object literal, without `_attrs`: it holds only scope ids,
34
+ * which the caller writes after the rest. Of the rest, `class` and `style` values are merged as an
35
+ * array, and any other key keeps the place it first had with the last value given. */
36
+ export declare function mergeProps(s: Scope, n: N): N;
37
+ /** One object of a style binding, and the run-time condition under which it is part of it. */
38
+ export interface StyleItem {
39
+ cond: string | null;
40
+ entries: {
41
+ key: string;
42
+ css: string;
43
+ value: N;
44
+ }[];
45
+ }
46
+ /** The objects a style binding merges, in order: an object literal, a static `style` the compiler
47
+ * parsed, `v-show`'s `cond ? null : { display: "none" }`, and arrays of those. */
48
+ export declare function styleItems(s: Scope, n: N, when: string | null): StyleItem[];
49
+ /** `ssrRenderStyle(value)`: a string as it is, or the objects merged — each property where it first
50
+ * appears, with the last value given — and written `name:value;`, all escaped. */
51
+ export declare function renderStyle(s: Scope, e: Emitter, n: N): void;