ferrovue 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +106 -13
  2. package/dist/args.d.ts +28 -0
  3. package/dist/args.js +69 -0
  4. package/dist/attrs.js +3 -0
  5. package/dist/calls.js +5 -2
  6. package/dist/children.js +18 -3
  7. package/dist/cli.js +80 -81
  8. package/dist/client-render.d.ts +23 -0
  9. package/dist/client-render.js +143 -0
  10. package/dist/client.d.ts +65 -6
  11. package/dist/client.js +269 -27
  12. package/dist/compiler.d.ts +9 -5
  13. package/dist/compiler.js +48 -14
  14. package/dist/component.d.ts +7 -3
  15. package/dist/component.js +37 -25
  16. package/dist/conformance.d.ts +24 -3
  17. package/dist/conformance.js +58 -7
  18. package/dist/constants.js +4 -2
  19. package/dist/context.d.ts +51 -4
  20. package/dist/context.js +173 -5
  21. package/dist/diagnostics.d.ts +6 -4
  22. package/dist/diagnostics.js +7 -3
  23. package/dist/editor.d.ts +18 -0
  24. package/dist/editor.js +59 -0
  25. package/dist/errors.d.ts +45 -3
  26. package/dist/errors.js +33 -3
  27. package/dist/expr.d.ts +2 -0
  28. package/dist/expr.js +91 -12
  29. package/dist/fallthrough.d.ts +1 -0
  30. package/dist/fallthrough.js +10 -2
  31. package/dist/files.d.ts +13 -0
  32. package/dist/files.js +49 -0
  33. package/dist/fixture.d.ts +3 -3
  34. package/dist/hydration.d.ts +12 -1
  35. package/dist/hydration.js +47 -1
  36. package/dist/index.d.ts +2 -2
  37. package/dist/index.js +1 -1
  38. package/dist/inputs.d.ts +24 -0
  39. package/dist/inputs.js +40 -0
  40. package/dist/link-router.d.ts +6 -2
  41. package/dist/link-router.js +7 -4
  42. package/dist/lists.d.ts +12 -1
  43. package/dist/lists.js +76 -2
  44. package/dist/model.d.ts +9 -1
  45. package/dist/model.js +2 -0
  46. package/dist/page-routes.d.ts +1 -0
  47. package/dist/paths.d.ts +5 -0
  48. package/dist/paths.js +7 -0
  49. package/dist/plugin.d.ts +2 -0
  50. package/dist/plugins/css-modules.d.ts +39 -0
  51. package/dist/plugins/css-modules.js +135 -0
  52. package/dist/plugins/head.js +4 -4
  53. package/dist/plugins/i18n.js +5 -12
  54. package/dist/plugins/index.js +2 -1
  55. package/dist/plugins/provide.js +4 -2
  56. package/dist/plugins/router.js +3 -2
  57. package/dist/plugins/scoped.d.ts +10 -0
  58. package/dist/plugins/scoped.js +17 -4
  59. package/dist/plugins/shared-types.js +2 -1
  60. package/dist/plugins/stores.js +7 -6
  61. package/dist/rust.d.ts +9 -1
  62. package/dist/rust.js +38 -15
  63. package/dist/schema.d.ts +25 -0
  64. package/dist/schema.js +73 -0
  65. package/dist/script.js +12 -5
  66. package/dist/slots.js +2 -0
  67. package/dist/ssr.js +42 -4
  68. package/dist/styles.d.ts +5 -1
  69. package/dist/styles.js +131 -4
  70. package/dist/testing.d.ts +2 -1
  71. package/dist/testing.js +1 -1
  72. package/dist/typescript.d.ts +11 -1
  73. package/dist/typescript.js +124 -33
  74. package/dist/vite.d.ts +18 -10
  75. package/dist/vite.js +106 -51
  76. package/dist/volar.d.ts +15 -0
  77. package/dist/volar.js +87 -0
  78. package/package.json +27 -7
  79. 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 supports the latest stable Rust, currently **1.99**. Generated code uses let-chains, so the
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. **Everything in it is replaced.** |
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.6", default-features = false }`.
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
- | 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 |
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>`, and `v-bind()` in CSS
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 (`.filter(Boolean)`,
237
- `x => { return … }`), `.map()` to optional values, a computed list as a slot prop
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 the `ferrovue` command and the Vite plugin
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/client.ts browser-side helpers: `mountIslands`, `mountPage`, `hydrateState`, `<ClientOnly>`
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 target = expr(s, callee.object);
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, IGNORED_PROPS, isAttrs, mergedParts } from "./attrs.js";
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 marker = (p) => p.type === "ObjectExpression" && p.properties.length > 0 && p.properties.every((q) => q.type === "ObjectProperty" && IGNORED_PROPS.has(q.key.name ?? q.key.value));
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
  }