ferrovue 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +201 -50
  2. package/dist/attrs.d.ts +2 -1
  3. package/dist/attrs.js +7 -4
  4. package/dist/children.d.ts +2 -2
  5. package/dist/children.js +66 -13
  6. package/dist/classes.d.ts +4 -0
  7. package/dist/classes.js +23 -1
  8. package/dist/client.d.ts +9 -4
  9. package/dist/client.js +106 -17
  10. package/dist/compiler.d.ts +1 -1
  11. package/dist/compiler.js +38 -10
  12. package/dist/component.d.ts +1 -1
  13. package/dist/component.js +2 -3
  14. package/dist/conformance.d.ts +64 -0
  15. package/dist/conformance.js +276 -0
  16. package/dist/context.d.ts +15 -6
  17. package/dist/context.js +3 -0
  18. package/dist/dynamic.d.ts +18 -0
  19. package/dist/dynamic.js +576 -0
  20. package/dist/errors.d.ts +168 -1
  21. package/dist/errors.js +96 -1
  22. package/dist/file-routes.d.ts +33 -0
  23. package/dist/file-routes.js +224 -0
  24. package/dist/fixture.d.ts +16 -0
  25. package/dist/fixture.js +22 -7
  26. package/dist/index.d.ts +1 -1
  27. package/dist/link-router.d.ts +1 -1
  28. package/dist/link-router.js +1 -0
  29. package/dist/lists.d.ts +4 -0
  30. package/dist/lists.js +2 -2
  31. package/dist/loops.js +4 -4
  32. package/dist/model.d.ts +11 -0
  33. package/dist/model.js +44 -1
  34. package/dist/numbers.js +3 -2
  35. package/dist/page-routes.d.ts +9 -0
  36. package/dist/page-routes.js +10 -0
  37. package/dist/plugin.d.ts +13 -1
  38. package/dist/plugin.js +3 -0
  39. package/dist/plugins/client-only.js +3 -0
  40. package/dist/plugins/head.d.ts +15 -0
  41. package/dist/plugins/head.js +385 -0
  42. package/dist/plugins/index.js +3 -1
  43. package/dist/plugins/provide.d.ts +44 -0
  44. package/dist/plugins/provide.js +528 -0
  45. package/dist/plugins/router-link.js +7 -3
  46. package/dist/plugins/router.d.ts +4 -1
  47. package/dist/plugins/router.js +56 -10
  48. package/dist/plugins/stores.js +19 -9
  49. package/dist/routes.d.ts +5 -2
  50. package/dist/routes.js +8 -2
  51. package/dist/rust.js +46 -18
  52. package/dist/script.js +19 -3
  53. package/dist/settle.d.ts +2 -0
  54. package/dist/settle.js +5 -0
  55. package/dist/slots.js +18 -4
  56. package/dist/ssr.d.ts +3 -0
  57. package/dist/ssr.js +29 -0
  58. package/dist/strings.d.ts +3 -0
  59. package/dist/strings.js +16 -2
  60. package/dist/template.js +92 -11
  61. package/dist/testing.d.ts +2 -3
  62. package/dist/testing.js +2 -29
  63. package/dist/types.d.ts +5 -0
  64. package/dist/typescript.js +7 -3
  65. package/dist/vhtml.d.ts +4 -0
  66. package/dist/vhtml.js +63 -0
  67. package/dist/vite.d.ts +7 -1
  68. package/dist/vite.js +65 -5
  69. package/package.json +15 -1
package/README.md CHANGED
@@ -56,10 +56,10 @@ 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", "children" }`. 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" }`. 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
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
- | `trustedHtml` | no | Rust type of a `TrustedHtml` prop, e.g. `crate::html::Sanitised` (needed for `v-html`) |
62
+ | `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
63
  | `helpers` | no | `{ module, functions }`: functions a template may call, each mapped to a Rust twin |
64
64
  | `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
65
  | `i18n` | no | vue-i18n: `{ messages, locale?, fallbackLocale? }`, the directory of locale files (`en.json`, `nl.json`), the default locale and the fallbacks |
@@ -84,12 +84,12 @@ defineProps<{ name: string; unread: number; note?: string }>();
84
84
  ### 4. Generate and render
85
85
 
86
86
  ```sh
87
- npx ferrovue init # a starter ferrovue.config.json and components/Hello.vue
88
- npx ferrovue # writes src/generated/{greeting.rs, mod.rs}
89
- npx ferrovue --check # in CI: exits 1 if the committed modules are stale
90
- npx ferrovue --check --diff # …and shows what differs, as `diff -u` does
91
- npx ferrovue --check --format json # errors as JSON, for an editor or CI annotations
92
- npx ferrovue --watch # regenerates when components or configs change
87
+ pnpm ferrovue init # a starter ferrovue.config.json and components/Hello.vue
88
+ pnpm ferrovue # writes src/generated/{greeting.rs, mod.rs}
89
+ pnpm ferrovue --check # in CI: exits 1 if the committed modules are stale
90
+ pnpm ferrovue --check --diff # …and shows what differs, as `diff -u` does
91
+ pnpm ferrovue --check --format json # errors as JSON, for an editor or CI annotations
92
+ pnpm ferrovue --watch # regenerates when components or configs change
93
93
  ```
94
94
 
95
95
  Every error carries a stable code, as `error[FV0602]: components/Card.vue:4:17: …`, documented in
@@ -115,8 +115,8 @@ let html: String = greeting::html(&props).into_string();
115
115
  let island = greeting::island(&props).into_string();
116
116
 
117
117
  // (`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 or teleports has `html()`,
119
- // and the page's own app mounts it; see examples/fullstack.)
118
+ // `data-props`. One that also takes slots, the route, stores, translations, teleports, provided
119
+ // values or the page head has `html()`, and the page's own app mounts it; see examples/fullstack.)
120
120
 
121
121
  // …or straight into a buffer you already hold:
122
122
  let mut page = String::from("<!doctype html><body>");
@@ -129,11 +129,22 @@ route.
129
129
 
130
130
  The router and vue-i18n are features of the crate, `router` and `i18n`, both on by default. An
131
131
  application with neither routes nor translations can leave them out, and build neither
132
- `ferrovue-router` nor `ferrovue-i18n`: `ferrovue = { version = "0.5", default-features = false }`.
132
+ `ferrovue-router` nor `ferrovue-i18n`: `ferrovue = { version = "0.6", default-features = false }`.
133
133
 
134
134
  With the `maud` feature, `ferrovue::Html` implements `maud::Render`, so `(greeting::html(&props))` can go
135
135
  straight into a `maud::html!` page.
136
136
 
137
+ For `v-html`, `ferrovue::BasicHtml` is built in: `BasicHtml::new(untrusted)` escapes everything but
138
+ `<b>`, `<i>`, `<em>`, `<strong>`, `<code>`, `<br>`, `<p>`, `<ul>`, `<ol>` and `<li>` written with no
139
+ attributes, and balances them; `BasicHtml::from_text(text)` turns plain text into paragraphs.
140
+ `ferrovue::InlineHtml` does the same with `<b>`, `<i>`, `<em>`, `<strong>`, `<code>` and `<br>`
141
+ only: it is what a prop typed `InlineHtml` from `ferrovue/types` is on the server, and the HTML
142
+ `v-html` may write inside a `<p>`.
143
+ With the `ammonia` feature, `ferrovue::Sanitised` is HTML cleaned by [ammonia](https://docs.rs/ammonia):
144
+ name it as `trustedHtml` and build a `v-html` prop with `Sanitised::new(untrusted)`, or
145
+ `Sanitised::with(&builder, untrusted)` for a policy of your own. The server renders the cleaned
146
+ string, and the island's `data-props` carries that same string to the client.
147
+
137
148
  With the `axum` feature, `Html` is an `IntoResponse`, and with `actix-web` a `Responder`: a handler
138
149
  responds with the component, as `text/html; charset=utf-8`. `into_html(props)` and
139
150
  `into_island(props)` take the props by value, so a handler that builds them returns the page:
@@ -147,7 +158,7 @@ async fn hello(Path(name): Path<String>) -> impl IntoResponse {
147
158
  With the `dioxus` feature, a component goes into a Dioxus 0.7 page (`dioxus-ssr` or fullstack):
148
159
  `{greeting::island(&props)}` in `rsx!` is the island element itself, with exactly the markup Vue
149
160
  hydrates inside it. See `examples/dioxus` and the crate guide's
150
- [Dioxus page](crates/ferrovue/docs/guide/dioxus.md).
161
+ [Dioxus page](https://github.com/Kinsmir/ferrovue/blob/main/crates/ferrovue/docs/guide/dioxus.md).
151
162
 
152
163
  ## What a component may use
153
164
 
@@ -157,11 +168,12 @@ ferrovue compiles `<script setup lang="ts">` components, and components with no
157
168
 
158
169
  | Area | Supported |
159
170
  |---|---|
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` |
171
+ | 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
172
  | 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
173
  | 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
174
  | 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`, `provide`, template refs (`ref(null)`, `useTemplateRef`) and functions are client-only. The template may name them only from event handlers, which the server drops |
175
+ | 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 |
176
+ | 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
177
  | 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
178
  | 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
179
  | 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` |
@@ -172,15 +184,17 @@ ferrovue compiles `<script setup lang="ts">` components, and components with no
172
184
  | `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
185
  | 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
186
  | Components | imported child components, `v-bind` of a child's own `Props`, `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 |
187
+ | `<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
188
  | 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
189
  | 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
190
  | 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
191
  | Forms | `v-model` on text inputs, checkboxes, radios, `<select>` and `<textarea>` (renders the initial state) |
179
192
  | 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. `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 |
193
+ | 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
194
  | 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
195
  | 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"`) |
196
+ | `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 |
197
+ | 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
198
 
185
199
  Refused at compile time, each with an error that names the construct:
186
200
 
@@ -188,7 +202,20 @@ Refused at compile time, each with an error that names the construct:
188
202
  - `<RouterView>` in a component with `<style scoped>`, which would give the page that component's
189
203
  id; and, since vue-router renders a link from virtual nodes, a `<slot>` inside a `<RouterLink>`
190
204
  that takes scope ids, or an element inside one in slot content given a `:slotted()` id
191
- - `<component :is>`
205
+ - `<component :is>` over an open set (a `string`, a `Component`, a key that is not a literal union),
206
+ over a prop that may be absent, or naming a key its object lacks or a tag that is not an HTML
207
+ element; `v-html` or `v-text` on it; and, inside an element it chooses (which Vue renders from
208
+ virtual nodes), a `<slot>` with fallback content, `v-show`, and `v-model` on a `<select>`
209
+ - in provide and inject: a key that is neither a string literal nor an `InjectionKey` symbol exported
210
+ from a `.ts` file, `inject(key)!`, a value that may be `null` or `undefined`, values of different
211
+ types (or a ref and a plain value) under one key, setup that assigns to an injected value,
212
+ `provide` in a component holding `<RouterView>`, a string key injected inside a Rust twin's slot,
213
+ and `provide(…)` not imported from `vue`
214
+ - `v-show`, and `v-model` on a `<select>`, in the content of a `<RouterLink>` or of a Rust twin's
215
+ slot, which Vue renders from virtual nodes
216
+ - in the page head: options given to `useHead`, `useHeadSafe`, `templateParams`, a `titleTemplate`
217
+ function, event handlers (`onload`), a `class` or `style` that may be `null` (on which unhead's
218
+ renderer throws), and an object or array given to a `useSeoMeta` key
192
219
  - the Options API (a `<script>` without `setup`), and a type parameter of a generic component with
193
220
  no constraint (`generic="T"`)
194
221
  - constants that are not literals (`Date.now()`, a function), an object constant read whole or by a
@@ -197,6 +224,9 @@ Refused at compile time, each with an error that names the construct:
197
224
  - custom directives not listed in `clientDirectives`
198
225
  - `watchEffect`, `watch` with `immediate`, `onServerPrefetch`, top-level `await`, and statements in setup that change state
199
226
  - `route.meta` and `route.matched`
227
+ - in a folder of pages: a parameter beside text in one part of a path (`prefix-[id].vue`), repeatable
228
+ and optional catch-all parameters, parameter parsers (`[id=int]`), named views (`index@aside.vue`),
229
+ `definePage()`, a `<route>` block, and two pages, or a page and a component, with one component name
200
230
  - Pinia getters that read `this` or return a function
201
231
  - ordering comparisons between a string and a number, which JavaScript makes numeric
202
232
  - regular expressions (`.replace(/x/g, …)`, `.split(/,/)`), replacement functions, a search's
@@ -251,7 +281,7 @@ are, and `v-for="s in SIZES"` walks an array literal. A list of objects becomes
251
281
  the shared types, with a type for its items (here `SortsItem`, or the interface the constant is
252
282
  declared with):
253
283
 
254
- ```rust,ignore
284
+ ```rust
255
285
  pub const SORTS: &[SortsItem<'static>] = &[
256
286
  SortsItem { key: Cow::Borrowed("name"), label: Cow::Borrowed("By name") },
257
287
  SortsItem { key: Cow::Borrowed("price"), label: Cow::Borrowed("By price") },
@@ -284,7 +314,7 @@ The default is the plugin's production behaviour because the production build is
284
314
  get: with every option left alone, its styles apply. But plugin-vue hashes the path alone in its dev
285
315
  server, so with the default a page rendered during development carries other ids than the dev
286
316
  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
317
+ `"scopeId": "filepath"`, as [`examples/fullstack`](https://github.com/Kinsmir/ferrovue/tree/main/examples/fullstack) does: the ids are then the
288
318
  same in development and production, and do not change, nor change the generated Rust, whenever a
289
319
  component's source does.
290
320
 
@@ -347,8 +377,8 @@ page::render(&mut out, &props, &i18n);
347
377
 
348
378
  ### Escape hatches
349
379
 
350
- Some components will never compile: a component library's render functions, `inject` chains, code
351
- that measures the page in `onMounted`. Two ways keep one of them from blocking the page around it.
380
+ Some components will never compile: a component library's render functions, code that measures the
381
+ page in `onMounted`. Two ways keep one of them from blocking the page around it.
352
382
 
353
383
  **`<ClientOnly>`**: the server writes its `#fallback` slot (or a comment, without one), and the
354
384
  browser hydrates that same fallback, then swaps in the default slot once mounted. Both sides render
@@ -397,13 +427,11 @@ have. Prop types are those of helpers (`string`, `int?`, …); an absent `bool`
397
427
  bare `block` is `true`, as Vue casts them.
398
428
 
399
429
  **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: render the components that use the twin with Vue through
401
- `ferrovue/testing`'s `fixtureApp` and `attachSsrRender`, and with Rust through the generated
402
- `render_json`, and compare. Slot content reaches the twin as a component's render function sees it
430
+ 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
431
+ conformance suite (see [Testing against Vue](#testing-against-vue)). Slot content reaches the twin as a component's render function sees it
403
432
  (`<!--v-if-->` for an absent `v-if`, as Vue writes virtual nodes). The twin decides where it goes and
404
433
  whether fragment markers surround it, as the component does. `examples/fullstack` has a
405
- twin (`src/ui.rs`), its fixtures (`fixtures/`), the Vue half (`test/fixtures.test.ts`) and the Rust
406
- half (`src/fixtures.rs`).
434
+ twin (`src/ui.rs`) and its fixtures (`fixtures/`).
407
435
 
408
436
  ### Teleports
409
437
 
@@ -419,6 +447,25 @@ let modals = teleports.get("#modals").unwrap_or_default(); // inside <div id="mo
419
447
  As Vue recommends, teleport to a dedicated element such as `#modals`: the browser hydrates
420
448
  a target from its first node, and `body` also holds the app.
421
449
 
450
+ ### Page head
451
+
452
+ A component that calls `useHead` or `useSeoMeta` from `@unhead/vue` takes a `ferrovue::Head`, which
453
+ collects the calls of the whole tree in the order Vue's server runs them. After rendering the body,
454
+ write the head as unhead's server renderer writes it:
455
+
456
+ ```rust
457
+ let head = ferrovue::Head::new(); // createHead(): lang="en", charset and viewport; or Head::without_defaults()
458
+ book_page::render(&mut body, &props, &head);
459
+ let tags = head.render(); // head_tags, body_tags_open, body_tags, html_attrs, body_attrs
460
+ let page = format!("<html{}><head>{}</head><body{}>{}{}{}</body></html>",
461
+ tags.html_attrs, tags.head_tags, tags.body_attrs, tags.body_tags_open, body, tags.body_tags);
462
+ ```
463
+
464
+ On the client the head is unhead's: give the page's app its client head,
465
+ `mountPage(layout, islands, { plugins: [createHead()] })` with `createHead` from
466
+ `@unhead/vue/client`, and it takes over the tags the server wrote. The crate's
467
+ [`head`](https://docs.rs/ferrovue/latest/ferrovue/guide/head/index.html) guide has the details.
468
+
422
469
  ### Scoped slots from Rust
423
470
 
424
471
  A scoped slot's `Slots` field takes a closure that receives the props its outlet passes, as a
@@ -459,6 +506,25 @@ components alike, so a hand-written island can join them: `mountIslands({ ...isl
459
506
  An island whose name is not among them is left as the server rendered it, with a warning naming it
460
507
  (`onError` to handle it otherwise), as is one whose component fails to load.
461
508
 
509
+ An island can wait to hydrate, and to fetch its code, until it is needed. `hydrate` on the
510
+ server's `island()` writes a `data-hydrate` attribute on the wrapper, which `mountIslands` honours
511
+ with Vue's lazy hydration strategies:
512
+
513
+ ```rust
514
+ use ferrovue::Hydrate;
515
+
516
+ reviews::island(&props).hydrate(Hydrate::Visible); // scrolled into view
517
+ footer::island(&props).hydrate(Hydrate::visible_with("200px")); // within 200px of the view
518
+ chart::island(&props).hydrate(Hydrate::Idle); // the browser is idle
519
+ counter::island(&props).hydrate(Hydrate::Interaction); // pointerenter, click or focus
520
+ menu::island(&props).hydrate(Hydrate::InteractionOn(&["keydown"]));
521
+ sidebar::island(&props).hydrate(Hydrate::media("(min-width: 60rem)"));
522
+ ```
523
+
524
+ The island's markup is unchanged, so nothing about exactness changes. An event that woke an island
525
+ waiting for interaction is dispatched again once it has hydrated, so the first click is not lost.
526
+ An island without `data-hydrate` hydrates at once, as before.
527
+
462
528
  An island's `<style>` goes into its chunk too, and Vite links it only once the island's script has
463
529
  loaded, after the server's markup is on screen. Link those stylesheets from the page up front: a
464
530
  server reading Vite's manifest takes the `css` of the entry's `dynamicImports` as well as its own
@@ -466,7 +532,8 @@ server reading Vite's manifest takes the `css` of the entry's `dynamicImports` a
466
532
 
467
533
  An application whose own navigation layer leaves the page can keep `<RouterLink>` for its `href`s
468
534
  and active classes with `linkRouter(routes, { navigate })` from `ferrovue/link-router`: a router
469
- over the routes file whose every navigation after the first goes to `navigate`. The routing guide
535
+ over the routes file (or the default export of `ferrovue/routes`, with a folder of pages) whose
536
+ every navigation after the first goes to `navigate`. The routing guide
470
537
  (`ferrovue::guide::routing`) explains each part of it.
471
538
 
472
539
  ### Hydrating a whole page
@@ -509,17 +576,82 @@ import { hydrateState } from "ferrovue";
509
576
  hydrateState(pinia); // before app.mount(): every store starts from what the server rendered
510
577
  ```
511
578
 
579
+ ### Fixtures in an application's vitest run
580
+
581
+ `fixtureApp` from `ferrovue/testing` installs Pinia for a fixture's `$stores`, vue-router for its
582
+ routes and vue-i18n for its `i18n` option, importing each itself. A component's `useBasket()`,
583
+ `useRoute()` or `useI18n()` reads the instance its own import of the package gives it, so the two
584
+ must be one module instance. vitest loads `ferrovue/testing` from `node_modules` with Node, and
585
+ runs the application's code through Vite; where a peer goes through Vite too (listed in
586
+ `server.deps.inline`, aliased, linked from elsewhere, or installed twice), the fixture installs a
587
+ different instance from the one the component reads, and the render fails with Pinia's "there was
588
+ no active Pinia" or an undefined route. Pass the application's own modules in the options:
589
+
590
+ ```ts
591
+ const app = await fixtureApp(component, readFixture(json), routes, {
592
+ pinia: await import("pinia"),
593
+ vueRouter: await import("vue-router"),
594
+ vueI18n: await import("vue-i18n"),
595
+ });
596
+ ```
597
+
598
+ Without them, `fixtureApp` imports the peers itself, as before. Another way is to have vitest run
599
+ ferrovue through Vite as well, which puts it on the application's module graph; this also covers
600
+ `vue` itself, which `fixtureApp` always imports:
601
+
602
+ ```ts
603
+ // vitest.config.ts
604
+ export default defineConfig({ test: { server: { deps: { inline: ["ferrovue"] } } } });
605
+ ```
606
+
512
607
  ### Streaming with holes
513
608
 
514
609
  `ferrovue::hole()` is a slot whose content you write later. Render a layout with holes,
515
610
  `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` or `actix-web` feature, `ferrovue::HtmlStream` is that
517
- streamed response, from the render and a future for each hole's content:
611
+ order the content is ready. With the `stream` feature (which `axum` and `actix-web` turn on),
612
+ `ferrovue::HtmlStream` is that streamed body, from the render and a future for each hole's content:
518
613
 
519
614
  ```rust
520
615
  let body = ferrovue::HtmlStream::new(page).hole(async move { reviews_of(&id).await });
521
616
  ```
522
617
 
618
+ ### Testing against Vue
619
+
620
+ An application holds its own components to Vue as ferrovue's suite does, with a fixtures directory
621
+ and two calls. A fixture is a component's props as JSON (plus `$slots`, `$route`, `$stores` and
622
+ `$locale` where it takes them) beside the HTML Vue rendered for them:
623
+
624
+ ```text
625
+ fixtures/Reviews/three.json {"reviews":[{"reader":"Ada","stars":5,"text":"The spice must flow."}]}
626
+ fixtures/Reviews/three.html recorded from Vue with FERROVUE_FIXTURES_WRITE=1
627
+ ```
628
+
629
+ ```ts
630
+ // test/conformance.test.ts, run by vitest with environment: "happy-dom"
631
+ import { join } from "node:path";
632
+ import type { Component } from "vue";
633
+ import { conformanceSuite } from "ferrovue/testing";
634
+
635
+ await conformanceSuite({
636
+ config: join(import.meta.dirname, "../ferrovue.config.json"),
637
+ components: import.meta.glob<Component>("../client/components/*.vue", { eager: true, import: "default" }),
638
+ pinia: await import("pinia"),
639
+ vueRouter: await import("vue-router"),
640
+ });
641
+ ```
642
+
643
+ ```rust
644
+ ferrovue::conformance!("fixtures", generated::render_json, at_least = 24);
645
+ ```
646
+
647
+ The Vue half fails on a component with no fixtures, a fixture naming no component, a project with
648
+ no components, generated Rust that differs from what `ferrovue` writes now, a fixture Vue renders
649
+ differently from its `.html`, and a recorded `.html` that hydrates with any warning;
650
+ `FERROVUE_FIXTURES_WRITE=1` records the `.html` files instead. The Rust half renders every fixture through the generated `render_json`,
651
+ compares the bytes, and fails when fewer than `at_least` fixtures are found. The crate's
652
+ [`testing`](https://docs.rs/ferrovue/latest/ferrovue/guide/testing/index.html) guide has the
653
+ details; `examples/fullstack` uses both.
654
+
523
655
  ## Performance
524
656
 
525
657
  Time to render one component to an HTML string. On the left is Vue's `renderToString` on Node, and
@@ -529,10 +661,10 @@ and both write the same bytes: each side checks its output against
529
661
 
530
662
  | Scenario | What renders | Output | Vue `renderToString` | ferrovue | Speed-up |
531
663
  |---|---|---|---|---|---|
532
- | `small` | `Nav`: two `<RouterLink>`s resolved against the current route | 206 B | 41.39 µs | 0.962 µs | 43.0× |
533
- | `list` | `Lists`: 1,000 words, 1,000 numbers, 100 groups of 10 members | 102 KiB | 363.9 µs | 49.00 µs | 7.4× |
534
- | `tree` | `Tree`: a recursive component, binary tree 8 levels deep (255 nodes) | 9.5 KiB | 539.5 µs | 3.89 µs | 138.5× |
535
- | `page` | `Dashboard`: 22 `Panel`s with named slots, a `Text`, 20 `Frame`s holding loops | 8.0 KiB | 192.4 µs | 4.84 µs | 39.7× |
664
+ | `small` | `Nav`: two `<RouterLink>`s resolved against the current route | 206 B | 36.41 µs | 0.989 µs | 36.8× |
665
+ | `list` | `Lists`: 1,000 words, 1,000 numbers, 100 groups of 10 members | 102 KiB | 355.8 µs | 46.16 µs | 7.7× |
666
+ | `tree` | `Tree`: a recursive component, binary tree 8 levels deep (255 nodes) | 9.5 KiB | 522.2 µs | 3.86 µs | 135.2× |
667
+ | `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
668
 
537
669
  These are mean times per render. The Vue column is tinybench's mean, with 5 s per scenario after
538
670
  a 1 s warm-up. The ferrovue column is criterion's mean point estimate, using its defaults of a 3 s
@@ -580,6 +712,7 @@ of Vue and of each integration. Your project may use any later patch of the same
580
712
  | `vue-router` (optional) | `~5.3.1` | 5.3.1 |
581
713
  | `pinia` (optional) | `~4.0.3` | 4.0.3 |
582
714
  | `vue-i18n` (optional) | `~11.4.13` | 11.4.13 |
715
+ | `@unhead/vue` (optional) | `~3.4.2` | 3.4.2 |
583
716
 
584
717
  A new minor of any of them (Vue 3.6, vue-router 5.4, …) needs a ferrovue release that re-records
585
718
  the fixtures from it. A weekly CI job re-records them from the newest patch each range allows and
@@ -591,10 +724,15 @@ before it reaches you; a ferrovue patch follows when one does.
591
724
  ```text
592
725
  crates/ferrovue/ the Rust runtime crate, which generated code calls
593
726
  src/ a module per part (`Html`, slots, class and style, the state script,
594
- strings, teleports, fallthrough attributes), each module's unit tests
595
- beside it in <module>/tests.rs; lib.rs re-exports them all, and the
596
- three crates below
727
+ strings, teleports, the page head, fallthrough attributes, `BasicHtml`
728
+ and `InlineHtml`,
729
+ `Sanitised`, the fixture check `conformance!` writes), each module's
730
+ unit tests beside it in <module>/tests.rs; lib.rs re-exports them all,
731
+ and the three crates below
732
+ docs/guide/ the crate guide, `ferrovue::guide` on docs.rs
597
733
  tests/conformance/ components, fixtures, recorded HTML, generated Rust
734
+ tests/literal_props/ components compiled with `"builders": false`
735
+ tests/sanitised/ a component whose `TrustedHtml` is `ferrovue::Sanitised`
598
736
  tests/vectors/ vectors recorded from JavaScript and Vue
599
737
  tests/properties.rs property-based tests of the runtime
600
738
  benches/ criterion benchmarks of generated renderers (see Performance)
@@ -606,36 +744,49 @@ crates/ferrovue-i18n/ vue-i18n's t() (the `i18n` feature)
606
744
  packages/ferrovue/ the compiler (npm package)
607
745
  src/index.ts `ferrovue`: the browser API, `mountIslands`, `mountPage`, `hydrateState` and the types
608
746
  src/compiler.ts `ferrovue/compiler`: `generate`, `write`
747
+ src/context.ts, model.ts `ferrovue.config.json`, and the types and values the compiler passes around
609
748
  src/component.ts, script.ts, typescript.ts, constants.ts
610
749
  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
- src/expr.ts, strings.ts, numbers.ts, narrowing.ts, calls.ts, lists.ts
614
- expressions: operators, strings, numbers, narrowing, calls, lists
750
+ src/template.ts, children.ts, slots.ts, loops.ts, vhtml.ts
751
+ the compiled template: statements, child components, slots, v-for,
752
+ where `v-html` may go
753
+ src/expr.ts, strings.ts, numbers.ts, narrowing.ts, calls.ts, lists.ts, parens.ts
754
+ expressions: operators, strings, numbers, narrowing, calls, lists,
755
+ the parentheses Rust needs
615
756
  src/attrs.ts, classes.ts, styles.ts, fallthrough.ts
616
757
  attributes, class and style, attributes a parent passes on
758
+ src/dynamic.ts `<component :is>` over a closed set of choices
617
759
  src/plugin.ts the plugin interface (see CONTRIBUTING.md)
618
760
  src/plugins/ vue-router, Pinia, vue-i18n, scoped styles, <Teleport>, shared types,
619
- <ClientOnly>, Rust twins
761
+ <ClientOnly>, Rust twins, provide and inject, the page head
620
762
  src/rust.ts, emitter.ts the Rust source written out
621
763
  src/cli.ts, vite.ts the `ferrovue` command and the Vite plugin
764
+ src/diff.ts the diff `ferrovue --check --diff` prints for a stale file
622
765
  src/errors.ts, diagnostics.ts
623
766
  every error code with its title, and errors as the CLI, JSON and Vite show them
624
767
  src/client.ts browser-side helpers: `mountIslands`, `mountPage`, `hydrateState`, `<ClientOnly>`
625
768
  src/islands.ts `ferrovue/islands`, which the Vite plugin writes: every island, loaded lazily
626
769
  src/link-router.ts `ferrovue/link-router`: `<RouterLink>` while the application navigates on its own
627
770
  src/routes.ts a routes file as vue-router's route records
628
- src/testing.ts utilities for a project's own conformance suite
771
+ src/file-routes.ts the routes vue-router's file-based routing builds from a folder of pages
772
+ src/page-routes.ts `ferrovue/routes`, which the Vite plugin writes from the pages
773
+ src/testing.ts `ferrovue/testing`: utilities for a project's own conformance suite
774
+ src/fixture.ts, settle.ts `fixtureApp` and `readFixture`, and waiting for async components to settle
775
+ src/conformance.ts `conformanceSuite`, which `ferrovue/testing` exports
776
+ src/ssr.ts `attachSsrRender`, which `ferrovue/testing` exports
629
777
  src/hydration.ts `hydrateRecordedPage`, which `ferrovue/testing` exports
630
- src/types.ts `ferrovue/types`: `TrustedHtml`, `Float`
778
+ src/types.ts `ferrovue/types`: `TrustedHtml`, `InlineHtml`, `Float`
631
779
  test/ compiler, CLI, router, vector, island, Vite and conformance tests
632
780
  browser/ conformance fixtures and recorded pages hydrated in real browsers (`pnpm test:browser`)
633
781
  bench/ Vue renderToString benchmarks, the other half of Performance
634
782
  fuzz/ the randomised differential tester (`pnpm fuzz`)
635
783
  examples/greeting/ the smallest setup: one component rendered from Rust
636
- examples/fullstack/ axum + Vite: islands, a page hydrated whole, Pinia state, routes and
637
- streaming, <ClientOnly>, a Rust twin and its fixtures
784
+ examples/fullstack/ axum + Vite: islands (some hydrating later), a page hydrated whole,
785
+ Pinia state, routes from a folder of pages, the page head, streaming,
786
+ <ClientOnly>, a Rust twin, and a conformance suite
638
787
  examples/dioxus/ a Dioxus page, rendered with dioxus-ssr, with an island in it
788
+ scripts/ferrovue-in.ts the `ferrovue` command run in a project of this repository (`pnpm conformance:check`, CI)
789
+ scripts/inspect.ts Vue's SSR code and render of a component beside the Rust ferrovue generates for it
639
790
  scripts/release.ts the release version bump (see RELEASING.md)
640
791
  ```
641
792
 
@@ -648,15 +799,15 @@ pnpm typecheck
648
799
  cargo test --all-features
649
800
  ```
650
801
 
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.
802
+ [TESTING.md](https://github.com/Kinsmir/ferrovue/blob/main/TESTING.md) explains how the suite fits together and how to add a case,
803
+ [CONTRIBUTING.md](https://github.com/Kinsmir/ferrovue/blob/main/CONTRIBUTING.md) how to add support for a Vue construct, and
804
+ [RELEASING.md](https://github.com/Kinsmir/ferrovue/blob/main/RELEASING.md) how a release is cut.
654
805
 
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.
806
+ [`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
807
 
657
808
  ## Licence
658
809
 
659
- Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or [MIT licence](LICENSE-MIT),
810
+ 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
811
  at your option. Unless you explicitly state otherwise, any contribution you intentionally submit for
661
812
  inclusion in this work, as defined in the Apache-2.0 licence, is dual-licensed as above, without any
662
813
  additional terms or conditions.
package/dist/attrs.d.ts CHANGED
@@ -4,7 +4,8 @@ export declare function display(e: Emitter, v: Val): boolean;
4
4
  export declare function interpolate(s: Scope, e: Emitter, v: Val, n: N): void;
5
5
  export declare function attrValue(e: Emitter, v: Val): void;
6
6
  export declare function renderAttr(s: Scope, e: Emitter, key: string, v: Val, n: N): void;
7
- export declare function renderDynamicAttr(s: Scope, e: Emitter, key: string, v: Val, n: N): void;
7
+ export declare function keepsAttrCase(tag: string): boolean;
8
+ export declare function renderDynamicAttr(s: Scope, e: Emitter, key: string, v: Val, n: N, tag?: string | null): void;
8
9
  export declare const IGNORED_PROPS: Set<string>;
9
10
  export declare function directiveProps(s: Scope, n: N): boolean;
10
11
  export declare function isAttrs(n: N): boolean;
package/dist/attrs.js CHANGED
@@ -1,4 +1,4 @@
1
- import { escapeHtml, hyphenate, isBooleanAttr, isSSRSafeAttrName, propsToAttrMap } from "@vue/shared";
1
+ import { escapeHtml, hyphenate, isBooleanAttr, isSSRSafeAttrName, isSVGTag, propsToAttrMap } from "@vue/shared";
2
2
  import { fail, nothing, GenError, rustStr } from "./model.js";
3
3
  import { CONFIG_FILE, ctx } from "./context.js";
4
4
  import { describeTy, expr } from "./expr.js";
@@ -75,8 +75,11 @@ export function renderAttr(s, e, key, v, n) {
75
75
  attrValue(e, v);
76
76
  e.lit(`"`);
77
77
  }
78
- export function renderDynamicAttr(s, e, key, v, n) {
79
- const name = propsToAttrMap[key] ?? key.toLowerCase();
78
+ export function keepsAttrCase(tag) {
79
+ return tag.indexOf("-") > 0 || isSVGTag(tag);
80
+ }
81
+ export function renderDynamicAttr(s, e, key, v, n, tag = null) {
82
+ const name = tag !== null && keepsAttrCase(tag) ? key : (propsToAttrMap[key] ?? key.toLowerCase());
80
83
  if (!isSSRSafeAttrName(name))
81
84
  fail(s.comp, "FV0404", `unsafe attribute name \`${name}\``, n);
82
85
  renderable(s, key, v, n);
@@ -91,7 +94,7 @@ export function renderDynamicAttr(s, e, key, v, n) {
91
94
  }
92
95
  if (v.ty.k === "opt") {
93
96
  e.open(`if let Some(v) = ${v.code}`);
94
- renderDynamicAttr(s, e, key, { code: "v", ty: v.ty.of }, n);
97
+ renderDynamicAttr(s, e, key, { code: "v", ty: v.ty.of }, n, tag);
95
98
  e.close();
96
99
  return;
97
100
  }
@@ -10,7 +10,7 @@ export declare function renderChild(s: Scope, e: Emitter, n: N, twin?: TwinCall)
10
10
  export declare function array(key: string): boolean;
11
11
  export declare function fieldInit(name: string, value: string): string;
12
12
  export declare function takesSlots(c: Component): boolean;
13
- export declare function extraParams(c: Component): string;
14
- export declare function callChild(s: Scope, e: Emitter, child: Component, propsCode: string, slots: N, attrs: string | null): void;
13
+ export declare function extraParams(c: Component, unread?: (name: string) => boolean): string;
14
+ export declare function callChild(s: Scope, e: Emitter, child: Component, propsCode: string, slots: N, attrs: string | null, later?: boolean): void;
15
15
  export declare function callWith(s: Scope, e: Emitter, child: Component, m: string, head: string, slotsTy: string, trailing: string, slots: N, vnode?: boolean): void;
16
16
  export declare function ownInto(comp: Component, v: Val, want: Ty, node: N): string;