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.
- package/README.md +9 -5
- package/dist/attrs.js +62 -4
- package/dist/calls.js +18 -6
- package/dist/children.js +29 -8
- package/dist/classes.js +6 -1
- package/dist/client.d.ts +4 -4
- package/dist/client.js +4 -4
- package/dist/compiler.js +2 -1
- package/dist/component.js +2 -2
- package/dist/conditional.js +3 -3
- package/dist/config.d.ts +4 -0
- package/dist/conformance.js +2 -2
- package/dist/context.d.ts +4 -0
- package/dist/context.js +8 -0
- package/dist/errors.d.ts +14 -6
- package/dist/errors.js +14 -6
- package/dist/link-router.d.ts +5 -2
- package/dist/link-router.js +3 -1
- package/dist/link-routes.d.ts +35 -0
- package/dist/link-routes.js +435 -0
- package/dist/logical.js +3 -4
- package/dist/loops.js +16 -11
- package/dist/members.d.ts +3 -0
- package/dist/members.js +23 -11
- package/dist/model.d.ts +15 -0
- package/dist/model.js +33 -0
- package/dist/narrowing.js +13 -8
- package/dist/numbers.d.ts +2 -0
- package/dist/numbers.js +35 -1
- package/dist/operators.js +2 -2
- package/dist/page-routes.d.ts +1 -1
- package/dist/page-routes.js +1 -1
- package/dist/parens.d.ts +2 -0
- package/dist/parens.js +65 -1
- package/dist/plugins/router-link.js +39 -23
- package/dist/rust.d.ts +1 -1
- package/dist/rust.js +1 -1
- package/dist/schema.js +1 -0
- package/dist/script.d.ts +0 -2
- package/dist/script.js +24 -13
- package/dist/setup-writes.d.ts +15 -0
- package/dist/setup-writes.js +369 -0
- package/dist/slots.js +13 -2
- package/dist/ssr.d.ts +7 -1
- package/dist/ssr.js +4 -3
- package/dist/strings.js +24 -6
- package/dist/template.js +9 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vite.js +30 -3
- package/package.json +5 -1
- 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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
129
|
-
|
|
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",
|
|
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
|
-
|
|
324
|
-
|
|
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.
|
|
387
|
-
|
|
388
|
-
|
|
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
|
|
181
|
-
* next page's location:
|
|
182
|
-
* the page shown as it is, when the container is missing or
|
|
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
|
|
575
|
-
* next page's location:
|
|
576
|
-
* the page shown as it is, when the container is missing or
|
|
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) : [];
|
package/dist/conditional.js
CHANGED
|
@@ -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?: {
|
package/dist/conformance.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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 `
|
|
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";
|