ferrovue 0.13.0 → 0.14.1

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 (62) hide show
  1. package/README.md +10 -6
  2. package/dist/attrs.d.ts +2 -1
  3. package/dist/attrs.js +84 -5
  4. package/dist/calls.js +18 -6
  5. package/dist/children.js +29 -8
  6. package/dist/classes.d.ts +2 -1
  7. package/dist/classes.js +50 -6
  8. package/dist/client.d.ts +4 -4
  9. package/dist/client.js +4 -4
  10. package/dist/compiler.js +2 -1
  11. package/dist/component.js +2 -2
  12. package/dist/conditional.js +16 -7
  13. package/dist/config.d.ts +4 -0
  14. package/dist/conformance.js +2 -2
  15. package/dist/constants.d.ts +22 -2
  16. package/dist/constants.js +87 -5
  17. package/dist/context.d.ts +7 -5
  18. package/dist/context.js +8 -0
  19. package/dist/dynamic.js +3 -2
  20. package/dist/errors.d.ts +14 -6
  21. package/dist/errors.js +14 -6
  22. package/dist/functions.d.ts +1 -4
  23. package/dist/functions.js +10 -11
  24. package/dist/link-router.d.ts +5 -2
  25. package/dist/link-router.js +3 -1
  26. package/dist/link-routes.d.ts +35 -0
  27. package/dist/link-routes.js +436 -0
  28. package/dist/logical.js +3 -4
  29. package/dist/loops.js +16 -11
  30. package/dist/members.d.ts +3 -0
  31. package/dist/members.js +23 -11
  32. package/dist/model.d.ts +23 -0
  33. package/dist/model.js +33 -0
  34. package/dist/narrowing.js +13 -8
  35. package/dist/numbers.d.ts +2 -0
  36. package/dist/numbers.js +35 -1
  37. package/dist/operators.js +2 -2
  38. package/dist/page-routes.d.ts +1 -1
  39. package/dist/page-routes.js +1 -1
  40. package/dist/parens.d.ts +2 -0
  41. package/dist/parens.js +65 -1
  42. package/dist/plugins/loaders.js +3 -4
  43. package/dist/plugins/provide.js +6 -6
  44. package/dist/plugins/router-link.js +39 -23
  45. package/dist/rust.d.ts +1 -1
  46. package/dist/rust.js +1 -1
  47. package/dist/schema.js +1 -0
  48. package/dist/script.d.ts +0 -2
  49. package/dist/script.js +34 -25
  50. package/dist/setup-writes.d.ts +15 -0
  51. package/dist/setup-writes.js +369 -0
  52. package/dist/slots.js +13 -2
  53. package/dist/ssr.d.ts +7 -1
  54. package/dist/ssr.js +4 -3
  55. package/dist/strings.d.ts +3 -1
  56. package/dist/strings.js +31 -6
  57. package/dist/template.js +9 -1
  58. package/dist/typescript.js +4 -3
  59. package/dist/vite.d.ts +1 -1
  60. package/dist/vite.js +30 -3
  61. package/package.json +5 -1
  62. 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.1/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:
@@ -225,7 +226,7 @@ ferrovue compiles `<script setup lang="ts">` components, and components with no
225
226
  |---|---|
226
227
  | Prop types | `string`, `number` (integers, `i64`, written and computed as JavaScript does, exact within ±2⁵³), `Float` from `ferrovue/types` (fractions, `f64`), `boolean`, unions of two or more string literals (`"sm" \| "md"`, an alias, or written in place), each a Rust enum generated for it (`Size::Md`, `"md".parse::<Size>()`, written and serialised as its literal; see the crate's [`props`](https://docs.rs/ferrovue/latest/ferrovue/guide/props/index.html#unions-of-string-literals) guide), or an enum of the application's own that `unions` names for the alias; a list of such a union, a list of its enum (`Size[]` is `Vec<Size>`); `T \| undefined`, `T \| null` (an `Option` written as `null`), arrays (`T[]`, `Array<T>`, `readonly T[]`), interfaces and object type aliases (recursive ones too), object types written in place, mapped types and `Record`s over a union of names, indexed types (`Card["size"]`), tuples (`[label: string, n: number]`, a tuple struct written as an array), dictionaries (`Record<string, T>`, `{ [key: string]: T }`, `ferrovue::props::Record` in Rust, which keeps JavaScript's order of keys), another component's exported `Props`, `TrustedHtml`, `InlineHtml`, `Date` (a prop, optional or nullable too, and inside lists, dictionaries and interfaces: `ferrovue::date::JsDate`, or `chrono::DateTime<Utc>` with the `chrono` feature) |
227
228
  | Shared types | Interfaces and type aliases imported from `.ts` files (generated once, into `types.rs`; imported by a relative path or through an alias the project declares, in its tsconfig.json's `compilerOptions.paths` or, with the Vite plugin, Vite's `resolve.alias`: `@/lib/format`), 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` of string members is a Rust enum, as a union of string literals is, its variants named after its members; one of numbers is a number |
228
- | Constants | Constants imported from `.ts` files, object constants declared in `<script setup>`, and `enum`s (imported, or declared in the component, with members set to such constants or to earlier members), evaluated when the component is compiled: strings, numbers, booleans, `null`, arithmetic and joined strings of them as JavaScript evaluates them (`60 * MINUTE`, `` `${BASE}/feed` ``, `"#" + 1.5`, and `**` where it gives an exact integer within ±2⁵³), 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`); lookup tables: an object constant (`as const`, `Record<Size, T>` or untyped, keys computed from enum members too) read by a key whose type is a union of string or integer literals or an `enum` (a prop, a `computed`, a `v-for` item's field, a `?:` of literals, or built from one by `.toUpperCase()`, `.toLowerCase()` or a template literal), or by a constant number (`O[HALF]` reads `O["0.5"]`), compiled to a `match` over the members (over the variants of a union's enum, with no catch-all arm; a table of a union's members, `Record<Tone, Tone>`, reaches a union prop as a `match` from variant to variant), read on by field (`THEME[tone].fg`) in text, attributes, `:class` and `:style`, with `undefined` for a member the object lacks (`ICONS[kind] ?? "none"`, `ICONS[kind]?.label`, `v-if="ICONS[kind]"`, `kind && ICONS[kind]`); a free-text key tested with `Object.hasOwn(ROLES, key)`, against the object's own keys, which narrows the key for `ROLES[key]` in what follows; `Object.keys()` of an object constant; a constant `new Map([[key, value], …])` of string or integer keys, read with `.get(key)` (`undefined` for a key it lacks) and `.has(key)`. See the crate's [`props`](https://docs.rs/ferrovue/latest/ferrovue/guide/props/index.html#constants) guide |
229
+ | Constants | Constants imported from `.ts` files (exported on their declaration, by name in `export { A, B as C }`, as `export default A`, or passed on with `export { A } from` and `export * from`), object constants declared in `<script setup>`, and `enum`s (imported, or declared in the component, with members set to such constants or to earlier members), evaluated when the component is compiled: strings, numbers, booleans, `null`, arithmetic and joined strings of them as JavaScript evaluates them (`60 * MINUTE`, `` `${BASE}/feed` ``, `"#" + 1.5`, and `**` where it gives an exact integer within ±2⁵³), 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`); lookup tables: an object constant (`as const`, `Record<Size, T>` or untyped, keys computed from enum members too) read by a key whose type is a union of string or integer literals or an `enum` (a prop, a `computed`, a `v-for` item's field, a `?:` of literals, or built from one by `.toUpperCase()`, `.toLowerCase()` or a template literal), or by a constant number (`O[HALF]` reads `O["0.5"]`), compiled to a `match` over the members (over the variants of a union's enum, with no catch-all arm; a table of a union's members, `Record<Tone, Tone>`, reaches a union prop as a `match` from variant to variant), read on by field (`THEME[tone].fg`) in text, attributes, `:class` and `:style`, with `undefined` for a member the object lacks (`ICONS[kind] ?? "none"`, `ICONS[kind]?.label`, `v-if="ICONS[kind]"`, `kind && ICONS[kind]`); a free-text key tested with `Object.hasOwn(ROLES, key)`, against the object's own keys, which narrows the key for `ROLES[key]` in what follows; `Object.keys()` of an object constant; a constant `new Map([[key, value], …])` of string or integer keys, read with `.get(key)` (`undefined` for a key it lacks) and `.has(key)`. See the crate's [`props`](https://docs.rs/ferrovue/latest/ferrovue/guide/props/index.html#constants) guide |
229
230
  | Props | Props declared in place, by an interface of the component, or by one imported from a `.ts` file by a relative path (`defineProps<CardProps>()`), extended or joined with `&`; `withDefaults`, destructured props with defaults and new names (`const { size = "md", label: text } = 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 |
230
231
  | Setup | `ref`/`shallowRef`, `computed` (an expression, or a body of statements: see Statements; one that may throw, through `.repeat()`, `.padStart()`, a date, `$t` or a helper, is evaluated where the render first reads it, once, as Vue does), refs that start empty, typed by their type argument (`ref<Row[]>([])`, `ref<Row>()`, `ref<string \| undefined>(undefined)`), refs of a union of string literals, held as its enum (`ref<Choice>(props.tone ?? "")`, a member, a value of the union, a lookup table's value, or a `?:` or `??` of them), plain `const`/`let`, `.value` in script code, helpers with Rust twins, functions (see Functions), a plain `<script>` block beside setup. Lifecycle hooks, `watch` (not `immediate`), `defineEmits`, `defineSlots`, `defineOptions`, `defineExpose` and template refs (`ref(null)`, `useTemplateRef`) are client-only, as is a function only an event handler calls. The template may name them only from event handlers, which the server drops |
231
232
  | Statements | in a `computed`'s body and in a list method's callback (`.map()`, `.filter()`, `.find()`, `.some()`, `.reduce()`, a `.toSorted()` comparator and the others): `const` and `let` of any value the template may compute, destructured too (`const { name, posts: count } = author`); `if` / `else if` / `else` and early `return`, narrowing what follows as TypeScript narrows it (`if (!user) return ""; return user.name`, `if (!a \|\| !a.title) return "-"`); `switch` over a string or a number, with stacked `case`s, `default` and `break`; assignments to the body's `let`s (`=`, `+=`, `-=`, `*=`, `++`, `--` and the others), with the type TypeScript gives them; `for (const x of list)` over a list, `list.entries()`, `Object.keys()` or `Object.entries()`, `for (const ch of text)` over a string by its characters (a character beyond U+FFFF is one), and counting loops `for (let i = 0; i < n; i++)` to an integer bound, with `break`, `continue`, nested loops and `return` inside them, carrying strings, numbers and booleans from one item to the next (`let total = 0; for (const r of rows) total += r.n;`, thousands separators, the largest of a list). Each value is evaluated once, where JavaScript evaluates it, and only on the path that reaches it. `while`, `do…while` and `for…in` loops, assignments to props, refs, `const`s, parameters and fields, `try`, `throw`, `var` and a path that ends without `return` are refused. See the crate's [`script_logic`](https://docs.rs/ferrovue/latest/ferrovue/guide/script_logic/index.html) guide |
@@ -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.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { type N, type Scope, type Val } from "./model.ts";
1
+ import { type N, type Piece, type Scope, type Val } from "./model.ts";
2
2
  import { type ErrorCode } from "./errors.ts";
3
3
  import { Emitter } from "./emitter.ts";
4
+ export declare function displayPieces(e: Emitter, pieces: Piece[]): void;
4
5
  export declare function display(e: Emitter, v: Val): boolean;
5
6
  export declare function interpolate(s: Scope, e: Emitter, v: Val, n: N): void;
6
7
  export declare function attrValue(e: Emitter, v: Val): void;
package/dist/attrs.js CHANGED
@@ -1,20 +1,70 @@
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";
6
6
  import { ctx } from "./context.js";
7
7
  import { describeTy, expr, shown } from "./expr.js";
8
8
  import { known, truthy } from "./narrowing.js";
9
- import { asCow, isTemporary, unquote } from "./strings.js";
10
- import { atom, bare, condition, receiver, scrutinee, strArg } from "./parens.js";
9
+ import { asCow, isTemporary, piecesOf, unquote } from "./strings.js";
10
+ import { atom, bare, condition, ifElse, logical, 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 emptyTest(v) {
18
+ if (neverEmpty(v.ty))
19
+ return "false";
20
+ const branches = v.branches;
21
+ if (branches !== undefined && branches.of === v.code) {
22
+ const [yes, no] = [emptyTest(branches.yes), emptyTest(branches.no)];
23
+ const binds = branches.test.startsWith("let ");
24
+ if (yes === no && !binds)
25
+ return yes;
26
+ if (!binds && yes === "false")
27
+ return logical(not(branches.test), "&&", no);
28
+ if (!binds && no === "false")
29
+ return logical(branches.test, "&&", yes);
30
+ return `if ${branches.test} { ${yes} } else { ${no} }`;
31
+ }
32
+ const pieces = piecesOf(v);
33
+ if (pieces === undefined)
34
+ return `${receiver(v.code)}.is_empty()`;
35
+ const strs = pieces.filter((p) => typeof p !== "string" && !("shown" in p) && p.ty.k === "str");
36
+ if (strs.length < pieces.length || strs.some((p) => neverEmpty(p.ty)))
37
+ return "false";
38
+ return strs.map((p) => `${receiver(p.code)}.is_empty()`).join(" && ");
39
+ }
40
+ export function displayPieces(e, pieces) {
41
+ for (const p of pieces) {
42
+ if (typeof p === "string")
43
+ e.lit(escapeHtml(p));
44
+ else if ("shown" in p)
45
+ e.stmt(`{ use std::fmt::Write as _; let _ = write!(out, "{}", ${p.shown}); }`);
46
+ else
47
+ display(e, p);
48
+ }
49
+ }
17
50
  export function display(e, v) {
51
+ const pieces = piecesOf(v);
52
+ if (pieces !== undefined) {
53
+ displayPieces(e, pieces);
54
+ return true;
55
+ }
56
+ const branches = v.branches;
57
+ if (branches !== undefined && branches.of === v.code) {
58
+ e.open(`if ${branches.test}`);
59
+ display(e, branches.yes);
60
+ e.close(" else {");
61
+ const at = e.lines.length;
62
+ display(e, branches.no);
63
+ e.close();
64
+ if (e.lines.length === at + 1)
65
+ e.lines.splice(at - 1, 2, e.lines[at - 1].replace(/\} else \{$/, "}"));
66
+ return true;
67
+ }
18
68
  if (v.ty.k === "str" && known(v) !== undefined)
19
69
  e.lit(escapeHtml(unquote(v.code)));
20
70
  else if ((v.ty.k === "int" || v.ty.k === "float") && v.num !== undefined)
@@ -65,10 +115,31 @@ function renderable(s, key, v, n) {
65
115
  const fix = own?.fix ?? (ty.k === "list" ? 'join it into one string, as `.join(",")`' : "bind a string, a number or a boolean");
66
116
  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
117
  }
118
+ /** For an optional string that is a literal where a test holds and absent where it fails, the
119
+ * test and the literal. */
120
+ function someLiteral(v) {
121
+ const branches = v.ty.k === "opt" ? ifElse(v.code) : null;
122
+ if (branches === null || v.ty.k !== "opt")
123
+ return null;
124
+ const literal = (code) => /^Some\(("(?:[^"\\]|\\.)*")\)$/.exec(code)?.[1];
125
+ const [yes, no] = [literal(branches.yes), literal(branches.no)];
126
+ if (yes !== undefined && branches.no === "None")
127
+ return { test: branches.test, value: { code: yes, ty: v.ty.of } };
128
+ if (no !== undefined && branches.yes === "None" && !branches.test.startsWith("let "))
129
+ return { test: not(branches.test), value: { code: no, ty: v.ty.of } };
130
+ return null;
131
+ }
68
132
  export function renderAttr(s, e, key, v, n) {
69
133
  renderable(s, key, v, n);
70
134
  if (nothing(v.ty))
71
135
  return;
136
+ const some = someLiteral(v);
137
+ if (some !== null) {
138
+ e.open(`if ${condition(some.test)}`);
139
+ renderAttr(s, e, key, some.value, n);
140
+ e.close();
141
+ return;
142
+ }
72
143
  if (v.ty.k === "opt") {
73
144
  e.open(`if let Some(v) = ${scrutinee(v.code)}`);
74
145
  renderAttr(s, e, key, { code: "v", ty: v.ty.of }, n);
@@ -104,6 +175,13 @@ export function renderDynamicAttr(s, e, key, v, n, tag = null) {
104
175
  e.close();
105
176
  return;
106
177
  }
178
+ const some = someLiteral(v);
179
+ if (some !== null) {
180
+ e.open(`if ${condition(some.test)}`);
181
+ renderDynamicAttr(s, e, key, some.value, n, tag);
182
+ e.close();
183
+ return;
184
+ }
107
185
  if (v.ty.k === "opt") {
108
186
  e.open(`if let Some(v) = ${scrutinee(v.code)}`);
109
187
  renderDynamicAttr(s, e, key, { code: "v", ty: v.ty.of }, n, tag);
@@ -125,8 +203,9 @@ export function renderDynamicAttr(s, e, key, v, n, tag = null) {
125
203
  e.lit(` ${name}`);
126
204
  return;
127
205
  }
128
- if (v.ty.k === "str" && known(v) === undefined) {
129
- e.open(`if ${receiver(v.code)}.is_empty()`);
206
+ const empty = v.ty.k === "str" && known(v) === undefined ? emptyTest(v) : "false";
207
+ if (empty !== "false") {
208
+ e.open(`if ${empty}`);
130
209
  e.lit(` ${name}`);
131
210
  e.close(" else {");
132
211
  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.d.ts CHANGED
@@ -1,9 +1,10 @@
1
- import { type N, type Scope } from "./model.ts";
1
+ import { type N, type Scope, type Val } from "./model.ts";
2
2
  import { Emitter } from "./emitter.ts";
3
3
  export type ClassItem = {
4
4
  lit: string;
5
5
  } | {
6
6
  code: string;
7
+ join?: Val;
7
8
  };
8
9
  export declare function classItems(s: Scope, n: N): ClassItem[];
9
10
  export declare function renderClass(s: Scope, e: Emitter, n: N, after?: boolean): void;
package/dist/classes.js CHANGED
@@ -3,10 +3,10 @@ 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, piecesOf, 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
- import { stringAttr } from "./attrs.js";
9
+ import { displayPieces, stringAttr } from "./attrs.js";
10
10
  import { fresh, jsHint } from "./names.js";
11
11
  export function classItems(s, n) {
12
12
  switch (n.type) {
@@ -73,8 +73,13 @@ 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
- return [{ code: v.code }];
82
+ return [piecesOf(v) === undefined ? { code: v.code } : { code: v.code, join: v }];
78
83
  if (v.ty.k === "opt" && v.ty.of.k === "str")
79
84
  return [{ code: `${atom(v.code)}.unwrap_or("")` }];
80
85
  if (nothing(v.ty))
@@ -113,10 +118,49 @@ export function renderClass(s, e, n, after = false) {
113
118
  e.lit((wrote ? " " : "") + escapeHtml(it.lit));
114
119
  wrote = true;
115
120
  }
116
- if (i < items.length) {
117
- const rest = items.slice(i).map((it) => ("lit" in it ? rustStr(it.lit) : it.code));
118
- e.stmt(`fv::attrs::class_into(out, ${wrote}, &[${rest.join(", ")}]);`);
121
+ let group = [];
122
+ let at = null;
123
+ for (; i < items.length; i++) {
124
+ const it = items[i];
125
+ const parts = "join" in it && it.join !== undefined ? trimmedParts(it.join) : null;
126
+ if (parts === null) {
127
+ if (!group.length && !wrote && items.slice(i + 1).some((later) => "join" in later && later.join !== undefined)) {
128
+ at = fresh("at", "class");
129
+ e.stmt(`let ${at} = out.len();`);
130
+ }
131
+ group.push("lit" in it ? rustStr(it.lit) : it.code);
132
+ continue;
133
+ }
134
+ if (group.length)
135
+ e.stmt(`fv::attrs::class_into(out, ${wrote}, &[${group.join(", ")}]);`);
136
+ if (wrote)
137
+ e.lit(" ");
138
+ else if (group.length && at !== null)
139
+ e.stmt(`if out.len() != ${at} { out.push(' '); }`);
140
+ displayPieces(e, parts);
141
+ [group, at, wrote] = [[], null, true];
142
+ }
143
+ if (group.length)
144
+ e.stmt(`fv::attrs::class_into(out, ${wrote}, &[${group.join(", ")}]);`);
145
+ }
146
+ /** The pieces of a joined class name with the spaces `normalizeClass` trims taken off its ends,
147
+ * when only its first or last piece may hold them and the join writes a name: null otherwise. */
148
+ function trimmedParts(v) {
149
+ const pieces = [...piecesOf(v)];
150
+ const solid = (p, start) => p !== undefined && (typeof p === "string" ? p !== "" && p === (start ? p.trimStart() : p.trimEnd()) : "shown" in p || p.ty.k !== "str");
151
+ for (const start of [true, false]) {
152
+ const edge = start ? 0 : pieces.length - 1;
153
+ const next = start ? 1 : pieces.length - 2;
154
+ const p = pieces[edge];
155
+ if (typeof p === "string")
156
+ pieces[edge] = start ? p.trimStart() : p.trimEnd();
157
+ if (solid(pieces[edge], start))
158
+ continue;
159
+ if (typeof p === "string" || "shown" in p || pieces.length < 2 || !solid(pieces[next], start))
160
+ return null;
161
+ pieces[edge] = { code: `fv::strings::js_trim_${start ? "start" : "end"}(${strArg(p.code)})`, ty: STR };
119
162
  }
163
+ return pieces;
120
164
  }
121
165
  const UNWRITTEN = new Set(["on", "if", "else-if", "else", "for", "slot", "once", "memo", "cloak", "pre"]);
122
166
  /** The class list of an element in content Vue renders from virtual nodes, in the order Vue's
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,10 +1,10 @@
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";
5
- import { atom, bare, enclosed, occurrences, strArg } from "./parens.js";
5
+ import { atom, bare, condition, enclosed, occurrences, strArg } from "./parens.js";
6
6
  import { collected, sliced, typedEmpty, untypedEmpty } from "./lists.js";
7
- import { asCow, isTemporary, loneOf } from "./strings.js";
7
+ import { asCow, isTemporary, loneOf, piecesOf } from "./strings.js";
8
8
  import { asF64, intFromF64, isCast, isNumber } from "./numbers.js";
9
9
  import { choice, known, literalNarrowing, narrowTo, presence, truthTested, truthy } from "./narrowing.js";
10
10
  import { ownedStruct, ownedStructs } from "./objects.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)];
@@ -65,7 +65,14 @@ function branches(s, n, t) {
65
65
  a = { ...a, ty: b.ty };
66
66
  if (emptyB && a.ty.k === "list")
67
67
  b = { ...b, ty: a.ty };
68
- return choiceOf({ comp, n, a, b, emptyA, emptyB, choose: chooser(t, p, name, a), lone: loneOf(a, b) });
68
+ const [x, y] = [a, b];
69
+ const head = () => {
70
+ if (!p)
71
+ return { test: condition(truthy(t)), yes: x, no: y };
72
+ const [present, absent] = p.negated ? [y, x] : [x, y];
73
+ return { test: occurrences(present.code, name) ? p.pattern(name) : condition(p.present), yes: present, no: absent };
74
+ };
75
+ return choiceOf({ comp, n, a, b, emptyA, emptyB, choose: chooser(t, p, name, a), head, lone: loneOf(a, b) });
69
76
  }
70
77
  function chooser(t, p, name, a) {
71
78
  const second = (code) => (a.ty.k === "str" ? strArg(code) : bare(code));
@@ -90,7 +97,9 @@ function choiceOf(c) {
90
97
  if (owned !== null)
91
98
  return owned;
92
99
  if (sameTy(a.ty, b.ty) && a.ty.k === "str" && (isTemporary(a) || isTemporary(b))) {
93
- return { code: `&*(${choose(asCow(a), asCow(b))})`, ty: STR, ...lone };
100
+ const code = `&*(${choose(asCow(a), asCow(b))})`;
101
+ const joins = (v) => piecesOf(v) !== undefined || v.branches?.of === v.code;
102
+ return { code, ty: STR, ...lone, ...(joins(a) || joins(b) ? { branches: { of: code, ...c.head() } } : {}) };
94
103
  }
95
104
  if (sameTy(a.ty, b.ty) && a.ty.k === "list" && a.ty.of.k !== "undef")
96
105
  return listChoice(c);
@@ -174,7 +183,7 @@ function pickedBy(v, pick) {
174
183
  function ownedChoice(comp, a, b, n) {
175
184
  const base = (ty) => (ty.k === "opt" ? ty.of : nothing(ty) ? null : ty);
176
185
  const [ba, bb] = [base(a.ty), base(b.ty)];
177
- const of = ba ?? bb;
186
+ const of = ba !== null && bb !== null ? joinTy(ba, bb) : (ba ?? bb);
178
187
  if (of === null || (ba !== null && bb !== null && !sameTy(ba, bb)))
179
188
  return null;
180
189
  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?: {