ferrovue 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -13
- package/dist/args.d.ts +28 -0
- package/dist/args.js +69 -0
- package/dist/attrs.js +3 -0
- package/dist/calls.js +5 -2
- package/dist/children.js +18 -3
- package/dist/cli.js +80 -81
- package/dist/client-render.d.ts +27 -0
- package/dist/client-render.js +171 -0
- package/dist/client.d.ts +65 -6
- package/dist/client.js +269 -27
- package/dist/compiler.d.ts +9 -5
- package/dist/compiler.js +48 -14
- package/dist/component.d.ts +7 -3
- package/dist/component.js +37 -25
- package/dist/conformance.d.ts +24 -3
- package/dist/conformance.js +58 -7
- package/dist/constants.js +4 -2
- package/dist/context.d.ts +51 -4
- package/dist/context.js +173 -5
- package/dist/diagnostics.d.ts +6 -4
- package/dist/diagnostics.js +7 -3
- package/dist/editor.d.ts +18 -0
- package/dist/editor.js +59 -0
- package/dist/errors.d.ts +45 -3
- package/dist/errors.js +33 -3
- package/dist/expr.d.ts +2 -0
- package/dist/expr.js +91 -12
- package/dist/fallthrough.d.ts +1 -0
- package/dist/fallthrough.js +10 -2
- package/dist/files.d.ts +13 -0
- package/dist/files.js +49 -0
- package/dist/fixture.d.ts +3 -3
- package/dist/hydration.d.ts +12 -1
- package/dist/hydration.js +47 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/inputs.d.ts +24 -0
- package/dist/inputs.js +40 -0
- package/dist/link-router.d.ts +6 -2
- package/dist/link-router.js +7 -4
- package/dist/lists.d.ts +12 -1
- package/dist/lists.js +76 -2
- package/dist/model.d.ts +9 -1
- package/dist/model.js +2 -0
- package/dist/page-routes.d.ts +1 -0
- package/dist/paths.d.ts +5 -0
- package/dist/paths.js +7 -0
- package/dist/plugin.d.ts +2 -0
- package/dist/plugins/css-modules.d.ts +39 -0
- package/dist/plugins/css-modules.js +135 -0
- package/dist/plugins/head.js +4 -4
- package/dist/plugins/i18n.js +5 -12
- package/dist/plugins/index.js +2 -1
- package/dist/plugins/provide.js +4 -2
- package/dist/plugins/router.js +3 -2
- package/dist/plugins/scoped.d.ts +10 -0
- package/dist/plugins/scoped.js +17 -4
- package/dist/plugins/shared-types.js +2 -1
- package/dist/plugins/stores.js +7 -6
- package/dist/rust.d.ts +9 -1
- package/dist/rust.js +38 -15
- package/dist/schema.d.ts +25 -0
- package/dist/schema.js +73 -0
- package/dist/script.js +12 -5
- package/dist/slots.js +2 -0
- package/dist/ssr.js +42 -4
- package/dist/styles.d.ts +5 -1
- package/dist/styles.js +131 -4
- package/dist/testing.d.ts +2 -1
- package/dist/testing.js +1 -1
- package/dist/typescript.d.ts +11 -1
- package/dist/typescript.js +124 -33
- package/dist/vite.d.ts +18 -10
- package/dist/vite.js +106 -51
- package/dist/volar.d.ts +15 -0
- package/dist/volar.js +87 -0
- package/package.json +27 -7
- package/schema.json +333 -0
package/README.md
CHANGED
|
@@ -38,8 +38,8 @@ cargo add ferrovue serde --features serde/derive
|
|
|
38
38
|
cargo add --dev serde_json # the generated conformance helper uses it under cfg(test)
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
ferrovue
|
|
42
|
-
crate that includes it needs **edition 2024**.
|
|
41
|
+
ferrovue needs Rust **1.89** or later (CI tests that and the latest stable). Generated code uses
|
|
42
|
+
let-chains, so the crate that includes it needs **edition 2024**.
|
|
43
43
|
|
|
44
44
|
### 2. Configure
|
|
45
45
|
|
|
@@ -47,15 +47,22 @@ crate that includes it needs **edition 2024**.
|
|
|
47
47
|
|
|
48
48
|
```json
|
|
49
49
|
{
|
|
50
|
+
"$schema": "https://cdn.jsdelivr.net/npm/ferrovue@0.7.0/schema.json",
|
|
50
51
|
"components": "client/components",
|
|
51
52
|
"out": "src/generated"
|
|
52
53
|
}
|
|
53
54
|
```
|
|
54
55
|
|
|
56
|
+
`$schema` points an editor at the configuration's JSON Schema, published with each version of the
|
|
57
|
+
npm package, so VS Code and other JSON editors complete the keys, describe them on hover and flag a
|
|
58
|
+
misspelt one. Name the version of `ferrovue` you use; `ferrovue init` writes it. ferrovue does not
|
|
59
|
+
read `$schema`. Offline, the copy in the installed package works as well:
|
|
60
|
+
`"$schema": "./node_modules/ferrovue/schema.json"`.
|
|
61
|
+
|
|
55
62
|
| Key | Required | Meaning |
|
|
56
63
|
|---|---|---|
|
|
57
64
|
| `components` | yes | Directory of `.vue` files to compile |
|
|
58
|
-
| `out` | yes | Directory the Rust modules are written to. **
|
|
65
|
+
| `out` | yes | Directory the Rust modules are written to. **Every module ferrovue wrote there is replaced**, and one no component produces any more is removed; a `.rs` file without ferrovue's `// @generated` header is left alone, and `--check` reports it |
|
|
59
66
|
| `routes` | no | JSON file listing the app's routes: each a vue-router path, or `{ "path", "name", "children" }`. Or `{ "pages": "client/pages" }`: a folder of pages, whose file names give the routes as vue-router's file-based routing reads them (`index.vue`, `[id].vue`, `[[id]].vue`, `[...path].vue`, `(group)` folders, a `name.vue` beside `name/` as its layout), each page compiled as a component. Needed for `<RouterLink>`, `<RouterView>` and `useRoute()` |
|
|
60
67
|
| `router` | no | Instead of `routes`: `{ routes, base?, linkActiveClass?, linkExactActiveClass? }`, matching `createWebHistory(base)` and `createRouter`'s options |
|
|
61
68
|
| `stores` | no | Directory of Pinia option stores whose state components may read |
|
|
@@ -66,6 +73,8 @@ crate that includes it needs **edition 2024**.
|
|
|
66
73
|
| `clientDirectives` | no | Custom directives with no server output (no `getSSRProps`), by name without `v-`: `["focus"]` |
|
|
67
74
|
| `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) |
|
|
68
75
|
| `viteRoot` | no | Vite's root, relative to this file's directory, from which a component's path is hashed (default `.`) |
|
|
76
|
+
| `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](#scoped-styles) |
|
|
77
|
+
| `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](#scoped-styles) |
|
|
69
78
|
| `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`) |
|
|
70
79
|
|
|
71
80
|
### 3. Write a component
|
|
@@ -92,11 +101,28 @@ pnpm ferrovue --check --format json # errors as JSON, for an editor or CI annot
|
|
|
92
101
|
pnpm ferrovue --watch # regenerates when components or configs change
|
|
93
102
|
```
|
|
94
103
|
|
|
104
|
+
`-c, --config <path>` reads another configuration file. `--watch` regenerates on a change to what a
|
|
105
|
+
run reads (the configuration, the components, stores, locale messages, pages or routes file, and
|
|
106
|
+
the `.ts` files components import types from), not on any other file of the project. A Vite project
|
|
107
|
+
can use the plugin instead, which does the same in its dev server and before a build:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// vite.config.ts
|
|
111
|
+
import ferrovue from "ferrovue/vite";
|
|
112
|
+
|
|
113
|
+
export default { plugins: [ferrovue({ config: "config/ferrovue.json" })] };
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`root` is the project root the configuration's paths are relative to (Vite's working directory by
|
|
117
|
+
default), and `config` the configuration file in it, as `--config` takes it (`ferrovue.config.json`
|
|
118
|
+
by default).
|
|
119
|
+
|
|
95
120
|
Every error carries a stable code, as `error[FV0602]: components/Card.vue:4:17: …`, documented in
|
|
96
121
|
the crate's [`error_codes`](https://docs.rs/ferrovue/latest/ferrovue/guide/error_codes/index.html)
|
|
97
122
|
guide. `--format json` writes the same errors as JSON with the exit status unchanged, and a VS Code
|
|
98
123
|
problem matcher for `tasks.json` puts them in the Problems panel: both are in the
|
|
99
124
|
[quick start](https://docs.rs/ferrovue/latest/ferrovue/guide/quick_start/index.html#editor-and-ci-diagnostics).
|
|
125
|
+
In the editor, the `ferrovue/volar` plugin shows them as you type (step 5).
|
|
100
126
|
|
|
101
127
|
```rust
|
|
102
128
|
#[rustfmt::skip]
|
|
@@ -129,7 +155,7 @@ route.
|
|
|
129
155
|
|
|
130
156
|
The router and vue-i18n are features of the crate, `router` and `i18n`, both on by default. An
|
|
131
157
|
application with neither routes nor translations can leave them out, and build neither
|
|
132
|
-
`ferrovue-router` nor `ferrovue-i18n`: `ferrovue = { version = "0.
|
|
158
|
+
`ferrovue-router` nor `ferrovue-i18n`: `ferrovue = { version = "0.7", default-features = false }`.
|
|
133
159
|
|
|
134
160
|
With the `maud` feature, `ferrovue::Html` implements `maud::Render`, so `(greeting::html(&props))` can go
|
|
135
161
|
straight into a `maud::html!` page.
|
|
@@ -160,6 +186,23 @@ With the `dioxus` feature, a component goes into a Dioxus 0.7 page (`dioxus-ssr`
|
|
|
160
186
|
hydrates inside it. See `examples/dioxus` and the crate guide's
|
|
161
187
|
[Dioxus page](https://github.com/Kinsmir/ferrovue/blob/main/crates/ferrovue/docs/guide/dioxus.md).
|
|
162
188
|
|
|
189
|
+
### 5. Diagnostics in your editor
|
|
190
|
+
|
|
191
|
+
`ferrovue/volar` is a plugin for Vue's language tools, which the Vue (Official) extension for VS Code
|
|
192
|
+
and Vue support in other editors run. It compiles the project as you type and underlines what
|
|
193
|
+
ferrovue refuses in the open component, with the error's code linked to its documentation. Name it
|
|
194
|
+
in the `tsconfig.json` that includes your components, then run **Vue: Restart Vue and TS servers**:
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{
|
|
198
|
+
"include": ["components/**/*.vue", "components/**/*.ts"],
|
|
199
|
+
"vueCompilerOptions": { "plugins": ["ferrovue/volar"] }
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The [quick start](https://docs.rs/ferrovue/latest/ferrovue/guide/quick_start/index.html#diagnostics-in-your-editor)
|
|
204
|
+
has the details; `examples/fullstack/tsconfig.json` enables it.
|
|
205
|
+
|
|
163
206
|
## What a component may use
|
|
164
207
|
|
|
165
208
|
ferrovue compiles `<script setup lang="ts">` components, and components with no script at all
|
|
@@ -177,13 +220,15 @@ ferrovue compiles `<script setup lang="ts">` components, and components with no
|
|
|
177
220
|
| 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 |
|
|
178
221
|
| 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)) |
|
|
179
222
|
| 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` |
|
|
180
|
-
| 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 |
|
|
223
|
+
| 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, 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()`; chained and nested; a computed list read by `v-for`, `.join()`, `.length`, `.includes()`, kept by `computed`, or handed to a child |
|
|
181
224
|
| 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 |
|
|
182
225
|
| Attributes | static and bound attributes, boolean attributes, `:hidden`, `data-*` and `aria-*`, `v-bind` objects |
|
|
183
226
|
| `class` | strings, arrays, objects (`{ active: on }`, computed keys), `cond && "x"`, `cond ? "x" : null`, merged with a static `class` |
|
|
184
227
|
| `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 |
|
|
185
228
|
| 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 |
|
|
186
|
-
|
|
|
229
|
+
| `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`) |
|
|
230
|
+
| 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 |
|
|
231
|
+
| 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`; `defineAsyncComponent(() => import("./X.vue"))` (or `{ loader: … }`), rendered as the component it loads, which Vue's server renderer waits for |
|
|
187
232
|
| `<component :is>` | over a closed set of choices, compiled to a `match`: an imported component (`:is="Card"`), an HTML element's name (`is="h2"`), a `computed` or `?:` choosing among them, a prop typed as a union of string literals (`as: "h1" \| "h2"`), and an object of imported components (or element names) declared in setup or imported from a `.ts` file, read by such a prop (`ICONS[name]`); props, fallthrough attributes, slots and scope ids reach each choice as they reach a static child; also inside `<KeepAlive>` and `<Transition>`. See the crate's [`generated_code`](https://docs.rs/ferrovue/latest/ferrovue/guide/generated_code/index.html#component-is) guide |
|
|
188
233
|
| 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) |
|
|
189
234
|
| 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 |
|
|
@@ -198,7 +243,9 @@ ferrovue compiles `<script setup lang="ts">` components, and components with no
|
|
|
198
243
|
|
|
199
244
|
Refused at compile time, each with an error that names the construct:
|
|
200
245
|
|
|
201
|
-
- `<style module
|
|
246
|
+
- `<style module>` without `cssModules` in the configuration; a CSS module in another file, one a
|
|
247
|
+
preprocessor compiles, or one that imports from another file; `v-bind()` in `<style>` of a value
|
|
248
|
+
that is not a string or a number
|
|
202
249
|
- `<RouterView>` in a component with `<style scoped>`, which would give the page that component's
|
|
203
250
|
id; and, since vue-router renders a link from virtual nodes, a `<slot>` inside a `<RouterLink>`
|
|
204
251
|
that takes scope ids, or an element inside one in slot content given a `:slotted()` id
|
|
@@ -233,8 +280,9 @@ Refused at compile time, each with an error that names the construct:
|
|
|
233
280
|
starting position or a split's limit (`.includes(x, 3)`, `.split(",", 2)`), `.toLocaleUpperCase()`
|
|
234
281
|
and `.toLocaleLowerCase()` (the server's locale is not the browser's), `parseInt` with another radix
|
|
235
282
|
or of a number, `.repeat()` by a negative literal
|
|
236
|
-
- array methods given anything but an arrow function whose body is an expression
|
|
237
|
-
`x => { return … }`), `.map()` to optional values,
|
|
283
|
+
- array methods given anything but `Boolean` or an arrow function whose body is an expression
|
|
284
|
+
(`.map(String)`, `x => { return … }`), `.map()` to optional values, methods other than
|
|
285
|
+
`.filter(Boolean)` over a list of optional values, a computed list as a slot prop
|
|
238
286
|
- a dictionary's field read by name (`r.key`, `r[key]`), which may be absent although TypeScript says
|
|
239
287
|
it is not; `Object.entries()` anywhere but as a `v-for`'s source; dictionaries of optional values
|
|
240
288
|
- two strings that may each hold half of a surrogate pair compared, searched or joined (see
|
|
@@ -310,6 +358,15 @@ Set `viteRoot` when Vite's root is not the directory holding `ferrovue.config.js
|
|
|
310
358
|
plugin (`ferrovue/vite`) compares the two when a component has scoped styles: a build in which they
|
|
311
359
|
differ fails, and the dev server warns.
|
|
312
360
|
|
|
361
|
+
`v-bind()` in `<style>` sets CSS variables on the component's root, which Vue's server writes into
|
|
362
|
+
its `style` and ferrovue writes there too. Their names follow the same id, and whether plugin-vue
|
|
363
|
+
compiles for production (`vite build`) or development (the dev server): set `isProduction` to match,
|
|
364
|
+
`true` by default. CSS modules (`<style module>`) take their class names from Vite's
|
|
365
|
+
`css.modules.generateScopedName`: set it to a pattern, and `cssModules` in `ferrovue.config.json` to
|
|
366
|
+
the same, and ferrovue computes each class as postcss-modules does in Vite. The Vite plugin compares
|
|
367
|
+
both, and fails a build in which they differ. See the crate's
|
|
368
|
+
[`scoped_styles`](https://docs.rs/ferrovue/latest/ferrovue/guide/scoped_styles/index.html) guide.
|
|
369
|
+
|
|
313
370
|
The default is the plugin's production behaviour because the production build is the one readers
|
|
314
371
|
get: with every option left alone, its styles apply. But plugin-vue hashes the path alone in its dev
|
|
315
372
|
server, so with the default a page rendered during development carries other ids than the dev
|
|
@@ -565,6 +622,24 @@ The record repeats the text of every prop, so a page hydrated this way is larger
|
|
|
565
622
|
islands; the guide's `islands_and_hydration` page compares the two, and `examples/fullstack` serves
|
|
566
623
|
a page of each kind.
|
|
567
624
|
|
|
625
|
+
An application that goes to the next page without a reload shows it from the record in the fetched
|
|
626
|
+
document, with the same app:
|
|
627
|
+
|
|
628
|
+
```ts
|
|
629
|
+
import { hydrateState, readPage, renderPage } from "ferrovue";
|
|
630
|
+
|
|
631
|
+
const next = readPage(await (await fetch(href)).text()); // no DOMParser: works in a worker too
|
|
632
|
+
const pinia = createPinia();
|
|
633
|
+
hydrateState(pinia, { text: next.state });
|
|
634
|
+
const router = linkRouter(routes, { navigate, location: href }); // at the next page's location
|
|
635
|
+
current = await renderPage(() => import("./Picks.vue"), islands, next.record, { pinia, router, previous: current });
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
`renderPage` mounts the next page's app in place of the container before it unmounts `previous`, so
|
|
639
|
+
a router both use stays started. `renderRecordedPage` from `ferrovue/testing` checks that a page
|
|
640
|
+
rendered from its record on the client shows what the server wrote, and `conformanceSuite` checks
|
|
641
|
+
the same of every fixture.
|
|
642
|
+
|
|
568
643
|
### Hydrating Pinia state
|
|
569
644
|
|
|
570
645
|
```rust
|
|
@@ -719,6 +794,15 @@ the fixtures from it. A weekly CI job re-records them from the newest patch each
|
|
|
719
794
|
hydrates the committed HTML with it, so a patch that changes what Vue writes or hydrates is caught
|
|
720
795
|
before it reaches you; a ferrovue patch follows when one does.
|
|
721
796
|
|
|
797
|
+
### Deprecations
|
|
798
|
+
|
|
799
|
+
Something ferrovue stops supporting is deprecated first, for at least one minor release, and
|
|
800
|
+
removed in the next major release. A deprecated item of the crates carries `#[deprecated]`, so the
|
|
801
|
+
Rust compiler warns where it is used. A deprecated configuration key warns with
|
|
802
|
+
`warning[FV1117]` and a deprecated command-line option with `warning[FV1118]`, each naming what
|
|
803
|
+
replaces it, and the JSON Schema marks the key deprecated for editors. Each release's CHANGELOG lists
|
|
804
|
+
what it deprecates under "Deprecated".
|
|
805
|
+
|
|
722
806
|
## Repository layout
|
|
723
807
|
|
|
724
808
|
```text
|
|
@@ -741,10 +825,13 @@ crates/ferrovue-core/ escaping and JavaScript's numbers, which the other
|
|
|
741
825
|
crates/ferrovue-router/ vue-router's matching and links (the `router` feature)
|
|
742
826
|
tests/vectors/ vectors recorded from vue-router
|
|
743
827
|
crates/ferrovue-i18n/ vue-i18n's t() (the `i18n` feature)
|
|
828
|
+
crates/ferrovue-contract/ unpublished: Rust using every item of the generated-code contract,
|
|
829
|
+
and the version check's message (trybuild)
|
|
744
830
|
packages/ferrovue/ the compiler (npm package)
|
|
745
|
-
src/index.ts `ferrovue`: the browser API, `mountIslands`, `mountPage`, `hydrateState` and the types
|
|
831
|
+
src/index.ts `ferrovue`: the browser API, `mountIslands`, `mountPage`, `readPage`, `renderPage`, `hydrateState` and the types
|
|
746
832
|
src/compiler.ts `ferrovue/compiler`: `generate`, `write`
|
|
747
833
|
src/context.ts, model.ts `ferrovue.config.json`, and the types and values the compiler passes around
|
|
834
|
+
src/schema.ts the configuration's keys, types and defaults, from which `schema.json` is written
|
|
748
835
|
src/component.ts, script.ts, typescript.ts, constants.ts
|
|
749
836
|
a `.vue` file read, <script setup>, TypeScript types, constants and enums
|
|
750
837
|
src/template.ts, children.ts, slots.ts, loops.ts, vhtml.ts
|
|
@@ -760,11 +847,13 @@ packages/ferrovue/ the compiler (npm package)
|
|
|
760
847
|
src/plugins/ vue-router, Pinia, vue-i18n, scoped styles, <Teleport>, shared types,
|
|
761
848
|
<ClientOnly>, Rust twins, provide and inject, the page head
|
|
762
849
|
src/rust.ts, emitter.ts the Rust source written out
|
|
763
|
-
src/cli.ts, vite.ts
|
|
850
|
+
src/cli.ts, args.ts, vite.ts
|
|
851
|
+
the `ferrovue` command, its options, and the Vite plugin
|
|
764
852
|
src/diff.ts the diff `ferrovue --check --diff` prints for a stale file
|
|
765
853
|
src/errors.ts, diagnostics.ts
|
|
766
854
|
every error code with its title, and errors as the CLI, JSON and Vite show them
|
|
767
|
-
src/
|
|
855
|
+
src/volar.ts, editor.ts `ferrovue/volar`: errors in the editor, from a plugin for Vue's language tools
|
|
856
|
+
src/client.ts browser-side helpers: `mountIslands`, `mountPage`, `readPage`, `renderPage`, `hydrateState`, `<ClientOnly>`
|
|
768
857
|
src/islands.ts `ferrovue/islands`, which the Vite plugin writes: every island, loaded lazily
|
|
769
858
|
src/link-router.ts `ferrovue/link-router`: `<RouterLink>` while the application navigates on its own
|
|
770
859
|
src/routes.ts a routes file as vue-router's route records
|
|
@@ -774,9 +863,12 @@ packages/ferrovue/ the compiler (npm package)
|
|
|
774
863
|
src/fixture.ts, settle.ts `fixtureApp` and `readFixture`, and waiting for async components to settle
|
|
775
864
|
src/conformance.ts `conformanceSuite`, which `ferrovue/testing` exports
|
|
776
865
|
src/ssr.ts `attachSsrRender`, which `ferrovue/testing` exports
|
|
777
|
-
src/hydration.ts `hydrateRecordedPage`, which `ferrovue/testing` exports
|
|
866
|
+
src/hydration.ts `hydrateRecordedPage` and `renderRecordedPage`, which `ferrovue/testing` exports
|
|
867
|
+
src/client-render.ts the DOM of a client render as it is compared with the server's markup
|
|
778
868
|
src/types.ts `ferrovue/types`: `TrustedHtml`, `InlineHtml`, `Float`
|
|
779
869
|
test/ compiler, CLI, router, vector, island, Vite and conformance tests
|
|
870
|
+
api/ the package's API report, an `.api.md` per export and `cli.txt` (`pnpm api:report`)
|
|
871
|
+
schema.json the JSON Schema of `ferrovue.config.json` (`pnpm schema:generate`)
|
|
780
872
|
browser/ conformance fixtures and recorded pages hydrated in real browsers (`pnpm test:browser`)
|
|
781
873
|
bench/ Vue renderToString benchmarks, the other half of Performance
|
|
782
874
|
fuzz/ the randomised differential tester (`pnpm fuzz`)
|
|
@@ -788,6 +880,7 @@ examples/dioxus/ a Dioxus page, rendered with dioxus-ssr, with an is
|
|
|
788
880
|
scripts/ferrovue-in.ts the `ferrovue` command run in a project of this repository (`pnpm conformance:check`, CI)
|
|
789
881
|
scripts/inspect.ts Vue's SSR code and render of a component beside the Rust ferrovue generates for it
|
|
790
882
|
scripts/release.ts the release version bump (see RELEASING.md)
|
|
883
|
+
scripts/api-report.ts the npm package's API report, written and checked (`pnpm api:report`, `pnpm api:check`)
|
|
791
884
|
```
|
|
792
885
|
|
|
793
886
|
## Development
|
package/dist/args.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { GenError } from "./model.ts";
|
|
2
|
+
import type { Deprecation } from "./schema.ts";
|
|
3
|
+
/** A command-line option: its names, short one first, the value it takes, and its line in `--help`. */
|
|
4
|
+
export interface Flag {
|
|
5
|
+
names: readonly string[];
|
|
6
|
+
value?: string;
|
|
7
|
+
help: string;
|
|
8
|
+
deprecated?: Deprecation;
|
|
9
|
+
}
|
|
10
|
+
/** The commands `ferrovue` takes, with their lines in `--help`. */
|
|
11
|
+
export declare const COMMANDS: Readonly<Record<string, string>>;
|
|
12
|
+
/** The options `ferrovue` takes. */
|
|
13
|
+
export declare const FLAGS: readonly Flag[];
|
|
14
|
+
/** What the command line asks for: the command, and each option given by its long name, with its
|
|
15
|
+
* value or `true`. */
|
|
16
|
+
export interface Args {
|
|
17
|
+
command: string | null;
|
|
18
|
+
options: Map<string, string | true>;
|
|
19
|
+
/** The name each option was given by, as an error about it quotes it. */
|
|
20
|
+
spelt: Map<string, string>;
|
|
21
|
+
warnings: GenError[];
|
|
22
|
+
}
|
|
23
|
+
/** `ferrovue --help`. */
|
|
24
|
+
export declare function helpText(flags?: readonly Flag[], commands?: Readonly<Record<string, string>>): string;
|
|
25
|
+
/** Read the command line, or say what is wrong with it. A deprecated option gives a warning. */
|
|
26
|
+
export declare function parseArgs(argv: readonly string[], flags?: readonly Flag[], commands?: Readonly<Record<string, string>>): Args | {
|
|
27
|
+
error: string;
|
|
28
|
+
};
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { GenError } from "./model.js";
|
|
2
|
+
/** The commands `ferrovue` takes, with their lines in `--help`. */
|
|
3
|
+
export const COMMANDS = {
|
|
4
|
+
init: "scaffold starter ferrovue.config.json and components",
|
|
5
|
+
};
|
|
6
|
+
/** The options `ferrovue` takes. */
|
|
7
|
+
export const FLAGS = [
|
|
8
|
+
{ names: ["--check"], help: "check that generated files match committed files without writing" },
|
|
9
|
+
{ names: ["-d", "--diff"], help: "show unified diff of changes when checking (use with --check)" },
|
|
10
|
+
{ names: ["--format"], value: "<format>", help: "how to write errors and results: human (default) or json" },
|
|
11
|
+
{ names: ["--watch"], help: "watch components, stores, routes and config for changes and regenerate" },
|
|
12
|
+
{ names: ["-c", "--config"], value: "<path>", help: "path to configuration file (default: ferrovue.config.json)" },
|
|
13
|
+
{ names: ["-v", "--version"], help: "print the version and exit" },
|
|
14
|
+
{ names: ["-h", "--help"], help: "print this help and exit" },
|
|
15
|
+
];
|
|
16
|
+
const longName = (flag) => flag.names.at(-1);
|
|
17
|
+
/** `ferrovue --help`. */
|
|
18
|
+
export function helpText(flags = FLAGS, commands = COMMANDS) {
|
|
19
|
+
const line = (left, help) => ` ${left.padEnd(23)} ${help}`;
|
|
20
|
+
const option = (flag) => {
|
|
21
|
+
const [short, long] = flag.names.length > 1 ? [`${flag.names[0]}, `, longName(flag)] : [" ", longName(flag)];
|
|
22
|
+
const help = flag.deprecated ? `${flag.help} (deprecated: use ${flag.deprecated.use})` : flag.help;
|
|
23
|
+
return line(`${short}${long}${flag.value ? ` ${flag.value}` : ""}`, help);
|
|
24
|
+
};
|
|
25
|
+
return `Usage: ferrovue [command] [options]
|
|
26
|
+
|
|
27
|
+
Compile Vue components to Rust render functions, run from the project root.
|
|
28
|
+
|
|
29
|
+
Commands:
|
|
30
|
+
${Object.entries(commands).map(([name, help]) => line(name, help)).join("\n")}
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
${flags.map(option).join("\n")}`;
|
|
34
|
+
}
|
|
35
|
+
/** Read the command line, or say what is wrong with it. A deprecated option gives a warning. */
|
|
36
|
+
export function parseArgs(argv, flags = FLAGS, commands = COMMANDS) {
|
|
37
|
+
const args = { command: null, options: new Map(), spelt: new Map(), warnings: [] };
|
|
38
|
+
for (let i = 0; i < argv.length; i++) {
|
|
39
|
+
const arg = argv[i];
|
|
40
|
+
const eq = arg.startsWith("--") ? arg.indexOf("=") : -1;
|
|
41
|
+
const name = eq === -1 ? arg : arg.slice(0, eq);
|
|
42
|
+
const flag = flags.find((f) => f.names.includes(name));
|
|
43
|
+
if (!flag) {
|
|
44
|
+
if (arg in commands && args.command === null) {
|
|
45
|
+
args.command = arg;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
return { error: `unknown option or command '${arg}'\n\nRun \`ferrovue --help\` for usage.` };
|
|
49
|
+
}
|
|
50
|
+
let value = true;
|
|
51
|
+
if (flag.value) {
|
|
52
|
+
if (eq !== -1)
|
|
53
|
+
value = arg.slice(eq + 1);
|
|
54
|
+
else if (i + 1 < argv.length && !argv[i + 1].startsWith("-"))
|
|
55
|
+
value = argv[++i];
|
|
56
|
+
else
|
|
57
|
+
return { error: `option '${name}' requires an argument` };
|
|
58
|
+
}
|
|
59
|
+
else if (eq !== -1) {
|
|
60
|
+
return { error: `option '${name}' takes no value` };
|
|
61
|
+
}
|
|
62
|
+
if (flag.deprecated) {
|
|
63
|
+
args.warnings.push(new GenError("FV1118", `'${name}' is deprecated since ${flag.deprecated.since}, and goes in the next major release: use ${flag.deprecated.use}`));
|
|
64
|
+
}
|
|
65
|
+
args.options.set(longName(flag), value);
|
|
66
|
+
args.spelt.set(longName(flag), name);
|
|
67
|
+
}
|
|
68
|
+
return args;
|
|
69
|
+
}
|
package/dist/attrs.js
CHANGED
|
@@ -338,7 +338,10 @@ export function valueAttr(s, v, n) {
|
|
|
338
338
|
const make = of === "str" ? "fv::Attr::str" : of === "int" ? "fv::Attr::Int" : of === "float" ? "fv::Attr::Float" : of === "bool" ? "fv::Attr::Bool" : null;
|
|
339
339
|
if (make !== null)
|
|
340
340
|
return `${atom(v.code)}.map_or(fv::Attr::Undefined, ${make})`;
|
|
341
|
+
break;
|
|
341
342
|
}
|
|
343
|
+
default:
|
|
344
|
+
break;
|
|
342
345
|
}
|
|
343
346
|
return fail(s.comp, "FV0412", "an attribute that may fall through is a string, a number or a boolean", n);
|
|
344
347
|
}
|
package/dist/calls.js
CHANGED
|
@@ -2,7 +2,7 @@ import { BOOL, fail, FLOAT, INT, sameTy, STR } from "./model.js";
|
|
|
2
2
|
import { ctx } from "./context.js";
|
|
3
3
|
import { claim } from "./plugin.js";
|
|
4
4
|
import { atom, bare, binary, condition, operand, receiver, strArg, UNARY } from "./parens.js";
|
|
5
|
-
import { computedListMethod, items, listMethod, objectCall } from "./lists.js";
|
|
5
|
+
import { computedListMethod, isBoolean, items, listMethod, objectCall, presentLiteral } from "./lists.js";
|
|
6
6
|
import { lonely, loneOf, meet, stringMethod, stringsEqual } from "./strings.js";
|
|
7
7
|
import { asF64, intFromF64, isNumber } from "./numbers.js";
|
|
8
8
|
import { boolOf, truthy } from "./narrowing.js";
|
|
@@ -41,6 +41,8 @@ export function call(s, n) {
|
|
|
41
41
|
const helper = s.helpers.get(callee.name);
|
|
42
42
|
if (helper)
|
|
43
43
|
return helperCall(s, helper, args, n);
|
|
44
|
+
if (isBoolean(s, callee) && args.length === 1)
|
|
45
|
+
return boolOf(truthy(expr(s, args[0])));
|
|
44
46
|
if (callee.name === "String" && args.length === 1) {
|
|
45
47
|
const a = expr(s, args[0]);
|
|
46
48
|
if (a.ty.k === "str")
|
|
@@ -121,7 +123,8 @@ export function call(s, n) {
|
|
|
121
123
|
}
|
|
122
124
|
return fail(comp, "FV0704", `\`Math.${method}()\` is supported on numbers as \`max\`, \`min\`, \`abs\`, \`round\`, \`floor\`, \`ceil\` and \`trunc\``, n);
|
|
123
125
|
}
|
|
124
|
-
const
|
|
126
|
+
const literal = method === "filter" && args.length === 1 && isBoolean(s, args[0]) && callee.object.type === "ArrayExpression";
|
|
127
|
+
const target = (literal ? presentLiteral(s, callee.object) : null) ?? expr(s, callee.object);
|
|
125
128
|
if (target.ty.k === "str") {
|
|
126
129
|
const lone = loneOf(target);
|
|
127
130
|
switch (args.length === 0 ? method : "") {
|
package/dist/children.js
CHANGED
|
@@ -6,8 +6,8 @@ import { expr, fieldVal, holdsNothing } from "./expr.js";
|
|
|
6
6
|
import { asF64 } from "./numbers.js";
|
|
7
7
|
import { atom, bare } from "./parens.js";
|
|
8
8
|
import { Emitter } from "./emitter.js";
|
|
9
|
-
import { attrOf, dollarAttrs,
|
|
10
|
-
import { passedKey } from "./fallthrough.js";
|
|
9
|
+
import { attrOf, dollarAttrs, isAttrs, mergedParts } from "./attrs.js";
|
|
10
|
+
import { isListener, passedKey } from "./fallthrough.js";
|
|
11
11
|
import { claim, paramsOf, slotContextOf, slotFieldsOf } from "./plugin.js";
|
|
12
12
|
import { statements } from "./template.js";
|
|
13
13
|
import { slotContent, slotFieldBorrows, staticallyFilled } from "./slots.js";
|
|
@@ -30,7 +30,17 @@ export function renderChild(s, e, n, twin) {
|
|
|
30
30
|
const merges = parts.length > 0 && parts[0] !== rawProps;
|
|
31
31
|
const passesAttrs = parts.some(isAttrs);
|
|
32
32
|
const passed = (p) => s.fallthrough !== null && ((isAttrs(p) && s.comp.inheritAttrs) || dollarAttrs(s, p));
|
|
33
|
-
const
|
|
33
|
+
const handlers = (p) => {
|
|
34
|
+
const dynamic = p.type === "ObjectProperty" && p.computed && isCall(p.key, "_toHandlerKey");
|
|
35
|
+
if (!isCall(p, "_toHandlers") && !dynamic)
|
|
36
|
+
return false;
|
|
37
|
+
const declared = child.props.fields.find((f) => isListener(f.js));
|
|
38
|
+
if (declared)
|
|
39
|
+
fail(s.comp, "FV0514", `a listener whose name is not known before rendering, given to ${child.name}, which declares \`${declared.js}\` as a prop`, p);
|
|
40
|
+
return true;
|
|
41
|
+
};
|
|
42
|
+
const silent = (p) => p.type === "ObjectProperty" && !p.computed && !passedKey(child, p.key.name ?? String(p.key.value));
|
|
43
|
+
const marker = (p) => handlers(p) || (p.type === "ObjectExpression" && p.properties.length > 0 && p.properties.every((q) => silent(q) || handlers(q)));
|
|
34
44
|
const objects = parts.filter((p) => !isAttrs(p) && !dollarAttrs(s, p) && !marker(p));
|
|
35
45
|
if (parts.some(passed)) {
|
|
36
46
|
for (const name of s.comp.attrNames) {
|
|
@@ -60,6 +70,8 @@ export function renderChild(s, e, n, twin) {
|
|
|
60
70
|
const loaded = !merges && local !== null && s.loadedLater.has(local);
|
|
61
71
|
for (const obj of objects) {
|
|
62
72
|
for (const p of obj.properties) {
|
|
73
|
+
if (handlers(p))
|
|
74
|
+
continue;
|
|
63
75
|
if (p.type !== "ObjectProperty" || p.computed)
|
|
64
76
|
fail(s.comp, "FV0504", "child props hold plain keys", p);
|
|
65
77
|
const key = p.key.type === "Identifier" ? p.key.name : String(p.key.value);
|
|
@@ -97,6 +109,9 @@ export function renderChild(s, e, n, twin) {
|
|
|
97
109
|
});
|
|
98
110
|
callChild(s, e, child, `&super::${child.module}::Props { ${inits.join(", ")} }`, slots, childAttrsArg(s, child, parts, merges, fallthrough, ids, n), later);
|
|
99
111
|
}
|
|
112
|
+
function isCall(n, callee) {
|
|
113
|
+
return n?.type === "CallExpression" && n.callee.type === "Identifier" && n.callee.name === callee;
|
|
114
|
+
}
|
|
100
115
|
export function array(key) {
|
|
101
116
|
return /^(0|[1-9]\d*)$/.test(key) && Number(key) < 2 ** 32 - 1;
|
|
102
117
|
}
|