ferrovue 0.4.0 → 0.5.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 +198 -34
- package/dist/attrs.d.ts +1 -1
- package/dist/attrs.js +24 -22
- package/dist/calls.js +17 -15
- package/dist/children.d.ts +9 -2
- package/dist/children.js +48 -35
- package/dist/classes.js +11 -11
- package/dist/cli.js +31 -5
- package/dist/client.d.ts +35 -2
- package/dist/client.js +77 -2
- package/dist/compiler.d.ts +1 -1
- package/dist/compiler.js +7 -3
- package/dist/component.d.ts +3 -1
- package/dist/component.js +114 -19
- package/dist/constants.d.ts +42 -0
- package/dist/constants.js +292 -0
- package/dist/context.d.ts +25 -1
- package/dist/context.js +11 -5
- package/dist/diagnostics.d.ts +23 -0
- package/dist/diagnostics.js +30 -0
- package/dist/errors.d.ts +867 -0
- package/dist/errors.js +330 -0
- package/dist/expr.d.ts +1 -0
- package/dist/expr.js +120 -49
- package/dist/fixture.d.ts +3 -10
- package/dist/fixture.js +2 -6
- package/dist/hydration.d.ts +15 -0
- package/dist/hydration.js +80 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/link-router.d.ts +24 -0
- package/dist/link-router.js +36 -0
- package/dist/lists.js +30 -21
- package/dist/loops.js +14 -10
- package/dist/model.d.ts +32 -1
- package/dist/model.js +123 -6
- package/dist/narrowing.d.ts +8 -0
- package/dist/narrowing.js +40 -9
- package/dist/plugins/client-only.d.ts +8 -0
- package/dist/plugins/client-only.js +107 -0
- package/dist/plugins/i18n.js +14 -14
- package/dist/plugins/index.js +3 -1
- package/dist/plugins/router-link.js +33 -30
- package/dist/plugins/router.js +9 -9
- package/dist/plugins/scoped.js +1 -1
- package/dist/plugins/shared-types.js +1 -1
- package/dist/plugins/stores.js +26 -24
- package/dist/plugins/teleport.js +3 -3
- package/dist/plugins/twins.d.ts +10 -0
- package/dist/plugins/twins.js +164 -0
- package/dist/routes.d.ts +10 -0
- package/dist/routes.js +6 -0
- package/dist/rust.js +18 -14
- package/dist/script.d.ts +3 -0
- package/dist/script.js +119 -17
- package/dist/settle.d.ts +6 -0
- package/dist/settle.js +34 -0
- package/dist/slots.js +18 -18
- package/dist/strings.js +11 -11
- package/dist/styles.js +13 -13
- package/dist/template.js +11 -11
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +3 -2
- package/dist/typescript.d.ts +2 -1
- package/dist/typescript.js +46 -19
- package/dist/vite.d.ts +1 -1
- package/dist/vite.js +10 -9
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -61,10 +61,12 @@ crate that includes it needs **edition 2024**.
|
|
|
61
61
|
| `stores` | no | Directory of Pinia option stores whose state components may read |
|
|
62
62
|
| `trustedHtml` | no | Rust type of a `TrustedHtml` prop, e.g. `crate::html::Sanitised` (needed for `v-html`) |
|
|
63
63
|
| `helpers` | no | `{ module, functions }`: functions a template may call, each mapped to a Rust twin |
|
|
64
|
+
| `twins` | no | Components ferrovue does not compile, each rendered by a Rust function of yours: `{ "VBtn": { "rust": "crate::ui::v_btn", "props": { "label": "string" }, "slots": ["default"] } }`. See [Escape hatches](#escape-hatches) |
|
|
64
65
|
| `i18n` | no | vue-i18n: `{ messages, locale?, fallbackLocale? }`, the directory of locale files (`en.json`, `nl.json`), the default locale and the fallbacks |
|
|
65
66
|
| `clientDirectives` | no | Custom directives with no server output (no `getSSRProps`), by name without `v-`: `["focus"]` |
|
|
66
67
|
| `scopeId` | no | How a `<style scoped>` id is hashed, as `@vitejs/plugin-vue` hashes it: `"filepath-source"` (the default, the plugin's in a production build) or `"filepath"`. See [Scoped styles](#scoped-styles) |
|
|
67
68
|
| `viteRoot` | no | Vite's root, relative to this file's directory, from which a component's path is hashed (default `.`) |
|
|
69
|
+
| `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`) |
|
|
68
70
|
|
|
69
71
|
### 3. Write a component
|
|
70
72
|
|
|
@@ -86,9 +88,16 @@ npx ferrovue init # a starter ferrovue.config.json and components/Hello.vu
|
|
|
86
88
|
npx ferrovue # writes src/generated/{greeting.rs, mod.rs}
|
|
87
89
|
npx ferrovue --check # in CI: exits 1 if the committed modules are stale
|
|
88
90
|
npx ferrovue --check --diff # …and shows what differs, as `diff -u` does
|
|
91
|
+
npx ferrovue --check --format json # errors as JSON, for an editor or CI annotations
|
|
89
92
|
npx ferrovue --watch # regenerates when components or configs change
|
|
90
93
|
```
|
|
91
94
|
|
|
95
|
+
Every error carries a stable code, as `error[FV0602]: components/Card.vue:4:17: …`, documented in
|
|
96
|
+
the crate's [`error_codes`](https://docs.rs/ferrovue/latest/ferrovue/guide/error_codes/index.html)
|
|
97
|
+
guide. `--format json` writes the same errors as JSON with the exit status unchanged, and a VS Code
|
|
98
|
+
problem matcher for `tasks.json` puts them in the Problems panel: both are in the
|
|
99
|
+
[quick start](https://docs.rs/ferrovue/latest/ferrovue/guide/quick_start/index.html#editor-and-ci-diagnostics).
|
|
100
|
+
|
|
92
101
|
```rust
|
|
93
102
|
#[rustfmt::skip]
|
|
94
103
|
mod generated;
|
|
@@ -107,7 +116,7 @@ let island = greeting::island(&props).into_string();
|
|
|
107
116
|
|
|
108
117
|
// (`island()` exists for a component that renders from its props alone: the client rebuilds it from
|
|
109
118
|
// `data-props`. One that also takes slots, the route, stores, translations or teleports has `html()`,
|
|
110
|
-
// and the page's own app mounts it
|
|
119
|
+
// and the page's own app mounts it; see examples/fullstack.)
|
|
111
120
|
|
|
112
121
|
// …or straight into a buffer you already hold:
|
|
113
122
|
let mut page = String::from("<!doctype html><body>");
|
|
@@ -120,7 +129,7 @@ route.
|
|
|
120
129
|
|
|
121
130
|
The router and vue-i18n are features of the crate, `router` and `i18n`, both on by default. An
|
|
122
131
|
application with neither routes nor translations can leave them out, and build neither
|
|
123
|
-
`ferrovue-router` nor `ferrovue-i18n`: `ferrovue = { version = "0.
|
|
132
|
+
`ferrovue-router` nor `ferrovue-i18n`: `ferrovue = { version = "0.5", default-features = false }`.
|
|
124
133
|
|
|
125
134
|
With the `maud` feature, `ferrovue::Html` implements `maud::Render`, so `(greeting::html(&props))` can go
|
|
126
135
|
straight into a `maud::html!` page.
|
|
@@ -142,27 +151,30 @@ hydrates inside it. See `examples/dioxus` and the crate guide's
|
|
|
142
151
|
|
|
143
152
|
## What a component may use
|
|
144
153
|
|
|
145
|
-
ferrovue compiles `<script setup lang="ts">` components
|
|
154
|
+
ferrovue compiles `<script setup lang="ts">` components, and components with no script at all
|
|
155
|
+
(an icon, a divider), which take no props. Props are declared by type, with
|
|
146
156
|
`defineProps<{ … }>()`, an interface, or a type alias.
|
|
147
157
|
|
|
148
158
|
| Area | Supported |
|
|
149
159
|
|---|---|
|
|
150
|
-
| Prop types | `string`, `number` (integers, `i64`, written and computed as JavaScript does
|
|
151
|
-
| Shared types | Interfaces and type aliases imported from `.ts` files (generated once, into `types.rs`), from a store's file, or from another component's `.vue` file; objects of a shared type can be passed between components |
|
|
152
|
-
|
|
|
153
|
-
|
|
|
160
|
+
| Prop types | `string`, `number` (integers, `i64`, written and computed as JavaScript does, exact within ±2⁵³), `Float` from `ferrovue/types` (fractions, `f64`), `boolean`, string-literal unions (`"sm" \| "md"`), `T \| undefined`, `T \| null` (an `Option` written as `null`), arrays (`T[]`, `Array<T>`, `readonly T[]`), interfaces and object type aliases (recursive ones too), dictionaries (`Record<string, T>`, `{ [key: string]: T }`, `ferrovue::Record` in Rust, which keeps JavaScript's order of keys), another component's exported `Props`, `TrustedHtml` |
|
|
161
|
+
| Shared types | Interfaces and type aliases imported from `.ts` files (generated once, into `types.rs`), from a store's file, or from another component's `.vue` file; objects of a shared type can be passed between components. A TypeScript `enum` whose members are literals is the type of its values: a string, or a number |
|
|
162
|
+
| Constants | Constants imported from `.ts` files and `enum`s (imported, or declared in the component), evaluated when the component is compiled: strings, numbers, booleans, `null`, lists of them, objects of them read field by field (`LABELS.save`, `Tone.Loud`, `Rank[5]`), and lists of objects of them, which become a `const` in `types.rs` (`{ value, label }` items get a type named after the constant, `OptionsItem`, unless the constant is typed with an interface: `OPTIONS: Option[]`; a field that is `null` in some items is `T | null`). See [Constants](#constants) |
|
|
163
|
+
| Props | `withDefaults`, destructured props with defaults (`const { size = "md" } = defineProps<…>()`), the props object (`const props = defineProps<…>()`, then `props.label` in the template or in script code), optional booleans (Vue casts an absent one to `false`), `defineModel` (named, required, with defaults), components with no props. A generic component (`<script setup generic="T extends Item">`) renders each type parameter as its constraint, which is all the template can rely on |
|
|
164
|
+
| Setup | `ref`/`shallowRef`, `computed` (an expression or a single `return`), refs that start empty, typed by their type argument (`ref<Row[]>([])`, `ref<Row>()`, `ref<string \| undefined>(undefined)`), plain `const`/`let`, `.value` in script code, helpers with Rust twins, a plain `<script>` block beside setup. Lifecycle hooks, `watch` (not `immediate`), `defineEmits`, `defineSlots`, `defineOptions`, `defineExpose`, `provide`, template refs (`ref(null)`, `useTemplateRef`) and functions are client-only. The template may name them only from event handlers, which the server drops |
|
|
154
165
|
| Text | `{{ }}` of strings, integers and booleans; `+`, `-`, `*`, `/`, `%`, with integers and fractions as JavaScript computes them; `<`, `>`, `<=`, `>=` (between numbers, or between strings by UTF-16 code unit), `===`, `!==`; unary `-`, `!`; `??`, `\|\|`, `&&`, `?:`; template literals; optional chaining `a?.b`; `.length`; `String()`, `.toString()`, `.toFixed()`, `Math.max`/`min`/`abs`/`round`/`floor`/`ceil`/`trunc`; `Number()`, `parseInt()` (no radix, 10 or 16), `parseFloat()`; `JSON.stringify()` of strings, numbers, booleans and lists of those; array literals |
|
|
155
166
|
| Strings | `.trim()`, `.trimStart()`, `.trimEnd()`, `.toUpperCase()`, `.toLowerCase()`, `.includes()`, `.startsWith()`, `.endsWith()`, `.indexOf()`, `.lastIndexOf()`, `.slice()`, `.substring()`, `.at()`, `.charAt()`, `.split()`, `.replace()` and `.replaceAll()` with string patterns (`$&`, `` $` ``, `$'`, `$$` read as JavaScript reads them), `.padStart()`, `.padEnd()`, `.repeat()`; indices and lengths count UTF-16 code units, as JavaScript's do (see [Strings](#strings)) |
|
|
156
|
-
| Conditions | `v-if` / `v-else-if` / `v-else`, `?:`, `&&` and `||`, with optional values narrowed as TypeScript narrows them: `v-if="user"`, `user !== undefined`, and `!user` or `user ===
|
|
157
|
-
| Lists | `v-for` over arrays (of strings, numbers, objects or child props), array literals and number ranges (`n in 5`); with an index; with destructured items (`{ id, name } in rows`); nested; on `<template>`. `.includes()`, `.join()`, and `.filter()`, `.map()`, `.some()`, `.every()`, `.find()`, `.findIndex()` with an arrow function of the item (a name, or destructured) and its index, whose body is an expression, narrowing inside as outside; `.slice()`; chained and nested; a computed list read by `v-for`, `.join()`, `.length`, `.includes()`, kept by `computed`, or handed to a child |
|
|
167
|
+
| Conditions | `v-if` / `v-else-if` / `v-else`, `?:`, `&&` and `||`, with optional and nullable values narrowed as TypeScript narrows them: `v-if="user"`, `user !== undefined`, `user !== null`, `user != null`, and `!user`, `user === undefined` or `user === null` for the `v-else` |
|
|
168
|
+
| 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()` with an arrow function of the item (a name, or destructured) and its index, whose body is an expression, narrowing inside as outside; `.slice()`; chained and nested; a computed list read by `v-for`, `.join()`, `.length`, `.includes()`, kept by `computed`, or handed to a child |
|
|
158
169
|
| Dictionaries | `v-for="(value, key, index) in r"` over a `Record<string, T>`, and `([key, value], index) in Object.entries(r)`, in JavaScript's order: keys that are array indices first, in numeric order, then the others as the props give them; `Object.keys(r)`, `Object.values(r)`, `Object.entries(r).length`; handed to a child |
|
|
159
170
|
| Attributes | static and bound attributes, boolean attributes, `:hidden`, `data-*` and `aria-*`, `v-bind` objects |
|
|
160
171
|
| `class` | strings, arrays, objects (`{ active: on }`, computed keys), `cond && "x"`, `cond ? "x" : null`, merged with a static `class` |
|
|
161
172
|
| `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 |
|
|
162
173
|
| 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 |
|
|
163
|
-
| Components | imported child components, `v-bind` of a child's own `Props`, `v-model` on a child's `defineModel`, recursion; props named in `kebab-case` or `camelCase` |
|
|
174
|
+
| Components | imported child components, `v-bind` of a child's own `Props`, `v-model` on a child's `defineModel`, recursion; props named in `kebab-case` or `camelCase`; `defineAsyncComponent(() => import("./X.vue"))` (or `{ loader: … }`), rendered as the component it loads, which Vue's server renderer waits for |
|
|
175
|
+
| Escape hatches | `<ClientOnly>` from `ferrovue/client`, whose default slot the server never renders, so anything may go in it; components listed in `twins`, rendered by Rust functions of yours. See [Escape hatches](#escape-hatches) |
|
|
164
176
|
| 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 |
|
|
165
|
-
| Slots | default and named slots, fallbacks, `$slots.name` tests
|
|
177
|
+
| 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 |
|
|
166
178
|
| Forms | `v-model` on text inputs, checkboxes, radios, `<select>` and `<textarea>` (renders the initial state) |
|
|
167
179
|
| 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` |
|
|
168
180
|
| Vue Router | `<RouterLink>` (resolved by name or imported) with a string `to` or `{ name, params, query, hash }` / `{ path, query, hash }`, `active-class`, `exact-active-class`, `aria-current-value`, `replace`; vue-router's own encoding and active-link matching, nested routes included (a parent link is active on its children's pages, exact only on its own); a history base. `useRoute()` and `$route`: `path`, `fullPath`, `hash`, `name`, `params`, and `query` (a value written once, without `=`, or repeated, exactly as vue-router parses it; `typeof route.query.q === "string"` narrows one to a single string). `<RouterView>`, at the top and in nested route components: each takes the page it shows as a slot |
|
|
@@ -177,6 +189,10 @@ Refused at compile time, each with an error that names the construct:
|
|
|
177
189
|
id; and, since vue-router renders a link from virtual nodes, a `<slot>` inside a `<RouterLink>`
|
|
178
190
|
that takes scope ids, or an element inside one in slot content given a `:slotted()` id
|
|
179
191
|
- `<component :is>`
|
|
192
|
+
- the Options API (a `<script>` without `setup`), and a type parameter of a generic component with
|
|
193
|
+
no constraint (`generic="T"`)
|
|
194
|
+
- constants that are not literals (`Date.now()`, a function), an object constant read whole or by a
|
|
195
|
+
key chosen at run time (`LABELS[key]`), and `new` (`new Date(at)`, `new Intl.NumberFormat()`)
|
|
180
196
|
- `<RouterLink custom>`, slot props that are array literals, defaults in destructured slot props, and outlets of one slot that pass different props
|
|
181
197
|
- custom directives not listed in `clientDirectives`
|
|
182
198
|
- `watchEffect`, `watch` with `immediate`, `onServerPrefetch`, top-level `await`, and statements in setup that change state
|
|
@@ -193,7 +209,9 @@ Refused at compile time, each with an error that names the construct:
|
|
|
193
209
|
it is not; `Object.entries()` anywhere but as a `v-for`'s source; dictionaries of optional values
|
|
194
210
|
- two strings that may each hold half of a surrogate pair compared, searched or joined (see
|
|
195
211
|
[Strings](#strings))
|
|
196
|
-
- `null`
|
|
212
|
+
- a type that is both `null` and `undefined` (`T | null | undefined`, `x?: T | null`), a default
|
|
213
|
+
for a nullable prop, and `=== null` or `=== undefined` of a value that may be either (`a?.b` where
|
|
214
|
+
`b` is nullable; `== null` tests both)
|
|
197
215
|
- an attribute (`class` and `style` aside) bound to a value that may be neither a string, a number
|
|
198
216
|
nor a boolean: a list, an object, or a `route.query` value, which is an array when its key is
|
|
199
217
|
repeated. Vue's server renderer leaves the attribute out and hydration then sets it, silently;
|
|
@@ -207,16 +225,50 @@ Refused at compile time, each with an error that names the construct:
|
|
|
207
225
|
passed to a component whose root is a `<Transition>` or `<KeepAlive>` around a `v-if`, which Vue's
|
|
208
226
|
server drops but its client keeps
|
|
209
227
|
|
|
228
|
+
Every error names the file, line and column, with the line quoted and a caret under the construct.
|
|
229
|
+
|
|
210
230
|
An object prop handed to a child component is cloned. Its strings are `Cow`s, so borrowed ones
|
|
211
231
|
cost nothing to copy.
|
|
212
232
|
|
|
233
|
+
### Constants
|
|
234
|
+
|
|
235
|
+
A constant a component imports from a `.ts` file, or an `enum`, is evaluated when the component is
|
|
236
|
+
compiled, as `types.rs` holds the shared types:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
// components/catalog.ts
|
|
240
|
+
export const LABELS = { title: "Catalogue", empty: "Nothing yet" } as const;
|
|
241
|
+
export const SIZES = ["sm", "md", "lg"] as const;
|
|
242
|
+
export const SORTS = [
|
|
243
|
+
{ key: "name", label: "By name" },
|
|
244
|
+
{ key: "price", label: "By price" },
|
|
245
|
+
] as const;
|
|
246
|
+
export enum Tone { Calm = "calm", Loud = "loud" }
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
`{{ LABELS.title }}` and `v-if="tone === Tone.Loud"` are written into the page as the strings they
|
|
250
|
+
are, and `v-for="s in SIZES"` walks an array literal. A list of objects becomes a `const` beside
|
|
251
|
+
the shared types, with a type for its items (here `SortsItem`, or the interface the constant is
|
|
252
|
+
declared with):
|
|
253
|
+
|
|
254
|
+
```rust,ignore
|
|
255
|
+
pub const SORTS: &[SortsItem<'static>] = &[
|
|
256
|
+
SortsItem { key: Cow::Borrowed("name"), label: Cow::Borrowed("By name") },
|
|
257
|
+
SortsItem { key: Cow::Borrowed("price"), label: Cow::Borrowed("By price") },
|
|
258
|
+
];
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
A constant is a literal, a list, an object, or another constant of the same file. One that is
|
|
262
|
+
anything else (`Date.now()`, a function) is refused where the template reads it, naming the file
|
|
263
|
+
and line it is declared on.
|
|
264
|
+
|
|
213
265
|
### Scoped styles
|
|
214
266
|
|
|
215
267
|
A `<style scoped>` component's elements carry `data-v-<id>`, and its CSS is rewritten by the client
|
|
216
268
|
build to select them. The server has to write the id the client build chose, which is not something
|
|
217
269
|
the browser checks when it hydrates: a wrong id hydrates cleanly and leaves the styles unapplied. So
|
|
218
|
-
ferrovue computes it as `@vitejs/plugin-vue` does
|
|
219
|
-
`.vue` file's path from Vite's root, followed by its source unless only the path is hashed
|
|
270
|
+
ferrovue computes it as `@vitejs/plugin-vue` does (the first 8 hex digits of a SHA-256 of the
|
|
271
|
+
`.vue` file's path from Vite's root, followed by its source unless only the path is hashed), and the
|
|
220
272
|
two must be configured alike:
|
|
221
273
|
|
|
222
274
|
| `@vitejs/plugin-vue` | `ferrovue.config.json` |
|
|
@@ -254,8 +306,8 @@ String methods count as JavaScript counts: in UTF-16 code units, so `"🦀".leng
|
|
|
254
306
|
from U+E000 to U+FFFF after one beyond U+FFFF. Each runtime routine is held to vectors recorded from
|
|
255
307
|
JavaScript (`crates/*/tests/vectors/`).
|
|
256
308
|
|
|
257
|
-
A JavaScript string can hold half of a surrogate pair
|
|
258
|
-
`.split("")`
|
|
309
|
+
A JavaScript string can hold half of a surrogate pair (`"🦀".slice(0, 1)`, `.charAt(1)`,
|
|
310
|
+
`.split("")`), and a Rust string cannot. ferrovue writes U+FFFD in its place, which is exactly what
|
|
259
311
|
the page carries anyway: a server sends Vue's string as UTF-8, and UTF-8 writes each half as U+FFFD
|
|
260
312
|
(`res.end`, `Buffer.from` and `TextEncoder` all do). Its length is the same. Where the half itself
|
|
261
313
|
would decide the result, ferrovue refuses at compile time: two strings that may each hold a half
|
|
@@ -277,8 +329,8 @@ Where JavaScript throws, the generated code panics, as Vue's render rejects: a n
|
|
|
277
329
|
|
|
278
330
|
### Dictionaries
|
|
279
331
|
|
|
280
|
-
A `Record<string, T>` prop is a `ferrovue::Record`, built from pairs
|
|
281
|
-
`[("b", 1), ("10", 2)].into_iter().collect()`
|
|
332
|
+
A `Record<string, T>` prop is a `ferrovue::Record`, built from pairs
|
|
333
|
+
(`[("b", 1), ("10", 2)].into_iter().collect()`) or read from JSON. It holds its keys in the order a
|
|
282
334
|
JavaScript object does, array indices (`"0"` to `"4294967294"`) first in numeric order, so `v-for`
|
|
283
335
|
walks them as Vue does, and it is written back as JSON in that order, which the browser reads back
|
|
284
336
|
the same.
|
|
@@ -293,6 +345,66 @@ let i18n = generated::i18n::i18n("nl"); // falls back as configured
|
|
|
293
345
|
page::render(&mut out, &props, &i18n);
|
|
294
346
|
```
|
|
295
347
|
|
|
348
|
+
### Escape hatches
|
|
349
|
+
|
|
350
|
+
Some components will never compile: a component library's render functions, `inject` chains, code
|
|
351
|
+
that measures the page in `onMounted`. Two ways keep one of them from blocking the page around it.
|
|
352
|
+
|
|
353
|
+
**`<ClientOnly>`**: the server writes its `#fallback` slot (or a comment, without one), and the
|
|
354
|
+
browser hydrates that same fallback, then swaps in the default slot once mounted. Both sides render
|
|
355
|
+
the fallback first, so this is exact by construction. ferrovue does not compile the default slot at
|
|
356
|
+
all: any component, import or expression may go there.
|
|
357
|
+
|
|
358
|
+
```vue
|
|
359
|
+
<script setup lang="ts">
|
|
360
|
+
import { ClientOnly } from "ferrovue/client";
|
|
361
|
+
import { VDataTable } from "vuetify/components";
|
|
362
|
+
|
|
363
|
+
defineProps<{ rows: { name: string }[] }>();
|
|
364
|
+
</script>
|
|
365
|
+
|
|
366
|
+
<template>
|
|
367
|
+
<ClientOnly>
|
|
368
|
+
<VDataTable :items="rows" />
|
|
369
|
+
<template #fallback><p class="loading">Loading the table…</p></template>
|
|
370
|
+
</ClientOnly>
|
|
371
|
+
</template>
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
The swap needs Vue running on that part of the page, so put `<ClientOnly>` inside an island (or an
|
|
375
|
+
app the client hydrates whole); elsewhere the fallback stays. `<ClientOnly>` takes no attributes.
|
|
376
|
+
|
|
377
|
+
**Rust twins**: a component the server should render, written by you in Rust. List it under
|
|
378
|
+
`twins` with the props a template may pass it and the slots it may fill:
|
|
379
|
+
|
|
380
|
+
```json
|
|
381
|
+
{
|
|
382
|
+
"twins": {
|
|
383
|
+
"VBtn": {
|
|
384
|
+
"rust": "crate::ui::v_btn",
|
|
385
|
+
"props": { "label": "string", "size": "int?", "block": "bool" },
|
|
386
|
+
"slots": ["default"]
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
A template then uses `<VBtn>` (or `<v-btn>`), imported from anywhere or registered globally, and the
|
|
393
|
+
generated parent calls `crate::ui::v_btn` with the props, the slots (each a `ferrovue::Slot`) and
|
|
394
|
+
`&ferrovue::Attrs`: the attributes it is given beyond its props and the scope ids its root takes.
|
|
395
|
+
`twins.rs` beside the components holds `VBtnProps`, `VBtnSlots` and the signature your function must
|
|
396
|
+
have. Prop types are those of helpers (`string`, `int?`, …); an absent `bool` is `false`, and a
|
|
397
|
+
bare `block` is `true`, as Vue casts them.
|
|
398
|
+
|
|
399
|
+
**Exactness is your responsibility.** ferrovue cannot check that `v_btn` writes the bytes Vue's
|
|
400
|
+
server renderer writes for `VBtn`: its guarantee rests on your code here, as it does for helpers. Prove it with fixtures: render the components that use the twin with Vue through
|
|
401
|
+
`ferrovue/testing`'s `fixtureApp` and `attachSsrRender`, and with Rust through the generated
|
|
402
|
+
`render_json`, and compare. Slot content reaches the twin as a component's render function sees it
|
|
403
|
+
(`<!--v-if-->` for an absent `v-if`, as Vue writes virtual nodes). The twin decides where it goes and
|
|
404
|
+
whether fragment markers surround it, as the component does. `examples/fullstack` has a
|
|
405
|
+
twin (`src/ui.rs`), its fixtures (`fixtures/`), the Vue half (`test/fixtures.test.ts`) and the Rust
|
|
406
|
+
half (`src/fixtures.rs`).
|
|
407
|
+
|
|
296
408
|
### Teleports
|
|
297
409
|
|
|
298
410
|
A component that renders a `<Teleport>` takes a `ferrovue::Teleports`. After rendering the page body,
|
|
@@ -304,7 +416,7 @@ page::render(&mut body, &props, &teleports);
|
|
|
304
416
|
let modals = teleports.get("#modals").unwrap_or_default(); // inside <div id="modals">
|
|
305
417
|
```
|
|
306
418
|
|
|
307
|
-
As Vue recommends, teleport to a dedicated element
|
|
419
|
+
As Vue recommends, teleport to a dedicated element such as `#modals`: the browser hydrates
|
|
308
420
|
a target from its first node, and `body` also holds the app.
|
|
309
421
|
|
|
310
422
|
### Scoped slots from Rust
|
|
@@ -352,6 +464,40 @@ loaded, after the server's markup is on screen. Link those stylesheets from the
|
|
|
352
464
|
server reading Vite's manifest takes the `css` of the entry's `dynamicImports` as well as its own
|
|
353
465
|
(`examples/fullstack/src/assets.rs`).
|
|
354
466
|
|
|
467
|
+
An application whose own navigation layer leaves the page can keep `<RouterLink>` for its `href`s
|
|
468
|
+
and active classes with `linkRouter(routes, { navigate })` from `ferrovue/link-router`: a router
|
|
469
|
+
over the routes file whose every navigation after the first goes to `navigate`. The routing guide
|
|
470
|
+
(`ferrovue::guide::routing`) explains each part of it.
|
|
471
|
+
|
|
472
|
+
### Hydrating a whole page
|
|
473
|
+
|
|
474
|
+
A page built from a layout whose slots hold components, each rendered from its own props, can be
|
|
475
|
+
hydrated as one app. `ferrovue::Page` takes each slot's parts as `Part`s, made from the same
|
|
476
|
+
`html()` values that write the markup, renders the layout, and writes what it rendered as a
|
|
477
|
+
`<script type="application/json">`; `mountPage` reads it back, loads the components it names and
|
|
478
|
+
hydrates the layout with each slot's parts:
|
|
479
|
+
|
|
480
|
+
```rust
|
|
481
|
+
let mut page = ferrovue::Page::new();
|
|
482
|
+
let content = page.slot("default", books.iter().map(|b| Part::new(pick::NAME, pick::html(b))));
|
|
483
|
+
let reviews = page.hole("reviews"); // filled later, while the page streams
|
|
484
|
+
let record = page.render_to(&mut out, picks::html(&props, picks::Slots {
|
|
485
|
+
default: Some(content.slot()),
|
|
486
|
+
reviews: Some(reviews.slot()),
|
|
487
|
+
}, &route));
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
```ts
|
|
491
|
+
import { mountPage } from "ferrovue";
|
|
492
|
+
import islands from "ferrovue/islands";
|
|
493
|
+
|
|
494
|
+
await mountPage(() => import("./Picks.vue"), islands, { pinia, router });
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
The record repeats the text of every prop, so a page hydrated this way is larger than one of
|
|
498
|
+
islands; the guide's `islands_and_hydration` page compares the two, and `examples/fullstack` serves
|
|
499
|
+
a page of each kind.
|
|
500
|
+
|
|
355
501
|
### Hydrating Pinia state
|
|
356
502
|
|
|
357
503
|
```rust
|
|
@@ -383,10 +529,10 @@ and both write the same bytes: each side checks its output against
|
|
|
383
529
|
|
|
384
530
|
| Scenario | What renders | Output | Vue `renderToString` | ferrovue | Speed-up |
|
|
385
531
|
|---|---|---|---|---|---|
|
|
386
|
-
| `small` | `Nav`: two `<RouterLink>`s resolved against the current route | 206 B |
|
|
387
|
-
| `list` | `Lists`: 1,000 words, 1,000 numbers, 100 groups of 10 members | 102 KiB |
|
|
388
|
-
| `tree` | `Tree`: a recursive component, binary tree 8 levels deep (255 nodes) | 9.5 KiB |
|
|
389
|
-
| `page` | `Dashboard`: 22 `Panel`s with named slots, a `Text`, 20 `Frame`s holding loops | 8.0 KiB |
|
|
532
|
+
| `small` | `Nav`: two `<RouterLink>`s resolved against the current route | 206 B | 41.39 µs | 0.962 µs | 43.0× |
|
|
533
|
+
| `list` | `Lists`: 1,000 words, 1,000 numbers, 100 groups of 10 members | 102 KiB | 363.9 µs | 49.00 µs | 7.4× |
|
|
534
|
+
| `tree` | `Tree`: a recursive component, binary tree 8 levels deep (255 nodes) | 9.5 KiB | 539.5 µs | 3.89 µs | 138.5× |
|
|
535
|
+
| `page` | `Dashboard`: 22 `Panel`s with named slots, a `Text`, 20 `Frame`s holding loops | 8.0 KiB | 192.4 µs | 4.84 µs | 39.7× |
|
|
390
536
|
|
|
391
537
|
These are mean times per render. The Vue column is tinybench's mean, with 5 s per scenario after
|
|
392
538
|
a 1 s warm-up. The ferrovue column is criterion's mean point estimate, using its defaults of a 3 s
|
|
@@ -424,10 +570,21 @@ still build, run and agree on the HTML. It never compares their timings.
|
|
|
424
570
|
|
|
425
571
|
## Versions
|
|
426
572
|
|
|
427
|
-
Byte-identical output depends on Vue's own SSR compiler, so
|
|
428
|
-
`@vue/compiler-sfc`
|
|
429
|
-
Vue
|
|
430
|
-
|
|
573
|
+
Byte-identical output depends on Vue's own SSR compiler, so ferrovue compiles with an exact
|
|
574
|
+
`@vue/compiler-sfc` (currently 3.5.43), and its conformance fixtures are recorded from one version
|
|
575
|
+
of Vue and of each integration. Your project may use any later patch of the same minor:
|
|
576
|
+
|
|
577
|
+
| Peer | Supported | Recorded from |
|
|
578
|
+
|---|---|---|
|
|
579
|
+
| `vue` | `~3.5.43` (3.5.43 and later 3.5 patches) | 3.5.43 |
|
|
580
|
+
| `vue-router` (optional) | `~5.3.1` | 5.3.1 |
|
|
581
|
+
| `pinia` (optional) | `~4.0.3` | 4.0.3 |
|
|
582
|
+
| `vue-i18n` (optional) | `~11.4.13` | 11.4.13 |
|
|
583
|
+
|
|
584
|
+
A new minor of any of them (Vue 3.6, vue-router 5.4, …) needs a ferrovue release that re-records
|
|
585
|
+
the fixtures from it. A weekly CI job re-records them from the newest patch each range allows and
|
|
586
|
+
hydrates the committed HTML with it, so a patch that changes what Vue writes or hydrates is caught
|
|
587
|
+
before it reaches you; a ferrovue patch follows when one does.
|
|
431
588
|
|
|
432
589
|
## Repository layout
|
|
433
590
|
|
|
@@ -447,10 +604,10 @@ crates/ferrovue-router/ vue-router's matching and links (the `router` featu
|
|
|
447
604
|
tests/vectors/ vectors recorded from vue-router
|
|
448
605
|
crates/ferrovue-i18n/ vue-i18n's t() (the `i18n` feature)
|
|
449
606
|
packages/ferrovue/ the compiler (npm package)
|
|
450
|
-
src/index.ts `ferrovue`: the browser API, `mountIslands`, `hydrateState` and the types
|
|
607
|
+
src/index.ts `ferrovue`: the browser API, `mountIslands`, `mountPage`, `hydrateState` and the types
|
|
451
608
|
src/compiler.ts `ferrovue/compiler`: `generate`, `write`
|
|
452
|
-
src/component.ts, script.ts, typescript.ts
|
|
453
|
-
a `.vue` file read, <script setup>, TypeScript types
|
|
609
|
+
src/component.ts, script.ts, typescript.ts, constants.ts
|
|
610
|
+
a `.vue` file read, <script setup>, TypeScript types, constants and enums
|
|
454
611
|
src/template.ts, children.ts, slots.ts, loops.ts
|
|
455
612
|
the compiled template: statements, child components, slots, v-for
|
|
456
613
|
src/expr.ts, strings.ts, numbers.ts, narrowing.ts, calls.ts, lists.ts
|
|
@@ -458,19 +615,26 @@ packages/ferrovue/ the compiler (npm package)
|
|
|
458
615
|
src/attrs.ts, classes.ts, styles.ts, fallthrough.ts
|
|
459
616
|
attributes, class and style, attributes a parent passes on
|
|
460
617
|
src/plugin.ts the plugin interface (see CONTRIBUTING.md)
|
|
461
|
-
src/plugins/ vue-router, Pinia, vue-i18n, scoped styles, <Teleport>, shared types
|
|
618
|
+
src/plugins/ vue-router, Pinia, vue-i18n, scoped styles, <Teleport>, shared types,
|
|
619
|
+
<ClientOnly>, Rust twins
|
|
462
620
|
src/rust.ts, emitter.ts the Rust source written out
|
|
463
621
|
src/cli.ts, vite.ts the `ferrovue` command and the Vite plugin
|
|
464
|
-
src/
|
|
622
|
+
src/errors.ts, diagnostics.ts
|
|
623
|
+
every error code with its title, and errors as the CLI, JSON and Vite show them
|
|
624
|
+
src/client.ts browser-side helpers: `mountIslands`, `mountPage`, `hydrateState`, `<ClientOnly>`
|
|
465
625
|
src/islands.ts `ferrovue/islands`, which the Vite plugin writes: every island, loaded lazily
|
|
626
|
+
src/link-router.ts `ferrovue/link-router`: `<RouterLink>` while the application navigates on its own
|
|
627
|
+
src/routes.ts a routes file as vue-router's route records
|
|
466
628
|
src/testing.ts utilities for a project's own conformance suite
|
|
629
|
+
src/hydration.ts `hydrateRecordedPage`, which `ferrovue/testing` exports
|
|
467
630
|
src/types.ts `ferrovue/types`: `TrustedHtml`, `Float`
|
|
468
631
|
test/ compiler, CLI, router, vector, island, Vite and conformance tests
|
|
469
|
-
browser/ conformance fixtures hydrated in real browsers (`pnpm test:browser`)
|
|
632
|
+
browser/ conformance fixtures and recorded pages hydrated in real browsers (`pnpm test:browser`)
|
|
470
633
|
bench/ Vue renderToString benchmarks, the other half of Performance
|
|
471
634
|
fuzz/ the randomised differential tester (`pnpm fuzz`)
|
|
472
635
|
examples/greeting/ the smallest setup: one component rendered from Rust
|
|
473
|
-
examples/fullstack/ axum + Vite: islands, Pinia state, routes and
|
|
636
|
+
examples/fullstack/ axum + Vite: islands, a page hydrated whole, Pinia state, routes and
|
|
637
|
+
streaming, <ClientOnly>, a Rust twin and its fixtures
|
|
474
638
|
examples/dioxus/ a Dioxus page, rendered with dioxus-ssr, with an island in it
|
|
475
639
|
scripts/release.ts the release version bump (see RELEASING.md)
|
|
476
640
|
```
|
package/dist/attrs.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type N, type Scope, type Val } from "./model.ts";
|
|
2
2
|
import { Emitter } from "./emitter.ts";
|
|
3
3
|
export declare function display(e: Emitter, v: Val): boolean;
|
|
4
|
-
export declare function interpolate(e: Emitter, v: Val): void;
|
|
4
|
+
export declare function interpolate(s: Scope, e: Emitter, v: Val, n: N): void;
|
|
5
5
|
export declare function attrValue(e: Emitter, v: Val): void;
|
|
6
6
|
export declare function renderAttr(s: Scope, e: Emitter, key: string, v: Val, n: N): void;
|
|
7
7
|
export declare function renderDynamicAttr(s: Scope, e: Emitter, key: string, v: Val, n: N): void;
|
package/dist/attrs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { escapeHtml, hyphenate, isBooleanAttr, isSSRSafeAttrName, propsToAttrMap } from "@vue/shared";
|
|
2
|
-
import { fail, GenError, rustStr } from "./model.js";
|
|
2
|
+
import { fail, nothing, GenError, rustStr } from "./model.js";
|
|
3
3
|
import { CONFIG_FILE, ctx } from "./context.js";
|
|
4
4
|
import { describeTy, expr } from "./expr.js";
|
|
5
5
|
import { known, truthy } from "./narrowing.js";
|
|
@@ -30,39 +30,40 @@ export function display(e, v) {
|
|
|
30
30
|
return false;
|
|
31
31
|
return true;
|
|
32
32
|
}
|
|
33
|
-
export function interpolate(e, v) {
|
|
33
|
+
export function interpolate(s, e, v, n) {
|
|
34
34
|
if (display(e, v))
|
|
35
35
|
return;
|
|
36
36
|
switch (v.ty.k) {
|
|
37
37
|
case "undef":
|
|
38
|
+
case "null":
|
|
38
39
|
return;
|
|
39
40
|
case "opt":
|
|
40
41
|
e.open(`if let Some(v) = ${v.code}`);
|
|
41
|
-
interpolate(e, { code: "v", ty: v.ty.of });
|
|
42
|
+
interpolate(s, e, { code: "v", ty: v.ty.of }, n);
|
|
42
43
|
e.close();
|
|
43
44
|
return;
|
|
44
45
|
default:
|
|
45
46
|
if (ctx.plugins.some((p) => p.values?.interpolate?.(e, v)))
|
|
46
47
|
return;
|
|
47
|
-
|
|
48
|
+
fail(s.comp, "FV0401", `\`{{ }}\` of ${describeTy(v.ty)}: only strings, numbers and booleans can be interpolated`, n);
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
export function attrValue(e, v) {
|
|
51
52
|
if (!display(e, v))
|
|
52
|
-
throw new GenError("an attribute value must be a string, a number or a boolean");
|
|
53
|
+
throw new GenError("FV0402", "an attribute value must be a string, a number or a boolean");
|
|
53
54
|
}
|
|
54
55
|
function renderable(s, key, v, n) {
|
|
55
56
|
const ty = v.ty.k === "opt" ? v.ty.of : v.ty;
|
|
56
|
-
if (ty.k === "str" || ty.k === "int" || ty.k === "float" || ty.k === "bool" || ty
|
|
57
|
+
if (ty.k === "str" || ty.k === "int" || ty.k === "float" || ty.k === "bool" || nothing(ty))
|
|
57
58
|
return;
|
|
58
59
|
const own = claim((p) => p.values?.unbindable?.(ty));
|
|
59
60
|
const what = own?.what ?? describeTy(ty);
|
|
60
61
|
const fix = own?.fix ?? (ty.k === "list" ? 'join it into one string, as `.join(",")`' : "bind a string, a number or a boolean");
|
|
61
|
-
fail(s.comp, `\`${key}\` is bound to ${what}: Vue's server renderer leaves the attribute out, and hydration then sets it, changing the page; ${fix}`, n);
|
|
62
|
+
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);
|
|
62
63
|
}
|
|
63
64
|
export function renderAttr(s, e, key, v, n) {
|
|
64
65
|
renderable(s, key, v, n);
|
|
65
|
-
if (v.ty
|
|
66
|
+
if (nothing(v.ty))
|
|
66
67
|
return;
|
|
67
68
|
if (v.ty.k === "opt") {
|
|
68
69
|
e.open(`if let Some(v) = ${v.code}`);
|
|
@@ -77,9 +78,9 @@ export function renderAttr(s, e, key, v, n) {
|
|
|
77
78
|
export function renderDynamicAttr(s, e, key, v, n) {
|
|
78
79
|
const name = propsToAttrMap[key] ?? key.toLowerCase();
|
|
79
80
|
if (!isSSRSafeAttrName(name))
|
|
80
|
-
fail(s.comp, `unsafe attribute name \`${name}\``, n);
|
|
81
|
+
fail(s.comp, "FV0404", `unsafe attribute name \`${name}\``, n);
|
|
81
82
|
renderable(s, key, v, n);
|
|
82
|
-
if (v.ty
|
|
83
|
+
if (nothing(v.ty))
|
|
83
84
|
return;
|
|
84
85
|
const boolean = (t) => isBooleanAttr(name) || (name === "hidden" && (t.k === "bool" || t.k === "int" || t.k === "float"));
|
|
85
86
|
if (v.ty.k === "opt" && boolean(v.ty.of)) {
|
|
@@ -140,7 +141,7 @@ export function directiveProps(s, n) {
|
|
|
140
141
|
else if (dir?.type === "Identifier")
|
|
141
142
|
name = s.directives.get(dir.name) ?? null;
|
|
142
143
|
if (name === null || !ctx.clientDirectives.has(name)) {
|
|
143
|
-
fail(s.comp, `custom directive \`v-${name ?? "?"}\` may add attributes on the server; if it has no \`getSSRProps\`, list \`${name ?? "?"}\` in \`clientDirectives\` in ${CONFIG_FILE}`, n);
|
|
144
|
+
fail(s.comp, "FV0405", `custom directive \`v-${name ?? "?"}\` may add attributes on the server; if it has no \`getSSRProps\`, list \`${name ?? "?"}\` in \`clientDirectives\` in ${CONFIG_FILE}`, n);
|
|
144
145
|
}
|
|
145
146
|
return true;
|
|
146
147
|
}
|
|
@@ -151,7 +152,7 @@ export function dollarAttrs(s, n) {
|
|
|
151
152
|
if (!isDollarAttrs(n, s.attrsBindings))
|
|
152
153
|
return false;
|
|
153
154
|
if (s.comp.idsInAttrs) {
|
|
154
|
-
fail(s.comp, `\`$attrs\` in ${s.comp.name}, the root of a component that may be handed scope ids: it would hold them as attributes too`, n);
|
|
155
|
+
fail(s.comp, "FV0406", `\`$attrs\` in ${s.comp.name}, the root of a component that may be handed scope ids: it would hold them as attributes too`, n);
|
|
155
156
|
}
|
|
156
157
|
return true;
|
|
157
158
|
}
|
|
@@ -192,7 +193,7 @@ export function renderAttrs(s, e, n) {
|
|
|
192
193
|
else if (p.type === "ObjectExpression")
|
|
193
194
|
sources.push(`&[${attrEntries(s, p, "own").join(", ")}]`);
|
|
194
195
|
else
|
|
195
|
-
fail(s.comp, "attributes must be an object literal", p);
|
|
196
|
+
fail(s.comp, "FV0407", "attributes must be an object literal", p);
|
|
196
197
|
}
|
|
197
198
|
const ids = idsAt >= 0 && s.attrs !== null ? s.attrs : '""';
|
|
198
199
|
e.stmt(`fv::attrs_into(out, &[${sources.join(", ")}], ${idsAt >= 0 ? idsAt : sources.length}, ${ids});`);
|
|
@@ -223,16 +224,16 @@ function staticAttrs(s, e, n) {
|
|
|
223
224
|
}
|
|
224
225
|
function objectAttrs(s, e, n, merged) {
|
|
225
226
|
if (n.type !== "ObjectExpression")
|
|
226
|
-
fail(s.comp, "attributes must be an object literal", n);
|
|
227
|
+
fail(s.comp, "FV0407", "attributes must be an object literal", n);
|
|
227
228
|
for (const p of n.properties) {
|
|
228
229
|
if (p.type !== "ObjectProperty" || p.computed)
|
|
229
|
-
fail(s.comp, "attribute objects hold plain keys", p);
|
|
230
|
+
fail(s.comp, "FV0408", "attribute objects hold plain keys", p);
|
|
230
231
|
const raw = p.key.type === "Identifier" ? p.key.name : p.key.value;
|
|
231
232
|
if (IGNORED_PROPS.has(raw) || /^on[^a-z]/.test(raw) || raw.startsWith("."))
|
|
232
233
|
continue;
|
|
233
234
|
const key = raw.startsWith("^") ? raw.slice(1) : raw;
|
|
234
235
|
if (key === "className") {
|
|
235
|
-
fail(s.comp, "`className` renders by its own rule in Vue; bind `class`", p);
|
|
236
|
+
fail(s.comp, "FV0409", "`className` renders by its own rule in Vue; bind `class`", p);
|
|
236
237
|
}
|
|
237
238
|
else if (key === "class") {
|
|
238
239
|
const present = merged ? classPresent(s, p.value) : "true";
|
|
@@ -267,16 +268,16 @@ export function mergeProps(s, n) {
|
|
|
267
268
|
if (directiveProps(s, a))
|
|
268
269
|
continue;
|
|
269
270
|
if (a.type !== "ObjectExpression")
|
|
270
|
-
fail(s.comp, "`_mergeProps` of object literals is supported", a);
|
|
271
|
+
fail(s.comp, "FV0410", "`_mergeProps` of object literals is supported", a);
|
|
271
272
|
for (const p of a.properties) {
|
|
272
273
|
if (p.type !== "ObjectProperty" || p.computed)
|
|
273
|
-
fail(s.comp, "attribute objects hold plain keys", p);
|
|
274
|
+
fail(s.comp, "FV0408", "attribute objects hold plain keys", p);
|
|
274
275
|
const key = p.key.type === "Identifier" ? p.key.name : p.key.value;
|
|
275
276
|
const had = merged.get(key);
|
|
276
277
|
if (had && key === "class" && maybeEqual(s, p.value)) {
|
|
277
278
|
const before = literalClass(had.value);
|
|
278
279
|
if (before === null || p.value.type !== "StringLiteral") {
|
|
279
|
-
fail(s.comp, "a class merged with a string that may equal the class before it, which Vue then writes once: bind an array", p);
|
|
280
|
+
fail(s.comp, "FV0411", "a class merged with a string that may equal the class before it, which Vue then writes once: bind an array", p);
|
|
280
281
|
}
|
|
281
282
|
if (before !== p.value.value)
|
|
282
283
|
merged.set(key, { ...p, value: { type: "ArrayExpression", elements: [had.value, p.value], fvMerged: true } });
|
|
@@ -297,7 +298,7 @@ export function attrEntries(s, obj, side) {
|
|
|
297
298
|
const out = [];
|
|
298
299
|
for (const p of obj.properties) {
|
|
299
300
|
if (p.type !== "ObjectProperty" || p.computed)
|
|
300
|
-
fail(s.comp, "attribute objects hold plain keys", p);
|
|
301
|
+
fail(s.comp, "FV0408", "attribute objects hold plain keys", p);
|
|
301
302
|
const key = p.key.type === "Identifier" ? p.key.name : String(p.key.value);
|
|
302
303
|
if (IGNORED_PROPS.has(key) || /^on[^a-z]/.test(key) || key.startsWith("."))
|
|
303
304
|
continue;
|
|
@@ -307,7 +308,7 @@ export function attrEntries(s, obj, side) {
|
|
|
307
308
|
}
|
|
308
309
|
export function attrOf(s, key, n, side) {
|
|
309
310
|
if (key === "className")
|
|
310
|
-
fail(s.comp, "`className` renders by its own rule in Vue; bind `class`", n);
|
|
311
|
+
fail(s.comp, "FV0409", "`className` renders by its own rule in Vue; bind `class`", n);
|
|
311
312
|
if (key === "class")
|
|
312
313
|
return classAttr(s, n, side);
|
|
313
314
|
if (key === "style")
|
|
@@ -327,6 +328,7 @@ export function valueAttr(s, v, n) {
|
|
|
327
328
|
case "bool":
|
|
328
329
|
return `fv::Attr::Bool(${bare(v.code)})`;
|
|
329
330
|
case "undef":
|
|
331
|
+
case "null":
|
|
330
332
|
return "fv::Attr::Undefined";
|
|
331
333
|
case "opt": {
|
|
332
334
|
const of = v.ty.of.k;
|
|
@@ -335,5 +337,5 @@ export function valueAttr(s, v, n) {
|
|
|
335
337
|
return `${atom(v.code)}.map_or(fv::Attr::Undefined, ${make})`;
|
|
336
338
|
}
|
|
337
339
|
}
|
|
338
|
-
return fail(s.comp, "an attribute that may fall through is a string, a number or a boolean", n);
|
|
340
|
+
return fail(s.comp, "FV0412", "an attribute that may fall through is a string, a number or a boolean", n);
|
|
339
341
|
}
|