ferrovue 0.13.0 → 0.14.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 (51) hide show
  1. package/README.md +9 -5
  2. package/dist/attrs.js +62 -4
  3. package/dist/calls.js +18 -6
  4. package/dist/children.js +29 -8
  5. package/dist/classes.js +6 -1
  6. package/dist/client.d.ts +4 -4
  7. package/dist/client.js +4 -4
  8. package/dist/compiler.js +2 -1
  9. package/dist/component.js +2 -2
  10. package/dist/conditional.js +3 -3
  11. package/dist/config.d.ts +4 -0
  12. package/dist/conformance.js +2 -2
  13. package/dist/context.d.ts +4 -0
  14. package/dist/context.js +8 -0
  15. package/dist/errors.d.ts +14 -6
  16. package/dist/errors.js +14 -6
  17. package/dist/link-router.d.ts +5 -2
  18. package/dist/link-router.js +3 -1
  19. package/dist/link-routes.d.ts +35 -0
  20. package/dist/link-routes.js +435 -0
  21. package/dist/logical.js +3 -4
  22. package/dist/loops.js +16 -11
  23. package/dist/members.d.ts +3 -0
  24. package/dist/members.js +23 -11
  25. package/dist/model.d.ts +15 -0
  26. package/dist/model.js +33 -0
  27. package/dist/narrowing.js +13 -8
  28. package/dist/numbers.d.ts +2 -0
  29. package/dist/numbers.js +35 -1
  30. package/dist/operators.js +2 -2
  31. package/dist/page-routes.d.ts +1 -1
  32. package/dist/page-routes.js +1 -1
  33. package/dist/parens.d.ts +2 -0
  34. package/dist/parens.js +65 -1
  35. package/dist/plugins/router-link.js +39 -23
  36. package/dist/rust.d.ts +1 -1
  37. package/dist/rust.js +1 -1
  38. package/dist/schema.js +1 -0
  39. package/dist/script.d.ts +0 -2
  40. package/dist/script.js +24 -13
  41. package/dist/setup-writes.d.ts +15 -0
  42. package/dist/setup-writes.js +369 -0
  43. package/dist/slots.js +13 -2
  44. package/dist/ssr.d.ts +7 -1
  45. package/dist/ssr.js +4 -3
  46. package/dist/strings.js +24 -6
  47. package/dist/template.js +9 -1
  48. package/dist/vite.d.ts +1 -1
  49. package/dist/vite.js +30 -3
  50. package/package.json +5 -1
  51. package/schema.json +5 -0
package/README.md CHANGED
@@ -57,7 +57,7 @@ the configuration at your project root yourself:
57
57
 
58
58
  ```json
59
59
  {
60
- "$schema": "https://cdn.jsdelivr.net/npm/ferrovue@0.13.0/schema.json",
60
+ "$schema": "https://cdn.jsdelivr.net/npm/ferrovue@0.14.0/schema.json",
61
61
  "components": "client/components",
62
62
  "out": "src/generated"
63
63
  }
@@ -86,6 +86,7 @@ read `$schema`. Offline, the copy in the installed package works as well:
86
86
  | `scopeId` | no | How a `<style scoped>` id is hashed, as `@vitejs/plugin-vue` hashes it: `"filepath-source"` (the default, the plugin's in a production build) or `"filepath"`. See [`scoped_styles`](https://docs.rs/ferrovue/latest/ferrovue/guide/scoped_styles/index.html) |
87
87
  | `viteRoot` | no | Vite's root, relative to this file's directory, from which a component's path is hashed (default `.`) |
88
88
  | `isProduction` | no | Whether `@vitejs/plugin-vue` compiles for production, as Vite's `isProduction`: `true` (the default) for `vite build`, `false` for the dev server. It names the variables `v-bind()` in `<style>` sets. See [`scoped_styles`](https://docs.rs/ferrovue/latest/ferrovue/guide/scoped_styles/index.html) |
89
+ | `comments` | no | Whether templates' HTML comments render, as plugin-vue's `template.compilerOptions.comments`: by default kept when `isProduction` is `false` and left out otherwise. See [`generated_code`](https://docs.rs/ferrovue/latest/ferrovue/guide/generated_code/index.html#html-comments) |
89
90
  | `cssModules` | no | `{ generateScopedName, hashPrefix?, context? }`, as Vite's `css.modules` names CSS module classes: needed for `<style module>`. `context` is the directory Vite runs in (default `viteRoot`). See [`scoped_styles`](https://docs.rs/ferrovue/latest/ferrovue/guide/scoped_styles/index.html) |
90
91
  | `builders` | no | `false` leaves out each props struct's and shared type's `new()` and setters, for an app that builds its props as struct literals (default `true`) |
91
92
 
@@ -173,7 +174,7 @@ The crate's features:
173
174
 
174
175
  - `router` and `i18n`, both on by default: vue-router and vue-i18n. An application with neither
175
176
  routes nor translations can leave them out, and build neither `ferrovue-router` nor
176
- `ferrovue-i18n`: `ferrovue = { version = "0.13", default-features = false }`.
177
+ `ferrovue-i18n`: `ferrovue = { version = "0.14", default-features = false }`.
177
178
  - `axum` and `actix-web`: `Html` is an `IntoResponse` or a `Responder`, so a handler responds with
178
179
  the component as `text/html; charset=utf-8`. `into_html(props)` and `into_island(props)` take the
179
180
  props by value, so a handler that builds them returns the page:
@@ -239,18 +240,19 @@ ferrovue compiles `<script setup lang="ts">` components, and components with no
239
240
  | Conditions | `v-if` / `v-else-if` / `v-else`, `?:`, `&&` and `||`, with optional and nullable values narrowed as TypeScript narrows them: `v-if="user"`, `user !== undefined`, `user !== null`, `user != null`, `typeof user !== "undefined"`, and `!user`, `user === undefined` or `user === null` for the `v-else`; a test narrows every operand after it in a chain (`n != null && n > 1 && n < 9`, `n == null \|\| n < 0 \|\| n > 9`, `n != null && n > 1 ? n * 2 : 0`), and `a && b` gives `undefined` where `a`, whose present values are all truthy, is absent (`kind && ICONS[kind]`); a string of a union of string literals narrowed the same way, by `===` and `!==` with a literal, by its truth and by `||`, so that a `ref<Tone | "">` reaches a child's `Tone` prop as `tone \|\| undefined`, `tone === '' ? undefined : tone`, under `v-if="tone !== ''"` or through a `computed` annotated `Tone \| undefined` |
240
241
  | Lists | `v-for` over arrays (of strings, numbers, objects or child props), array literals and number ranges (`n in 5`); with an index; with destructured items (`{ id, name } in rows`); nested; on `<template>`. `.includes()` (a number looked for in a list of numbers whether either is a `number` or a `Float`, as JavaScript compares them), `.join()`, and `.filter()`, `.map()`, `.some()`, `.every()`, `.find()`, `.findIndex()`, `.findLast()`, `.findLastIndex()` with an arrow function of the item (a name, or destructured) and its index, whose body is an expression or a body of statements (see Statements), narrowing inside as outside, or with `Boolean`; `.filter(Boolean)` of a list or an array literal whose items may be `null` or `undefined`, which keeps the ones present and truthy as a list of the values (`[name, title, city].filter(Boolean).join(" · ")`); `Boolean(x)`; `.slice()`; an item by its index, `list[i]` with an integer `i` and `list.at(i)` (from the end when negative), `undefined` outside the list and narrowed as other optional values are (`v-if="rows[0]"`); `.indexOf()` and `.lastIndexOf()` of a string, number or boolean, compared with `===` (`NaN` found nowhere, `-0` found as `0`); `.concat()` of lists of one item type (numbers whether `number` or `Float`); `.reduce()` and `.reduceRight()` with an initial value, into a string, a number or a boolean; `.toReversed()`, and `.toSorted()` by `String(x)` as JavaScript sorts without a comparator (`[10, 9, 1]` to `[1, 10, 9]`) or by a comparator `(a, b) => a - b`, `a.k - b.k` of integers or `?:` over `<` and `>` of integers or strings, stable as JavaScript's; `[...list].sort(…)` and `[...list].reverse()`; spread in array literals (`[...a, x, ...b]`) of one item type; `.flat()` of lists of lists, to a literal depth or `Infinity`, and `.flatMap()` with an arrow function that gives a list (flattened one level) or a value (kept as `.map()` keeps it); chained and nested; a computed list read by `v-for`, `.join()`, `.length`, `.includes()`, kept by `computed`, or handed to a child |
241
242
  | Dictionaries | `v-for="(value, key, index) in r"` over a `Record<string, T>`, and `([key, value], index) in Object.entries(r)`, in JavaScript's order: keys that are array indices first, in numeric order, then the others as the props give them; `Object.keys(r)`, `Object.values(r)` (of a record of lists too, which `.flat()` joins), `Object.hasOwn(r, key)`, and `Object.entries(r)` read by the list methods, whose arrow functions destructure each pair (`([key, value]) => …`, `([, value]) => …`, `([key, { id }]) => …`) or read its parts (`pair[0]`, `pair[1]`), and walked by `v-for="[key, value] in Object.entries(r).filter(…)"`, kept by `computed` and sorted by a comparator of either part; handed to a child |
243
+ | HTML comments | left out, as Vue's production build leaves them out, or kept with `comments` or `"isProduction": false`, as on the dev server: written as Vue writes them, at the root, between roots, inside elements and in slot content |
242
244
  | Attributes | static and bound attributes, boolean attributes, `:hidden`, `data-*` and `aria-*`, `v-bind` objects |
243
245
  | `class` | strings, arrays, objects (`{ active: on }`, computed keys), `cond && "x"`, `cond ? "x" : null`, merged with a static `class` |
244
246
  | `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 |
245
247
  | 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 |
246
248
  | `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`) |
247
249
  | 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 |
248
- | 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`; props named like listeners (`on1`, `onSale`), left out when falsy where Vue's `mergeProps` joins a child's props, as Vue leaves them out; `defineAsyncComponent(() => import("./X.vue"))` (or `{ loader: … }`), rendered as the component it loads, which Vue's server renderer waits for |
250
+ | 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`; a `boolean` prop written bare (`<Button disabled />`), as `""` or as its kebab-case name, which Vue casts to `true`; props named like listeners (`on1`, `onSale`), left out when falsy where Vue's `mergeProps` joins a child's props, as Vue leaves them out; `defineAsyncComponent(() => import("./X.vue"))` (or `{ loader: … }`), rendered as the component it loads, which Vue's server renderer waits for |
249
251
  | `<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 or a `defineModel` typed as a union of string literals (`as: "h1" \| "h2"`, a `match` over its enum's variants), 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 |
250
252
  | Escape hatches | `<ClientOnly>` from `ferrovue`, whose default slot the server never renders, so anything may go in it; components listed in `twins`, rendered by Rust functions of yours. See the crate's [`errors_and_limits`](https://docs.rs/ferrovue/latest/ferrovue/guide/errors_and_limits/index.html#client-only-content) guide |
251
253
  | 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 |
252
254
  | 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 |
253
- | Forms | `v-model` on text inputs, checkboxes, radios, `<select>` and `<textarea>` (renders the initial state); a `<textarea>`, or an `<input>` with `v-model` of a literal `type`, at the component's root where no parent passes it `value` or `type` (FV0424 otherwise) |
255
+ | Forms | `v-model` on text inputs, checkboxes, radios, `<select>` and `<textarea>` (renders the initial state), of a string or a string that may be absent, with an `<option>` with no `value` compared with `null` as Vue's server compares it; a `<textarea>`, or an `<input>` with `v-model` of a literal `type`, at the component's root where no parent passes it `value` or `type` (FV0424 otherwise) |
254
256
  | 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`, on an element with no content when its attributes are bound by name and it is not the component's root (FV0425) |
255
257
  | 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 |
256
258
  | 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 |
@@ -307,7 +309,9 @@ The crate's guide, `ferrovue::guide` on docs.rs, covers each topic in depth:
307
309
  from a Node server that renders with Vue, and the
308
310
  [support policy](https://docs.rs/ferrovue/latest/ferrovue/guide/support/index.html): supported
309
311
  versions, fixes, deprecations and where to report a problem
310
- - Upgrading a project [from 0.11 to 0.12](https://docs.rs/ferrovue/latest/ferrovue/guide/upgrading_to_0_12/index.html),
312
+ - Upgrading a project [from 0.13 to 0.14](https://docs.rs/ferrovue/latest/ferrovue/guide/upgrading_to_0_14/index.html),
313
+ [from 0.12 to 0.13](https://docs.rs/ferrovue/latest/ferrovue/guide/upgrading_to_0_13/index.html),
314
+ [from 0.11 to 0.12](https://docs.rs/ferrovue/latest/ferrovue/guide/upgrading_to_0_12/index.html),
311
315
  [from 0.10 to 0.11](https://docs.rs/ferrovue/latest/ferrovue/guide/upgrading_to_0_11/index.html),
312
316
  [from 0.9 to 0.10](https://docs.rs/ferrovue/latest/ferrovue/guide/upgrading_to_0_10/index.html),
313
317
  [from 0.8 to 0.9](https://docs.rs/ferrovue/latest/ferrovue/guide/upgrading_to_0_9/index.html)
package/dist/attrs.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { escapeHtml, hyphenate, isBooleanAttr, isSSRSafeAttrName, isSVGTag, propsToAttrMap } from "@vue/shared";
2
- import { fail, nothing, rustStr } from "./model.js";
2
+ import { fail, neverEmpty, nothing, rustStr } from "./model.js";
3
3
  import { GenError } from "./errors.js";
4
4
  import { asWritten } from "./numbers.js";
5
5
  import { CONFIG_FILE } from "./config.js";
@@ -7,14 +7,43 @@ import { ctx } from "./context.js";
7
7
  import { describeTy, expr, shown } from "./expr.js";
8
8
  import { known, truthy } from "./narrowing.js";
9
9
  import { asCow, isTemporary, unquote } from "./strings.js";
10
- import { atom, bare, condition, receiver, scrutinee, strArg } from "./parens.js";
10
+ import { atom, bare, condition, ifElse, not, receiver, scrutinee, strArg } from "./parens.js";
11
11
  import { Emitter } from "./emitter.js";
12
12
  import { isDollarAttrs } from "./fallthrough.js";
13
13
  import { claim } from "./plugin.js";
14
14
  import { classAttr, classPresent, literalClass, maybeEqual, renderClass } from "./classes.js";
15
15
  import { mergedStyle, renderStyle, styleAttr } from "./styles.js";
16
16
  const NUMBER_BYTES = 6;
17
+ function piecesOf(v) {
18
+ const f = v.format;
19
+ if (v.ty.k !== "str" || f === undefined || v.code !== `&*format!(${rustStr(f.text)}, ${f.args.join(", ")})`)
20
+ return undefined;
21
+ return f.pieces;
22
+ }
23
+ function emptyTest(v) {
24
+ if (neverEmpty(v.ty))
25
+ return "false";
26
+ const pieces = piecesOf(v);
27
+ if (pieces === undefined)
28
+ return `${receiver(v.code)}.is_empty()`;
29
+ const strs = pieces.filter((p) => typeof p !== "string" && !("shown" in p) && p.ty.k === "str");
30
+ if (strs.length < pieces.length || strs.some((p) => neverEmpty(p.ty)))
31
+ return "false";
32
+ return strs.map((p) => `${receiver(p.code)}.is_empty()`).join(" && ");
33
+ }
17
34
  export function display(e, v) {
35
+ const pieces = piecesOf(v);
36
+ if (pieces !== undefined) {
37
+ for (const p of pieces) {
38
+ if (typeof p === "string")
39
+ e.lit(escapeHtml(p));
40
+ else if ("shown" in p)
41
+ e.stmt(`{ use std::fmt::Write as _; let _ = write!(out, "{}", ${p.shown}); }`);
42
+ else
43
+ display(e, p);
44
+ }
45
+ return true;
46
+ }
18
47
  if (v.ty.k === "str" && known(v) !== undefined)
19
48
  e.lit(escapeHtml(unquote(v.code)));
20
49
  else if ((v.ty.k === "int" || v.ty.k === "float") && v.num !== undefined)
@@ -65,10 +94,31 @@ function renderable(s, key, v, n) {
65
94
  const fix = own?.fix ?? (ty.k === "list" ? 'join it into one string, as `.join(",")`' : "bind a string, a number or a boolean");
66
95
  fail(s.comp, "FV0403", `\`${key}\` is bound to ${what}: Vue's server renderer leaves the attribute out, and hydration then sets it, changing the page; ${fix}`, n);
67
96
  }
97
+ /** For an optional string that is a literal where a test holds and absent where it fails, the
98
+ * test and the literal. */
99
+ function someLiteral(v) {
100
+ const branches = v.ty.k === "opt" ? ifElse(v.code) : null;
101
+ if (branches === null || v.ty.k !== "opt")
102
+ return null;
103
+ const literal = (code) => /^Some\(("(?:[^"\\]|\\.)*")\)$/.exec(code)?.[1];
104
+ const [yes, no] = [literal(branches.yes), literal(branches.no)];
105
+ if (yes !== undefined && branches.no === "None")
106
+ return { test: branches.test, value: { code: yes, ty: v.ty.of } };
107
+ if (no !== undefined && branches.yes === "None" && !branches.test.startsWith("let "))
108
+ return { test: not(branches.test), value: { code: no, ty: v.ty.of } };
109
+ return null;
110
+ }
68
111
  export function renderAttr(s, e, key, v, n) {
69
112
  renderable(s, key, v, n);
70
113
  if (nothing(v.ty))
71
114
  return;
115
+ const some = someLiteral(v);
116
+ if (some !== null) {
117
+ e.open(`if ${condition(some.test)}`);
118
+ renderAttr(s, e, key, some.value, n);
119
+ e.close();
120
+ return;
121
+ }
72
122
  if (v.ty.k === "opt") {
73
123
  e.open(`if let Some(v) = ${scrutinee(v.code)}`);
74
124
  renderAttr(s, e, key, { code: "v", ty: v.ty.of }, n);
@@ -104,6 +154,13 @@ export function renderDynamicAttr(s, e, key, v, n, tag = null) {
104
154
  e.close();
105
155
  return;
106
156
  }
157
+ const some = someLiteral(v);
158
+ if (some !== null) {
159
+ e.open(`if ${condition(some.test)}`);
160
+ renderDynamicAttr(s, e, key, some.value, n, tag);
161
+ e.close();
162
+ return;
163
+ }
107
164
  if (v.ty.k === "opt") {
108
165
  e.open(`if let Some(v) = ${scrutinee(v.code)}`);
109
166
  renderDynamicAttr(s, e, key, { code: "v", ty: v.ty.of }, n, tag);
@@ -125,8 +182,9 @@ export function renderDynamicAttr(s, e, key, v, n, tag = null) {
125
182
  e.lit(` ${name}`);
126
183
  return;
127
184
  }
128
- if (v.ty.k === "str" && known(v) === undefined) {
129
- e.open(`if ${receiver(v.code)}.is_empty()`);
185
+ const empty = v.ty.k === "str" && known(v) === undefined ? emptyTest(v) : "false";
186
+ if (empty !== "false") {
187
+ e.open(`if ${empty}`);
130
188
  e.lit(` ${name}`);
131
189
  e.close(" else {");
132
190
  e.lit(` ${name}="`);
package/dist/calls.js CHANGED
@@ -1,16 +1,17 @@
1
- import { BOOL, fail } from "./model.js";
1
+ import { absence, BOOL, fail, rustStr } from "./model.js";
2
2
  import { ctx } from "./context.js";
3
3
  import { claim } from "./plugin.js";
4
- import { atom, bare } from "./parens.js";
4
+ import { atom, bare, binary } from "./parens.js";
5
5
  import { isBoolean, listCall, listMethod, presentLiteral } from "./lists.js";
6
6
  import { meet, noArgStringMethod, regexTest, stringMethod, stringsEqual } from "./strings.js";
7
7
  import { numberMethod } from "./numbers.js";
8
8
  import { boolOf, truthy } from "./narrowing.js";
9
- import { coerce, expr, refuseClientOnly, refuseUnavailable } from "./expr.js";
9
+ import { coerce, describeTy, expr, refuseClientOnly, refuseUnavailable } from "./expr.js";
10
10
  import { callFunction } from "./functions.js";
11
11
  import { constOf, mapCall } from "./constants.js";
12
12
  import { refuseInPlace } from "./objects.js";
13
13
  import { globalCall, mathCall, staticCall } from "./globals.js";
14
+ import { refValue } from "./members.js";
14
15
  export function call(s, n) {
15
16
  const callee = n.callee;
16
17
  if (callee.type === "Identifier")
@@ -57,24 +58,35 @@ function renderHelperCall(s, name, args, n) {
57
58
  }
58
59
  function looseEqual(s, args, n) {
59
60
  const a = expr(s, args[0]);
61
+ const absent = a.ty.k === "opt" && a.ty.of.k === "str" ? absence(a.ty) : null;
62
+ const written = args[1]?.type === "NullLiteral" ? "null" : args[1]?.type === "StringLiteral" ? args[1].value : null;
63
+ if (args[1]?.type === "NullLiteral" && a.ty.k === "str")
64
+ return boolOf(binary(a.code, "==", '"null"'));
65
+ if (absent !== null && written !== null) {
66
+ return boolOf(written === absent ? `${atom(a.code)}.is_none_or(|v| v == ${rustStr(written)})` : binary(a.code, "==", `Some(${rustStr(written)})`));
67
+ }
68
+ if (args[1]?.type === "NullLiteral") {
69
+ return fail(s.comp, "FV0413", `an \`<option>\` with no \`value\` under a \`<select>\` whose \`v-model\` is ${describeTy(a.ty)}, which Vue's server compares with \`null\`: give the option a \`value\`, and the model a string type`, n);
70
+ }
60
71
  const b = expr(s, args[1]);
61
72
  if (a.ty.k === "str" && b.ty.k === "str") {
62
73
  meet(s.comp, a, b, "`v-model`", n, "equal");
63
74
  return boolOf(stringsEqual(a, b));
64
75
  }
65
- return fail(s.comp, "FV0413", "`v-model` comparison between these types", n);
76
+ return fail(s.comp, "FV0413", `\`v-model\` of ${describeTy(a.ty)} compared with a value that is ${describeTy(b.ty)}: give the model a string type and each option or control a string \`value\`, or bind \`:checked\` or \`:selected\``, n);
66
77
  }
67
78
  function includeBooleanAttr(s, args) {
68
79
  const a = expr(s, args[0]);
69
80
  if (a.konst !== undefined)
70
81
  return a;
71
- if (a.ty.k === "str")
82
+ if (a.ty.k === "str" || a.ty.k === "html")
72
83
  return { code: "true", ty: BOOL, konst: true };
73
- if (a.ty.k === "opt" && a.ty.of.k === "str")
84
+ if (a.ty.k === "opt" && (a.ty.of.k === "str" || a.ty.of.k === "html"))
74
85
  return { code: `${atom(a.code)}.is_some()`, ty: BOOL };
75
86
  return boolOf(truthy(a));
76
87
  }
77
88
  function memberCall(s, n) {
89
+ refValue(s, n.callee);
78
90
  const object = n.callee.object;
79
91
  const method = n.callee.property.name;
80
92
  const args = n.arguments;
package/dist/children.js CHANGED
@@ -1,4 +1,4 @@
1
- import { isSSRSafeAttrName, propsToAttrMap } from "@vue/shared";
1
+ import { hyphenate, isSSRSafeAttrName, propsToAttrMap } from "@vue/shared";
2
2
  import { absence, camelize, declares, enumItems, enumOf, fail, literalsOf, nothing, REFUSALS, refusalAt, rustStr, sameTy, snake, takesAttrs, variantPath } from "./model.js";
3
3
  import { ctx } from "./context.js";
4
4
  import { known } from "./narrowing.js";
@@ -101,7 +101,7 @@ export function renderChild(s, e, n, twin) {
101
101
  fail(s.comp, "FV0517", `\`${key}\`, a prop of ${child.name}, a Rust twin, is named like a listener, which Vue's \`mergeProps\` leaves out when it is falsy: rename the prop`, p);
102
102
  heard.add(field.js);
103
103
  }
104
- given.set(field.js, p.value);
104
+ given.set(field.js, castToTrue(field, p.value) ? { type: "BooleanLiteral", value: true, loc: p.value.loc, start: p.value.start, end: p.value.end, __fv: p.value.__fv } : p.value);
105
105
  continue;
106
106
  }
107
107
  if (array(key))
@@ -133,6 +133,16 @@ export function renderChild(s, e, n, twin) {
133
133
  });
134
134
  callChild(s, e, child, `&super::${child.module}::Props { ${inits.join(", ")} }`, slots, childAttrsArg(s, child, parts, merges, fallthrough, ids, n), later);
135
135
  }
136
+ /** Whether Vue's boolean casting turns `value`, given to the prop `f`, into `true`: the prop's
137
+ * runtime type is `Boolean`, and the value is `""` (the attribute written bare) or the prop's name in
138
+ * kebab-case. */
139
+ function castToTrue(f, value) {
140
+ const ty = f.ty.k === "opt" ? f.ty.of : f.ty;
141
+ if (ty.k !== "bool")
142
+ return false;
143
+ const text = value.type === "StringLiteral" ? value.value : value.type === "TemplateLiteral" && value.expressions.length === 0 ? value.quasis[0].value.cooked : null;
144
+ return text === "" || text === hyphenate(camelize(f.js));
145
+ }
136
146
  /** What `v`, a reference to a value of the prop `f` of type `ty`, passes when it is truthy: null when
137
147
  * every value of the type is. */
138
148
  function truthTest(s, child, f, ty, n) {
@@ -319,10 +329,18 @@ export function callWith(s, e, child, m, head, slotsTy, trailing, slots, vnode =
319
329
  statements({ ...inner, fill: false }, e, body);
320
330
  e.stmt("true");
321
331
  }
322
- else {
323
- e.stmt("let mut filled = false;");
324
- statements({ ...inner, fill: true }, e, body);
332
+ else
333
+ fillable(inner);
334
+ };
335
+ const fillable = (inner) => {
336
+ e.stmt("let mut filled = false;");
337
+ const declared = e.lines.length - 1;
338
+ statements({ ...inner, fill: true }, e, body);
339
+ if (e.reads("filled", declared + 1))
325
340
  e.stmt("filled");
341
+ else {
342
+ e.lines.splice(declared, 1);
343
+ e.stmt("false");
326
344
  }
327
345
  };
328
346
  const unread = (opened, binding) => {
@@ -350,6 +368,7 @@ export function callWith(s, e, child, m, head, slotsTy, trailing, slots, vnode =
350
368
  e.open(`${field}: Some(&|out: &mut String, ${sp}: &${m}::${shape.name}${life}${sidParam}${contextParams}| -> bool`);
351
369
  const opened = e.lines.length - 1;
352
370
  content({ ...s, locals, sid, vnode: asVnodes, opaque });
371
+ unread(opened, "out");
353
372
  unread(opened, sp);
354
373
  unread(opened, sid);
355
374
  for (const p of context)
@@ -364,6 +383,7 @@ export function callWith(s, e, child, m, head, slotsTy, trailing, slots, vnode =
364
383
  e.open(`${field}: Some(&|out: &mut String${sidParam}${contextParams}| -> bool`);
365
384
  const opened = e.lines.length - 1;
366
385
  content(inner);
386
+ unread(opened, "out");
367
387
  unread(opened, sid);
368
388
  for (const p of context)
369
389
  unread(opened, p.name);
@@ -373,6 +393,7 @@ export function callWith(s, e, child, m, head, slotsTy, trailing, slots, vnode =
373
393
  e.open(`${field}: Some(fv::render::Slot::slotted(&|out: &mut String${sidParam}| -> bool`);
374
394
  const opened = e.lines.length - 1;
375
395
  content(inner);
396
+ unread(opened, "out");
376
397
  unread(opened, sid);
377
398
  e.close(")),");
378
399
  }
@@ -383,9 +404,9 @@ export function callWith(s, e, child, m, head, slotsTy, trailing, slots, vnode =
383
404
  }
384
405
  else {
385
406
  e.open(`${field}: Some(fv::render::Slot::markup(&|out: &mut String| -> bool`);
386
- e.stmt("let mut filled = false;");
387
- statements({ ...inner, fill: true }, e, body);
388
- e.stmt("filled");
407
+ const opened = e.lines.length - 1;
408
+ fillable(inner);
409
+ unread(opened, "out");
389
410
  e.close(")),");
390
411
  }
391
412
  }
package/dist/classes.js CHANGED
@@ -3,7 +3,7 @@ import { directiveOwner, fail, nothing, rustStr, STR } from "./model.js";
3
3
  import { expr } from "./expr.js";
4
4
  import { choice, cond, known, narrowing, narrowTo, presence, truthy } from "./narrowing.js";
5
5
  import { pickPresent } from "./constants.js";
6
- import { meet } from "./strings.js";
6
+ import { meet, unquote } from "./strings.js";
7
7
  import { atom, bare, condition, logical, not, occurrences, strArg } from "./parens.js";
8
8
  import { Emitter } from "./emitter.js";
9
9
  import { stringAttr } from "./attrs.js";
@@ -73,6 +73,11 @@ export function classItems(s, n) {
73
73
  break;
74
74
  }
75
75
  const v = expr(s, n);
76
+ const literal = v.ty.k === "str" && known(v) !== undefined ? unquote(v.code) : null;
77
+ if (literal === "")
78
+ return [];
79
+ if (literal !== null && literal === literal.trim())
80
+ return [{ lit: literal }];
76
81
  if (v.ty.k === "str")
77
82
  return [{ code: v.code }];
78
83
  if (v.ty.k === "opt" && v.ty.of.k === "str")
package/dist/client.d.ts CHANGED
@@ -177,8 +177,8 @@ export interface RenderPageOptions extends Pick<MountOptions, "pinia" | "router"
177
177
  * document: load the components it names (and no others), render the app `createPageApp` builds
178
178
  * with `hydrate: false` into a copy of the container with no children, wait for the router, put the
179
179
  * copy in the container's place, mount, and then unmount `previous`. The next app is mounted
180
- * before the previous one is unmounted, so a router both use stays started. `router` must be at the
181
- * next page's location: `linkRouter` with the `location` the navigation goes to. Rejects, leaving
182
- * the page shown as it is, when the container is missing or a component is not given or does not
183
- * load. */
180
+ * before the previous one is unmounted, so a router both use stays started. `router`, or a
181
+ * `linkRoutes` in `plugins`, must be at the next page's location: built with the `location` the
182
+ * navigation goes to. Rejects, leaving the page shown as it is, when the container is missing or
183
+ * a component is not given or does not load. */
184
184
  export declare function renderPage(layout: IslandComponent, components: Record<string, IslandComponent>, record: PageRecord, options?: RenderPageOptions): Promise<App>;
package/dist/client.js CHANGED
@@ -571,10 +571,10 @@ export async function mountPage(layout, components, options = {}) {
571
571
  * document: load the components it names (and no others), render the app `createPageApp` builds
572
572
  * with `hydrate: false` into a copy of the container with no children, wait for the router, put the
573
573
  * copy in the container's place, mount, and then unmount `previous`. The next app is mounted
574
- * before the previous one is unmounted, so a router both use stays started. `router` must be at the
575
- * next page's location: `linkRouter` with the `location` the navigation goes to. Rejects, leaving
576
- * the page shown as it is, when the container is missing or a component is not given or does not
577
- * load. */
574
+ * before the previous one is unmounted, so a router both use stays started. `router`, or a
575
+ * `linkRoutes` in `plugins`, must be at the next page's location: built with the `location` the
576
+ * navigation goes to. Rejects, leaving the page shown as it is, when the container is missing or
577
+ * a component is not given or does not load. */
578
578
  export async function renderPage(layout, components, record, options = {}) {
579
579
  const container = containerOf(options);
580
580
  for (const text of options.loads ?? [])
package/dist/compiler.js CHANGED
@@ -2,7 +2,7 @@ import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "nod
2
2
  import { basename, join, resolve } from "node:path";
3
3
  import { relativePath } from "./paths.js";
4
4
  import { CONFIG_FILE, loadConfig } from "./config.js";
5
- import { ctx, JS_DATE, tyOfName } from "./context.js";
5
+ import { ctx, JS_DATE, templateComments, tyOfName } from "./context.js";
6
6
  import { refuseUnusedUnions } from "./enums.js";
7
7
  import { failIn, snake } from "./model.js";
8
8
  import { GenError, GenErrors } from "./errors.js";
@@ -46,6 +46,7 @@ function generateAll(root, config, sources, options) {
46
46
  ctx.unions = new Map(Object.entries(config.unions ?? {}));
47
47
  ctx.mappedUnions = new Map();
48
48
  ctx.builders = config.builders ?? true;
49
+ ctx.comments = templateComments(config);
49
50
  ctx.clientDirectives = new Set(config.clientDirectives ?? []);
50
51
  ctx.rootDir = root;
51
52
  ctx.vnodeTag = null;
package/dist/component.js CHANGED
@@ -309,7 +309,7 @@ export function refuseOptionsApi(comp, script, isChild) {
309
309
  * further than its `<script>`, `optionsApi`, for `refuseOptionsApi` once every component is read. */
310
310
  export function readComponent(file, root, name = basename(file, ".vue")) {
311
311
  const source = readSource(file);
312
- const { descriptor, errors } = parseSfc(source, { filename: file });
312
+ const { descriptor, errors } = parseSfc(source, { filename: file, templateParseOptions: { comments: ctx.comments } });
313
313
  const rel = relativePath(root, file);
314
314
  const comp = blankComponent(name, snake(name), rel);
315
315
  comp.source = source;
@@ -451,7 +451,7 @@ export function readComponent(file, root, name = basename(file, ".vue")) {
451
451
  filename: file,
452
452
  ...(claim((p) => p.templateOptions?.(comp)) ?? { id: name, scoped: false, slotted: false, ssrCssVars: [], isProd: true }),
453
453
  ssr: true,
454
- compilerOptions: { ...(script ? { bindingMetadata: script.bindings } : {}), sourceMap: true, nodeTransforms: [mergedControls(controls)] },
454
+ compilerOptions: { ...(script ? { bindingMetadata: script.bindings } : {}), comments: ctx.comments, sourceMap: true, nodeTransforms: [mergedControls(controls)] },
455
455
  });
456
456
  const kids = (n) => n.children.filter((c) => c.type !== 3 && !(c.type === 2 && !c.content.trim()));
457
457
  const roots = descriptor.template.ast ? kids(descriptor.template.ast) : [];
@@ -1,4 +1,4 @@
1
- import { absence, enumOf, fail, FLOAT, joinAbsence, joinTy, literalsOf, literalTy, nothing, refusalAt, sameTy, STR, withAbsence } from "./model.js";
1
+ import { absence, enumOf, fail, FLOAT, joinAbsence, joinTy, literalsOf, literalTy, neverEmpty, nothing, refusalAt, sameTy, STR, withAbsence } from "./model.js";
2
2
  import { GenError } from "./errors.js";
3
3
  import { annotatedStr, exactlyEnum, intoEnum, knownLiteral } from "./enums.js";
4
4
  import { ctx } from "./context.js";
@@ -56,7 +56,7 @@ function branches(s, n, t) {
56
56
  const literals = p ? null : literalNarrowing(s, n.test);
57
57
  let a = expr(p && !p.negated ? narrowTo(s, p, name) : (literals?.yes ?? s), n.consequent);
58
58
  let b = expr(p && p.negated ? narrowTo(s, p, name) : (literals?.no ?? s), n.alternate);
59
- if (sameTy(a.ty, b.ty) && a.code === b.code && (!name || !occurrences(a.code, name)))
59
+ if (sameTy(a.ty, b.ty) && neverEmpty(a.ty) === neverEmpty(b.ty) && a.code === b.code && (!name || !occurrences(a.code, name)))
60
60
  return a;
61
61
  const placed = (v) => (["struct", "child", "record"].includes(v.ty.k) && /^\w+(?:\.[\w#]+)+$/.test(v.code) ? { ...v, code: `&${v.code}` } : v);
62
62
  [a, b] = [placed(a), placed(b)];
@@ -174,7 +174,7 @@ function pickedBy(v, pick) {
174
174
  function ownedChoice(comp, a, b, n) {
175
175
  const base = (ty) => (ty.k === "opt" ? ty.of : nothing(ty) ? null : ty);
176
176
  const [ba, bb] = [base(a.ty), base(b.ty)];
177
- const of = ba ?? bb;
177
+ const of = ba !== null && bb !== null ? joinTy(ba, bb) : (ba ?? bb);
178
178
  if (of === null || (ba !== null && bb !== null && !sameTy(ba, bb)))
179
179
  return null;
180
180
  const ty = withAbsence(of, joinAbsence(absence(a.ty), absence(b.ty)));
package/dist/config.d.ts CHANGED
@@ -99,6 +99,10 @@ export interface Config {
99
99
  * default) for `vite build`, `false` for the dev server. It decides the names of the CSS
100
100
  * variables `v-bind()` in `<style>` sets: a hash in production, the expression in development. */
101
101
  isProduction?: boolean;
102
+ /** Whether the HTML comments of templates render, as `@vitejs/plugin-vue`'s
103
+ * `template.compilerOptions.comments`: by default as Vue decides it, kept when `isProduction` is
104
+ * `false` and left out when it is `true`. */
105
+ comments?: boolean;
102
106
  /** CSS modules (`<style module>`): how Vite's `css.modules` names their classes, which ferrovue
103
107
  * computes as postcss-modules does. Without it, `<style module>` is refused. */
104
108
  cssModules?: {
@@ -8,7 +8,7 @@ import { unifiedDiff } from "./diff.js";
8
8
  import { allPages, fileRoutes, pagesFolder, routeEntries } from "./file-routes.js";
9
9
  import { withDatePlans } from "./date-plans.js";
10
10
  import { fixtureApp, readFixture } from "./testing-browser.js";
11
- import { ctx } from "./context.js";
11
+ import { ctx, templateComments } from "./context.js";
12
12
  import { consoleInto, warnInto } from "./problems.js";
13
13
  import { headRendered, settled } from "./settle.js";
14
14
  import { attachSsrRender } from "./ssr.js";
@@ -254,7 +254,7 @@ export function registerConformance(api, options) {
254
254
  for (const [name, component] of given) {
255
255
  const source = sources.get(name);
256
256
  if (source)
257
- attachSsrRender(source, name, component);
257
+ attachSsrRender(source, name, component, { comments: templateComments(config) });
258
258
  }
259
259
  const cases = casesIn(fixtures);
260
260
  const { routes, options: configured } = fixtureOptions(root, config);
package/dist/context.d.ts CHANGED
@@ -8,6 +8,9 @@ import { GenError } from "./errors.ts";
8
8
  import { type Schema } from "./schema.ts";
9
9
  /** The options of Vite's `css.modules` that decide a class's name, as postcss-modules reads them. */
10
10
  export type CssModulesConfig = NonNullable<Config["cssModules"]>;
11
+ /** Whether the HTML comments of templates render under `config`: its `comments`, or, as Vue
12
+ * decides it, whether plugin-vue compiles for development. */
13
+ export declare function templateComments(config: Pick<Config, "comments" | "isProduction">): boolean;
11
14
  /** Check what a configuration file holds against `schema`, naming the first key that is unknown or
12
15
  * of the wrong shape, and handing `warn` a warning for each deprecated key. */
13
16
  export declare function checkConfig(raw: unknown, file: string, warn: (warning: GenError) => void, schema?: Schema): Config;
@@ -29,6 +32,7 @@ export declare const ctx: {
29
32
  unions: Map<string, string>;
30
33
  mappedUnions: Map<string, EnumDef>;
31
34
  builders: boolean;
35
+ comments: boolean;
32
36
  clientDirectives: Set<string>;
33
37
  plugins: readonly Plugin[];
34
38
  runs: Map<Plugin<unknown, unknown>, unknown>;
package/dist/context.js CHANGED
@@ -33,6 +33,11 @@ function closest(key, known) {
33
33
  }
34
34
  return best;
35
35
  }
36
+ /** Whether the HTML comments of templates render under `config`: its `comments`, or, as Vue
37
+ * decides it, whether plugin-vue compiles for development. */
38
+ export function templateComments(config) {
39
+ return config.comments ?? !(config.isProduction ?? true);
40
+ }
36
41
  /** Check what a configuration file holds against `schema`, naming the first key that is unknown or
37
42
  * of the wrong shape, and handing `warn` a warning for each deprecated key. */
38
43
  export function checkConfig(raw, file, warn, schema = CONFIG_SCHEMA) {
@@ -95,6 +100,8 @@ export function checkConfig(raw, file, warn, schema = CONFIG_SCHEMA) {
95
100
  }
96
101
  if (raw.isProduction !== undefined && typeof raw.isProduction !== "boolean")
97
102
  wrong("isProduction", "`true` or `false`: whether `@vitejs/plugin-vue` compiles for production");
103
+ if (raw.comments !== undefined && typeof raw.comments !== "boolean")
104
+ wrong("comments", "`true` or `false`: whether the HTML comments of templates render");
98
105
  if (raw.cssModules !== undefined) {
99
106
  const m = raw.cssModules;
100
107
  if (!isObject(m))
@@ -217,6 +224,7 @@ export const ctx = {
217
224
  unions: new Map(),
218
225
  mappedUnions: new Map(),
219
226
  builders: true,
227
+ comments: false,
220
228
  clientDirectives: new Set(),
221
229
  plugins: [],
222
230
  runs: new Map(),
package/dist/errors.d.ts CHANGED
@@ -64,7 +64,7 @@ export declare const ERRORS: {
64
64
  };
65
65
  readonly FV0105: {
66
66
  readonly title: "Statement in setup that could change what renders";
67
- readonly detail: "ferrovue translates setup to Rust and does not run it, so a statement that may change state the template reads (`count.value++`, `store.load()`, `items.push(x)`, a call of a function of another library) would render otherwise than Vue does. Lifecycle hooks such as `onMounted`, the compiler macros (`defineEmits`, `defineExpose`) and a `watch` without `immediate` are allowed. Move the statement into `onMounted` or an event handler, write the value as a `computed`, or compute it in Rust and pass it as a prop.";
67
+ readonly detail: "ferrovue translates setup to Rust and does not run it, so a statement that may change state the template reads (`count.value++`, `store.load()`, `items.push(x)`, a call of a function of another library) would render otherwise than Vue does. The initial value of a `const` or `let` the compiler does not translate is refused the same way when running it could change that state: an assignment, `++` or `--`, a call of a function of setup, a method that changes a value of setup (`push`, `splice`), or a value of setup given to a function of another library. Lifecycle hooks such as `onMounted`, the compiler macros (`defineEmits`, `defineExpose`) and a `watch` without `immediate` are allowed. Move the statement into `onMounted` or an event handler, write the value as a `computed`, or compute it in Rust and pass it as a prop.";
68
68
  };
69
69
  readonly FV0106: {
70
70
  readonly title: "`defineAsyncComponent` of anything but a component of this project";
@@ -372,7 +372,7 @@ export declare const ERRORS: {
372
372
  };
373
373
  readonly FV0413: {
374
374
  readonly title: "`v-model` comparing values of types it cannot compare";
375
- readonly detail: "`v-model` on a checkbox, a radio button or a `<select>` marks the option whose value is loosely equal to the model, which the server reproduces between two strings. Other pairs of types follow JavaScript's loose equality, which the server does not. Bind string values and a string model, or bind `:checked` or `:selected` yourself.";
375
+ readonly detail: "`v-model` on a checkbox, a radio button or a `<select>` marks the option whose value is loosely equal to the model, which the server reproduces for a model that is a string, or a string that may be absent, against a string value, and for an `<option>` with no `value`, which Vue's server compares with `null`. Other pairs of types follow JavaScript's loose equality, which the server does not. Bind string values and a string model, or bind `:checked` or `:selected` yourself.";
376
376
  };
377
377
  readonly FV0414: {
378
378
  readonly title: "`v-model` over an array";
@@ -422,6 +422,10 @@ export declare const ERRORS: {
422
422
  readonly title: "Element with no content given a custom directive and attributes that may hold its content";
423
423
  readonly detail: "Vue's server renderer writes an element with no content and a custom directive with the `textContent` or `innerHTML` among the attributes it merges into the element: those passed to the component, when the element is its root, those of a `v-bind` of an object or an attribute named at run time, and a bound `textContent` or `innerHTML`. The compiler does not translate that reading. Wrap a root element in another element, such as a `<div>`, which then takes the attributes passed to the component, bind each attribute by its name, and write the content inside the element.";
424
424
  };
425
+ readonly FV0426: {
426
+ readonly title: "`v-model` on an `<input>` whose `type` is bound";
427
+ readonly detail: "With a bound `type`, Vue's server renderer chooses as it renders what `v-model` writes: `checked` for a checkbox or a radio button, `value` for any other type, each by its own comparison with the model. The compiler translates `v-model` on an `<input>` of a literal `type`. Write the `type` as a literal, or write one `<input>` for each type under `v-if` and `v-else-if`.";
428
+ };
425
429
  readonly FV0501: {
426
430
  readonly title: "Child component that is not among the compiled components";
427
431
  readonly detail: "A child component is rendered by calling its generated Rust module, so it must be a `.vue` file in the components directory that ferrovue compiles. A component imported from a library, from `node_modules` or from outside that directory has no module to call. Move the file into the components directory, or render it with a function of your own named under `twins` in `ferrovue.config.json`. See [`errors_and_limits`](crate::guide::errors_and_limits#rust-twins).";
@@ -464,7 +468,7 @@ export declare const ERRORS: {
464
468
  };
465
469
  readonly FV0511: {
466
470
  readonly title: "Prop given a value of another type";
467
- readonly detail: "The prop's Rust type cannot hold the value, and Vue would hand it to the child as it is, so the child would read another type than it declares. The message names both types. Pass a value of the prop's type, convert it where it is passed, or change the prop's declared type.";
471
+ readonly detail: "The prop's Rust type cannot hold the value, and Vue would hand it to the child as it is, so the child would read another type than it declares. The message names both types. Pass a value of the prop's type, convert it where it is passed, or change the prop's declared type. A `boolean` prop takes `true` from an attribute written bare, `\"\"` or the prop's name in kebab-case, which Vue casts; any other string reaches the child as a string.";
468
472
  };
469
473
  readonly FV0512: {
470
474
  readonly title: "Type imported from a component that is not compiled";
@@ -642,6 +646,10 @@ export declare const ERRORS: {
642
646
  readonly title: "Comparison of a union of string literals with a literal outside it";
643
647
  readonly detail: "A value typed as a union of string literals is a Rust enum, and `===` or `!==` with a literal compares it with one of its variants. A literal outside the union is no variant: the comparison would always be `false` (or `true` for `!==`), and TypeScript refuses it as a comparison of types with no overlap. Compare with one of the union's members, or fix the literal's spelling.";
644
648
  };
649
+ readonly FV0638: {
650
+ readonly title: "Member of a ref other than `.value` read in setup";
651
+ readonly detail: "In `<script setup>`, a `ref`, a `shallowRef` or a `computed` is the ref object itself, whose only member is `.value`: `r.label` is `undefined`, `r.length` is `undefined` and `r.join()` throws. The template unwraps a ref, so there `r.label` reads the field of its value. Read the member through `.value` in setup: `r.value.label`.";
652
+ };
645
653
  readonly FV0701: {
646
654
  readonly title: "`Number()` of an unsupported value";
647
655
  readonly detail: "`Number(x)` is translated for a string, read by JavaScript's grammar for numbers with `js_number`, a number and a boolean, each present. `Number()` gives `NaN` for `undefined` and `0` for `null`, and converts an object or a list through its `String()`, which the server's typed values do not reproduce. Narrow an optional value with `v-if` or give it a fallback with `??`, and read a date with `.getTime()`.";
@@ -735,8 +743,8 @@ export declare const ERRORS: {
735
743
  readonly detail: "`String(x)`, `encodeURI(x)`, `encodeURIComponent(x)` and `s.concat(x)` take one value each and write it as `String()` does: strings, numbers and booleans, present or optional, and lists of them joined by `,`. An object writes `[object Object]`, and other values (a record, a date, a list of lists) by JavaScript rules the server does not reproduce. Pass the field you mean, format a date with `.toISOString()`, or build the string in Rust and pass it as a prop.";
736
744
  };
737
745
  readonly FV0724: {
738
- readonly title: "Integer beyond the range of `i64` where the template needs one, as it renders";
739
- readonly detail: "Integer arithmetic is done on doubles, as JavaScript does it, and a result written into the page is written from the double. Where the template needs the result as an integer, as a child's `number` prop, an index or the count of a `v-for` over a range, it is held as an `i64`, which stops at ±2⁶³: a result beyond that is refused as the component renders, where a cast to `i64` would change the number. A generated `try_render` returns it as a `ferrovue::render::Refused`, with the component and the line and column of the expression, so a server can answer `400 Bad Request`; `render` panics with its message. See [`errors_and_limits`](crate::guide::errors_and_limits#refused-at-render-time). Keep integer props within ±2⁵³, where JavaScript's integers are exact, or type the child's prop `Float`. See [`numbers`](crate::guide::numbers).";
746
+ readonly title: "Integer beyond the range of `i64` where the generated code holds one, as it renders";
747
+ readonly detail: "Integer arithmetic is done on doubles, as JavaScript does it, and a result written into the page is written from the double. Where the generated code holds the result as an integer, as a child's `number` prop, an index, the count of a `v-for` over a range, or an integer a `computed`, a variable, a list or a callback such as `.reduce()`'s keeps, it is held as an `i64`, which stops at ±2⁶³: a result beyond that is refused as the component renders, where a cast to `i64` would change the number. A generated `try_render` returns it as a `ferrovue::render::Refused`, with the component and the line and column of the expression, so a server can answer `400 Bad Request`; `render` panics with its message. See [`errors_and_limits`](crate::guide::errors_and_limits#refused-at-render-time). Keep integer props within ±2⁵³, where JavaScript's integers are exact, or type the numbers it is computed from `Float`. See [`numbers`](crate::guide::numbers).";
740
748
  };
741
749
  readonly FV0725: {
742
750
  readonly title: "String longer than JavaScript allows, as it renders";
@@ -1588,7 +1596,7 @@ export declare const ERRORS: {
1588
1596
  };
1589
1597
  readonly FV1708: {
1590
1598
  readonly title: "`useHeadSafe`";
1591
- readonly detail: "`useHeadSafe` passes its input through unhead's safe input plugin, which keeps only the tags and attributes it deems safe. ferrovue does not reproduce that filter. Use `useHead` with values the page trusts, and keep text you do not control out of `script` and `style`.";
1599
+ readonly detail: "`useHeadSafe` passes its input through unhead's safe input plugin, which keeps only the tags and attributes it deems safe. ferrovue does not reproduce that filter. Use `useHead` with values the page trusts, and keep text you do not control out of `script`, `style` and `noscript`.";
1592
1600
  };
1593
1601
  readonly FV1709: {
1594
1602
  readonly title: "`useSeoMeta` key or value ferrovue does not translate";