ferrovue 0.14.1 → 0.15.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 +5 -3
- package/dist/compiler.js +1 -0
- package/dist/component.js +2 -1
- package/dist/config.d.ts +9 -0
- package/dist/conformance.js +1 -1
- package/dist/context.d.ts +2 -1
- package/dist/context.js +3 -0
- package/dist/dynamic.js +1 -1
- package/dist/paths.d.ts +2 -0
- package/dist/paths.js +5 -1
- package/dist/plugins/css-modules.js +3 -2
- package/dist/plugins/scoped.js +3 -2
- package/dist/schema.js +1 -0
- package/dist/ssr.d.ts +4 -0
- package/dist/ssr.js +2 -1
- package/dist/strings.js +1 -1
- package/dist/template.js +4 -8
- package/dist/vite.js +36 -4
- package/dist/whitespace.d.ts +7 -0
- package/dist/whitespace.js +58 -0
- package/package.json +11 -7
- package/schema.json +10 -0
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ the configuration at your project root yourself:
|
|
|
57
57
|
|
|
58
58
|
```json
|
|
59
59
|
{
|
|
60
|
-
"$schema": "https://cdn.jsdelivr.net/npm/ferrovue@0.
|
|
60
|
+
"$schema": "https://cdn.jsdelivr.net/npm/ferrovue@0.15.0/schema.json",
|
|
61
61
|
"components": "client/components",
|
|
62
62
|
"out": "src/generated"
|
|
63
63
|
}
|
|
@@ -87,6 +87,7 @@ read `$schema`. Offline, the copy in the installed package works as well:
|
|
|
87
87
|
| `viteRoot` | no | Vite's root, relative to this file's directory, from which a component's path is hashed (default `.`) |
|
|
88
88
|
| `isProduction` | no | Whether `@vitejs/plugin-vue` compiles for production, as Vite's `isProduction`: `true` (the default) for `vite build`, `false` for the dev server. It names the variables `v-bind()` in `<style>` sets. See [`scoped_styles`](https://docs.rs/ferrovue/latest/ferrovue/guide/scoped_styles/index.html) |
|
|
89
89
|
| `comments` | no | Whether templates' HTML comments render, as plugin-vue's `template.compilerOptions.comments`: by default kept when `isProduction` is `false` and left out otherwise. See [`generated_code`](https://docs.rs/ferrovue/latest/ferrovue/guide/generated_code/index.html#html-comments) |
|
|
90
|
+
| `whitespace` | no | `"trim-edges"` trims the whitespace where a template's text meets its element's tags, with `trimEdges` from `ferrovue/whitespace` in plugin-vue's `template.compilerOptions.nodeTransforms` for the client; `"condense"`, the default, spaces text as Vue does. See [`generated_code`](https://docs.rs/ferrovue/latest/ferrovue/guide/generated_code/index.html#whitespace) |
|
|
90
91
|
| `cssModules` | no | `{ generateScopedName, hashPrefix?, context? }`, as Vite's `css.modules` names CSS module classes: needed for `<style module>`. `context` is the directory Vite runs in (default `viteRoot`). See [`scoped_styles`](https://docs.rs/ferrovue/latest/ferrovue/guide/scoped_styles/index.html) |
|
|
91
92
|
| `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`) |
|
|
92
93
|
|
|
@@ -174,7 +175,7 @@ The crate's features:
|
|
|
174
175
|
|
|
175
176
|
- `router` and `i18n`, both on by default: vue-router and vue-i18n. An application with neither
|
|
176
177
|
routes nor translations can leave them out, and build neither `ferrovue-router` nor
|
|
177
|
-
`ferrovue-i18n`: `ferrovue = { version = "0.
|
|
178
|
+
`ferrovue-i18n`: `ferrovue = { version = "0.15", default-features = false }`.
|
|
178
179
|
- `axum` and `actix-web`: `Html` is an `IntoResponse` or a `Responder`, so a handler responds with
|
|
179
180
|
the component as `text/html; charset=utf-8`. `into_html(props)` and `into_island(props)` take the
|
|
180
181
|
props by value, so a handler that builds them returns the page:
|
|
@@ -241,6 +242,7 @@ ferrovue compiles `<script setup lang="ts">` components, and components with no
|
|
|
241
242
|
| Lists | `v-for` over arrays (of strings, numbers, objects or child props), array literals and number ranges (`n in 5`); with an index; with destructured items (`{ id, name } in rows`); nested; on `<template>`. `.includes()` (a number looked for in a list of numbers whether either is a `number` or a `Float`, as JavaScript compares them), `.join()`, and `.filter()`, `.map()`, `.some()`, `.every()`, `.find()`, `.findIndex()`, `.findLast()`, `.findLastIndex()` with an arrow function of the item (a name, or destructured) and its index, whose body is an expression or a body of statements (see Statements), narrowing inside as outside, or with `Boolean`; `.filter(Boolean)` of a list or an array literal whose items may be `null` or `undefined`, which keeps the ones present and truthy as a list of the values (`[name, title, city].filter(Boolean).join(" · ")`); `Boolean(x)`; `.slice()`; an item by its index, `list[i]` with an integer `i` and `list.at(i)` (from the end when negative), `undefined` outside the list and narrowed as other optional values are (`v-if="rows[0]"`); `.indexOf()` and `.lastIndexOf()` of a string, number or boolean, compared with `===` (`NaN` found nowhere, `-0` found as `0`); `.concat()` of lists of one item type (numbers whether `number` or `Float`); `.reduce()` and `.reduceRight()` with an initial value, into a string, a number or a boolean; `.toReversed()`, and `.toSorted()` by `String(x)` as JavaScript sorts without a comparator (`[10, 9, 1]` to `[1, 10, 9]`) or by a comparator `(a, b) => a - b`, `a.k - b.k` of integers or `?:` over `<` and `>` of integers or strings, stable as JavaScript's; `[...list].sort(…)` and `[...list].reverse()`; spread in array literals (`[...a, x, ...b]`) of one item type; `.flat()` of lists of lists, to a literal depth or `Infinity`, and `.flatMap()` with an arrow function that gives a list (flattened one level) or a value (kept as `.map()` keeps it); chained and nested; a computed list read by `v-for`, `.join()`, `.length`, `.includes()`, kept by `computed`, or handed to a child |
|
|
242
243
|
| Dictionaries | `v-for="(value, key, index) in r"` over a `Record<string, T>`, and `([key, value], index) in Object.entries(r)`, in JavaScript's order: keys that are array indices first, in numeric order, then the others as the props give them; `Object.keys(r)`, `Object.values(r)` (of a record of lists too, which `.flat()` joins), `Object.hasOwn(r, key)`, and `Object.entries(r)` read by the list methods, whose arrow functions destructure each pair (`([key, value]) => …`, `([, value]) => …`, `([key, { id }]) => …`) or read its parts (`pair[0]`, `pair[1]`), and walked by `v-for="[key, value] in Object.entries(r).filter(…)"`, kept by `computed` and sorted by a comparator of either part; handed to a child |
|
|
243
244
|
| HTML comments | left out, as Vue's production build leaves them out, or kept with `comments` or `"isProduction": false`, as on the dev server: written as Vue writes them, at the root, between roots, inside elements and in slot content |
|
|
245
|
+
| Whitespace | text condensed as Vue condenses it, or with `"whitespace": "trim-edges"` also trimmed where it meets its element's tags, the client compiled alike with `trimEdges`: in elements, components and `<template v-slot>`, keeping `<pre>`, `<textarea>`, `v-pre` and a static `white-space: pre*` style. See [`generated_code`](https://docs.rs/ferrovue/latest/ferrovue/guide/generated_code/index.html#whitespace) |
|
|
244
246
|
| Attributes | static and bound attributes, boolean attributes, `:hidden`, `data-*` and `aria-*`, `v-bind` objects |
|
|
245
247
|
| `class` | strings, arrays, objects (`{ active: on }`, computed keys), `cond && "x"`, `cond ? "x" : null`, merged with a static `class` |
|
|
246
248
|
| `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 |
|
|
@@ -248,7 +250,7 @@ ferrovue compiles `<script setup lang="ts">` components, and components with no
|
|
|
248
250
|
| `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`) |
|
|
249
251
|
| 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 |
|
|
250
252
|
| Components | imported child components, `v-bind` of a child's own `Props`, listeners in any form (`@x`, `v-on="obj"`, `@[name]`), which the server skips, `v-model` on a child's `defineModel`, recursion; props named in `kebab-case` or `camelCase`; a `boolean` prop written bare (`<Button disabled />`), as `""` or as its kebab-case name, which Vue casts to `true`; props named like listeners (`on1`, `onSale`), left out when falsy where Vue's `mergeProps` joins a child's props, as Vue leaves them out; `defineAsyncComponent(() => import("./X.vue"))` (or `{ loader: … }`), rendered as the component it loads, which Vue's server renderer waits for |
|
|
251
|
-
| `<component :is>` | over a closed set of choices, compiled to a `match`: an imported component (`:is="Card"`), an HTML element's name (`is="h2"`), a `computed` or `?:` choosing among them, a prop or a `defineModel` typed as a union of string literals (`as: "h1" \| "h2"`, a `match` over its enum's variants), and an object of imported components (or element names) declared in setup or imported from a `.ts` file, read by such a prop (`ICONS[name]`); props, fallthrough attributes, slots and scope ids reach each choice as they reach a static child; also inside `<KeepAlive>` and `<Transition>`. See the crate's [`generated_code`](https://docs.rs/ferrovue/latest/ferrovue/guide/generated_code/index.html#component-is) guide |
|
|
253
|
+
| `<component :is>` | over a closed set of choices, compiled to a `match`: an imported component (`:is="Card"`), an HTML element's name (`is="h2"`), a `computed` or `?:` choosing among them, a prop or a `defineModel` typed as a union of string literals (`as: "h1" \| "h2"`, a `match` over its enum's variants) or as one (`as: "h1"`), 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 |
|
|
252
254
|
| Escape hatches | `<ClientOnly>` from `ferrovue`, whose default slot the server never renders, so anything may go in it; components listed in `twins`, rendered by Rust functions of yours. See the crate's [`errors_and_limits`](https://docs.rs/ferrovue/latest/ferrovue/guide/errors_and_limits/index.html#client-only-content) guide |
|
|
253
255
|
| 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 |
|
|
254
256
|
| 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 |
|
package/dist/compiler.js
CHANGED
|
@@ -47,6 +47,7 @@ function generateAll(root, config, sources, options) {
|
|
|
47
47
|
ctx.mappedUnions = new Map();
|
|
48
48
|
ctx.builders = config.builders ?? true;
|
|
49
49
|
ctx.comments = templateComments(config);
|
|
50
|
+
ctx.whitespace = config.whitespace ?? "condense";
|
|
50
51
|
ctx.clientDirectives = new Set(config.clientDirectives ?? []);
|
|
51
52
|
ctx.rootDir = root;
|
|
52
53
|
ctx.vnodeTag = null;
|
package/dist/component.js
CHANGED
|
@@ -14,6 +14,7 @@ import { claim } from "./plugin.js";
|
|
|
14
14
|
import { asyncChildren } from "./script.js";
|
|
15
15
|
import { declareConsts } from "./constants.js";
|
|
16
16
|
import { resolveImport } from "./resolve.js";
|
|
17
|
+
import { trimEdges } from "./whitespace.js";
|
|
17
18
|
function vueErrorNode(err, within) {
|
|
18
19
|
const loc = err.loc;
|
|
19
20
|
if (!loc)
|
|
@@ -451,7 +452,7 @@ export function readComponent(file, root, name = basename(file, ".vue")) {
|
|
|
451
452
|
filename: file,
|
|
452
453
|
...(claim((p) => p.templateOptions?.(comp)) ?? { id: name, scoped: false, slotted: false, ssrCssVars: [], isProd: true }),
|
|
453
454
|
ssr: true,
|
|
454
|
-
compilerOptions: { ...(script ? { bindingMetadata: script.bindings } : {}), comments: ctx.comments, sourceMap: true, nodeTransforms: [mergedControls(controls)] },
|
|
455
|
+
compilerOptions: { ...(script ? { bindingMetadata: script.bindings } : {}), comments: ctx.comments, sourceMap: true, nodeTransforms: [...(ctx.whitespace === "trim-edges" ? [trimEdges] : []), mergedControls(controls)] },
|
|
455
456
|
});
|
|
456
457
|
const kids = (n) => n.children.filter((c) => c.type !== 3 && !(c.type === 2 && !c.content.trim()));
|
|
457
458
|
const roots = descriptor.template.ast ? kids(descriptor.template.ast) : [];
|
package/dist/config.d.ts
CHANGED
|
@@ -103,6 +103,11 @@ export interface Config {
|
|
|
103
103
|
* `template.compilerOptions.comments`: by default as Vue decides it, kept when `isProduction` is
|
|
104
104
|
* `false` and left out when it is `true`. */
|
|
105
105
|
comments?: boolean;
|
|
106
|
+
/** How the text of templates is spaced: `"condense"` (the default) as Vue spaces it, or
|
|
107
|
+
* `"trim-edges"`, which also trims the whitespace where text meets its element's tags. The
|
|
108
|
+
* client compiles the same way with `trimEdges` from `ferrovue/whitespace` in
|
|
109
|
+
* `@vitejs/plugin-vue`'s `template.compilerOptions.nodeTransforms`. */
|
|
110
|
+
whitespace?: WhitespaceMode;
|
|
106
111
|
/** CSS modules (`<style module>`): how Vite's `css.modules` names their classes, which ferrovue
|
|
107
112
|
* computes as postcss-modules does. Without it, `<style module>` is refused. */
|
|
108
113
|
cssModules?: {
|
|
@@ -120,6 +125,10 @@ export interface Config {
|
|
|
120
125
|
builders?: boolean;
|
|
121
126
|
}
|
|
122
127
|
export type ScopeIdMode = "filepath" | "filepath-source";
|
|
128
|
+
/** How the text of templates is spaced: `"condense"`, Vue's own rule, or `"trim-edges"`, which
|
|
129
|
+
* also trims text where it meets its element's tags, as `trimEdges` from `ferrovue/whitespace`
|
|
130
|
+
* does for the client. */
|
|
131
|
+
export type WhitespaceMode = "condense" | "trim-edges";
|
|
123
132
|
/** Where the routes come from: a JSON file of routes, or a folder of pages. */
|
|
124
133
|
export type RoutesSource = string | {
|
|
125
134
|
/** The folder of pages, each a component, whose file names give the routes. */
|
package/dist/conformance.js
CHANGED
|
@@ -254,7 +254,7 @@ export function registerConformance(api, options) {
|
|
|
254
254
|
for (const [name, component] of given) {
|
|
255
255
|
const source = sources.get(name);
|
|
256
256
|
if (source)
|
|
257
|
-
attachSsrRender(source, name, component, { comments: templateComments(config) });
|
|
257
|
+
attachSsrRender(source, name, component, { comments: templateComments(config), whitespace: config.whitespace ?? "condense" });
|
|
258
258
|
}
|
|
259
259
|
const cases = casesIn(fixtures);
|
|
260
260
|
const { routes, options: configured } = fixtureOptions(root, config);
|
package/dist/context.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Config, TypeName } from "./config.ts";
|
|
1
|
+
import type { Config, TypeName, WhitespaceMode } from "./config.ts";
|
|
2
2
|
import type { Plugin } from "./plugin.ts";
|
|
3
3
|
import type { Declared, Exports } from "./constants.ts";
|
|
4
4
|
import type { Alias } from "./compiler.ts";
|
|
@@ -33,6 +33,7 @@ export declare const ctx: {
|
|
|
33
33
|
mappedUnions: Map<string, EnumDef>;
|
|
34
34
|
builders: boolean;
|
|
35
35
|
comments: boolean;
|
|
36
|
+
whitespace: WhitespaceMode;
|
|
36
37
|
clientDirectives: Set<string>;
|
|
37
38
|
plugins: readonly Plugin[];
|
|
38
39
|
runs: Map<Plugin<unknown, unknown>, unknown>;
|
package/dist/context.js
CHANGED
|
@@ -102,6 +102,8 @@ export function checkConfig(raw, file, warn, schema = CONFIG_SCHEMA) {
|
|
|
102
102
|
wrong("isProduction", "`true` or `false`: whether `@vitejs/plugin-vue` compiles for production");
|
|
103
103
|
if (raw.comments !== undefined && typeof raw.comments !== "boolean")
|
|
104
104
|
wrong("comments", "`true` or `false`: whether the HTML comments of templates render");
|
|
105
|
+
if (raw.whitespace !== undefined && raw.whitespace !== "condense" && raw.whitespace !== "trim-edges")
|
|
106
|
+
wrong("whitespace", '`"condense"` or `"trim-edges"`: how the text of templates is spaced');
|
|
105
107
|
if (raw.cssModules !== undefined) {
|
|
106
108
|
const m = raw.cssModules;
|
|
107
109
|
if (!isObject(m))
|
|
@@ -225,6 +227,7 @@ export const ctx = {
|
|
|
225
227
|
mappedUnions: new Map(),
|
|
226
228
|
builders: true,
|
|
227
229
|
comments: false,
|
|
230
|
+
whitespace: "condense",
|
|
228
231
|
clientDirectives: new Set(),
|
|
229
232
|
plugins: [],
|
|
230
233
|
runs: new Map(),
|
package/dist/dynamic.js
CHANGED
|
@@ -143,7 +143,7 @@ function propType(s, name) {
|
|
|
143
143
|
}
|
|
144
144
|
function fieldLiterals(ty) {
|
|
145
145
|
const literals = literalsOf(ty.k === "opt" ? ty.of : ty);
|
|
146
|
-
return (ty.k === "opt" ? ty.of : ty).k === "str" && literals !== undefined && literals.
|
|
146
|
+
return (ty.k === "opt" ? ty.of : ty).k === "str" && literals !== undefined && literals.every((l) => typeof l === "string") ? literals : null;
|
|
147
147
|
}
|
|
148
148
|
function keysOf(s, n, at) {
|
|
149
149
|
const name = propOf(s, n);
|
package/dist/paths.d.ts
CHANGED
|
@@ -3,3 +3,5 @@ import * as nodePath from "node:path";
|
|
|
3
3
|
* code, a diagnostic or a key, or compares with a configured folder such as `out`, reads the same
|
|
4
4
|
* on Windows as elsewhere. `path` is Node's own, and `path.win32` in a test. */
|
|
5
5
|
export declare function relativePath(from: string, to: string, path?: Pick<typeof nodePath, "relative" | "sep">): string;
|
|
6
|
+
/** A path written with `/` on every platform, as Vite writes the ids and imports it reads. */
|
|
7
|
+
export declare function slashed(file: string, path?: Pick<typeof nodePath, "sep">): string;
|
package/dist/paths.js
CHANGED
|
@@ -3,5 +3,9 @@ import * as nodePath from "node:path";
|
|
|
3
3
|
* code, a diagnostic or a key, or compares with a configured folder such as `out`, reads the same
|
|
4
4
|
* on Windows as elsewhere. `path` is Node's own, and `path.win32` in a test. */
|
|
5
5
|
export function relativePath(from, to, path = nodePath) {
|
|
6
|
-
return path.relative(from, to)
|
|
6
|
+
return slashed(path.relative(from, to), path);
|
|
7
|
+
}
|
|
8
|
+
/** A path written with `/` on every platform, as Vite writes the ids and imports it reads. */
|
|
9
|
+
export function slashed(file, path = nodePath) {
|
|
10
|
+
return file.split(path.sep).join("/");
|
|
7
11
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolve as resolvePath
|
|
1
|
+
import { resolve as resolvePath } from "node:path";
|
|
2
2
|
import genericNames from "generic-names";
|
|
3
3
|
import postcss, {} from "postcss";
|
|
4
4
|
import extractImports from "postcss-modules-extract-imports";
|
|
@@ -7,6 +7,7 @@ import modulesScope from "postcss-modules-scope";
|
|
|
7
7
|
import modulesValues from "postcss-modules-values";
|
|
8
8
|
import { fail, rustStr, sourceAt, STR, UNDEF } from "../model.js";
|
|
9
9
|
import { CONFIG_FILE } from "../config.js";
|
|
10
|
+
import { slashed } from "../paths.js";
|
|
10
11
|
import {} from "../context.js";
|
|
11
12
|
import { runOf, scopeOf } from "../plugin.js";
|
|
12
13
|
import { scopeHash } from "./scoped.js";
|
|
@@ -19,7 +20,7 @@ export function styleRequest(file, index, style, id) {
|
|
|
19
20
|
if (!IGNORED_ATTRS.has(name))
|
|
20
21
|
query += `&${encodeURIComponent(name)}${value ? `=${encodeURIComponent(value)}` : ""}`;
|
|
21
22
|
}
|
|
22
|
-
return `${resolvePath(file)
|
|
23
|
+
return `${slashed(resolvePath(file))}?vue&type=style&index=${index}${style.scoped ? `&scoped=${id}` : ""}${query}&lang.module.css`;
|
|
23
24
|
}
|
|
24
25
|
/** The classes, keyframes and values a CSS module exports, by name, as postcss-modules names them
|
|
25
26
|
* with `names`. */
|
package/dist/plugins/scoped.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { parse as parseJs } from "@babel/parser";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import { resolve as resolvePath } from "node:path";
|
|
4
4
|
import { fail, namesSelf, rustStr } from "../model.js";
|
|
5
5
|
import {} from "../config.js";
|
|
6
6
|
import { runOf } from "../plugin.js";
|
|
7
7
|
import { untilSettled } from "../fallthrough.js";
|
|
8
|
+
import { relativePath } from "../paths.js";
|
|
8
9
|
export function scopeIdOf(comp) {
|
|
9
10
|
return runOf(scoped).ids.get(comp) ?? null;
|
|
10
11
|
}
|
|
11
12
|
/** The id `@vitejs/plugin-vue` gives a component, `descriptor.id`: a hash of its path from Vite's
|
|
12
13
|
* root, followed by its source unless `mode` hashes the path alone. */
|
|
13
14
|
export function componentId(file, source, mode, viteRoot) {
|
|
14
|
-
const path =
|
|
15
|
+
const path = relativePath(viteRoot, resolvePath(file));
|
|
15
16
|
return createHash("sha256")
|
|
16
17
|
.update(mode === "filepath" ? path : path + source)
|
|
17
18
|
.digest("hex")
|
package/dist/schema.js
CHANGED
|
@@ -63,6 +63,7 @@ export const CONFIG_SCHEMA = {
|
|
|
63
63
|
viteRoot: { type: "string", default: "." },
|
|
64
64
|
isProduction: { type: "boolean", default: true },
|
|
65
65
|
comments: { type: "boolean" },
|
|
66
|
+
whitespace: { type: "string", enum: ["condense", "trim-edges"], default: "condense" },
|
|
66
67
|
cssModules: {
|
|
67
68
|
type: "object",
|
|
68
69
|
properties: { generateScopedName: STRING, hashPrefix: { type: "string", default: "" }, context: STRING },
|
package/dist/ssr.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { Component } from "vue";
|
|
2
|
+
import type { WhitespaceMode } from "./config.ts";
|
|
2
3
|
/** How `attachSsrRender` compiles a component's template. */
|
|
3
4
|
export interface SsrRenderOptions {
|
|
4
5
|
/** Whether the template's HTML comments render, as `comments` in `ferrovue.config.json`
|
|
5
6
|
* decides it for the generated code: `false` by default. */
|
|
6
7
|
comments?: boolean;
|
|
8
|
+
/** How the template's text is spaced, as `whitespace` in `ferrovue.config.json` decides it for
|
|
9
|
+
* the generated code: `"condense"` by default. */
|
|
10
|
+
whitespace?: WhitespaceMode;
|
|
7
11
|
}
|
|
8
12
|
/** Give a component compiled for the browser the `ssrRender` its SSR build would have. */
|
|
9
13
|
export declare function attachSsrRender(file: string, name: string, component: Component, options?: SsrRenderOptions): void;
|
package/dist/ssr.js
CHANGED
|
@@ -3,6 +3,7 @@ import { compileScript, compileTemplate, parse as parseSfc } from "@vue/compiler
|
|
|
3
3
|
import { getEscapedCssVarName } from "@vue/shared";
|
|
4
4
|
import * as vue from "vue";
|
|
5
5
|
import * as serverRenderer from "vue/server-renderer";
|
|
6
|
+
import { trimEdges } from "./whitespace.js";
|
|
6
7
|
const KEY = /(?:^|[{,])\s*"((?:[^"\\]|\\.)*)"\s*:/g;
|
|
7
8
|
function cssVarNames(file, component, count) {
|
|
8
9
|
const text = ["setup", "ssrRender"]
|
|
@@ -43,7 +44,7 @@ export function attachSsrRender(file, name, component, options = {}) {
|
|
|
43
44
|
ssr: true,
|
|
44
45
|
ssrCssVars: descriptor.cssVars,
|
|
45
46
|
isProd: true,
|
|
46
|
-
compilerOptions: { ...(script?.bindings ? { bindingMetadata: script.bindings } : {}), comments },
|
|
47
|
+
compilerOptions: { ...(script?.bindings ? { bindingMetadata: script.bindings } : {}), comments, ...(options.whitespace === "trim-edges" ? { nodeTransforms: [trimEdges] } : {}) },
|
|
47
48
|
}).code.replace(/^(\s*)":--[^"]*":/gm, (_, indent) => `${indent}":--${varNames[named++]}":`);
|
|
48
49
|
const body = code
|
|
49
50
|
.replace(/import \{([^}]*)\} from "vue"/g, (_, names) => `const {${names.replace(/ as /g, ": ")}} = __vue;`)
|
package/dist/strings.js
CHANGED
|
@@ -303,7 +303,7 @@ export function regexShape(pattern, flags) {
|
|
|
303
303
|
const start = at;
|
|
304
304
|
while (/[0-9]/.test(peek() ?? ""))
|
|
305
305
|
at++;
|
|
306
|
-
return at > start ? Number(pattern.slice(start, at)) : null;
|
|
306
|
+
return at > start ? Math.min(Number(pattern.slice(start, at)), 2 ** 31 - 1) : null;
|
|
307
307
|
};
|
|
308
308
|
const quantifier = () => {
|
|
309
309
|
const c = peek();
|
package/dist/template.js
CHANGED
|
@@ -35,7 +35,6 @@ function heldContent(p) {
|
|
|
35
35
|
return p.properties.find((q) => q.type !== "ObjectProperty" || q.computed || CONTENT_KEYS.has(q.key.name ?? q.key.value)) ?? null;
|
|
36
36
|
}
|
|
37
37
|
function directedContent(s, n) {
|
|
38
|
-
s.temps.add(n.left.name);
|
|
39
38
|
const parts = mergedParts(n.right);
|
|
40
39
|
const held = parts.map(heldContent).find((p) => p !== null);
|
|
41
40
|
if (held === undefined)
|
|
@@ -60,10 +59,7 @@ function rootControlKnown(s, names) {
|
|
|
60
59
|
}
|
|
61
60
|
function unpassedValue(s, n) {
|
|
62
61
|
const test = n?.type === "ConditionalExpression" ? n.test : null;
|
|
63
|
-
|
|
64
|
-
return n;
|
|
65
|
-
rootControlKnown(s, [test.left.value]);
|
|
66
|
-
return n.alternate;
|
|
62
|
+
return s.temps.has(test?.right?.name) ? n.alternate : n;
|
|
67
63
|
}
|
|
68
64
|
function modelAttrs(s, n) {
|
|
69
65
|
const [set, merged] = n.expressions;
|
|
@@ -125,6 +121,8 @@ export function slot(s, e, n) {
|
|
|
125
121
|
renderDynamicAttr(s, e, a[0].value, attrShown(s, a[0].value, a[1]), n);
|
|
126
122
|
return;
|
|
127
123
|
case "_ssrRenderAttrs":
|
|
124
|
+
if (a[0].type === "AssignmentExpression")
|
|
125
|
+
s.temps.add(a[0].left.name);
|
|
128
126
|
if (a.length > 1 && !(a[1].type === "StringLiteral" && a[1].value === "textarea" && rootControlKnown(s, ["value"])))
|
|
129
127
|
fail(s.comp, "FV0006", "`ssrRenderAttrs` with a tag argument", n);
|
|
130
128
|
if (a[0].type === "SequenceExpression") {
|
|
@@ -132,9 +130,7 @@ export function slot(s, e, n) {
|
|
|
132
130
|
return;
|
|
133
131
|
}
|
|
134
132
|
if (a[0].type === "AssignmentExpression") {
|
|
135
|
-
if (a.length
|
|
136
|
-
s.temps.add(a[0].left.name);
|
|
137
|
-
else
|
|
133
|
+
if (a.length === 1)
|
|
138
134
|
directedContent(s, a[0]);
|
|
139
135
|
renderAttrs(s, e, a[0].right);
|
|
140
136
|
return;
|
package/dist/vite.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
2
|
-
import { join, posix, resolve
|
|
2
|
+
import { join, posix, resolve } from "node:path";
|
|
3
3
|
import { write } from "./compiler.js";
|
|
4
4
|
import { CONFIG_FILE, loadConfig } from "./config.js";
|
|
5
5
|
import { templateComments } from "./context.js";
|
|
@@ -7,6 +7,7 @@ import {} from "./errors.js";
|
|
|
7
7
|
import { formatRefusal, formatWarning, isRefusal } from "./diagnostics.js";
|
|
8
8
|
import { allPages, fileRoutes, pagesFolder } from "./file-routes.js";
|
|
9
9
|
import { affects, inputsOf, watched } from "./inputs.js";
|
|
10
|
+
import { slashed } from "./paths.js";
|
|
10
11
|
/** How `@vitejs/plugin-vue`, as this build configures it, computes a `<style scoped>` id: from the
|
|
11
12
|
* path alone or the path and source, hashed from Vite's root; and whether it compiles for production.
|
|
12
13
|
* `null` without the plugin, or with an id generator of its own. */
|
|
@@ -27,6 +28,18 @@ function vueComments(config) {
|
|
|
27
28
|
const set = vue.api?.options?.template?.compilerOptions?.comments;
|
|
28
29
|
return typeof set === "boolean" ? set : !config.isProduction;
|
|
29
30
|
}
|
|
31
|
+
const TRIM_EDGES = Symbol.for("ferrovue.whitespace");
|
|
32
|
+
/** How `@vitejs/plugin-vue`, as this build configures it, spaces the text of templates:
|
|
33
|
+
* `"trim-edges"` when its `template.compilerOptions.nodeTransforms` holds `trimEdges`, from any
|
|
34
|
+
* copy of ferrovue. `null` without the plugin. */
|
|
35
|
+
function vueWhitespace(config) {
|
|
36
|
+
const vue = config.plugins.find((p) => p.name === "vite:vue");
|
|
37
|
+
if (!vue)
|
|
38
|
+
return null;
|
|
39
|
+
const transforms = vue.api?.options?.template?.compilerOptions?.nodeTransforms;
|
|
40
|
+
const trims = Array.isArray(transforms) && transforms.some((t) => typeof t === "function" && t[TRIM_EDGES] === "trim-edges");
|
|
41
|
+
return trims ? "trim-edges" : "condense";
|
|
42
|
+
}
|
|
30
43
|
function componentSources(root, config) {
|
|
31
44
|
let files = [];
|
|
32
45
|
try {
|
|
@@ -88,6 +101,20 @@ function commentsMismatch(root, vue, configFile = CONFIG_FILE) {
|
|
|
88
101
|
const kept = (on) => (on ? "keeps them" : "leaves them out");
|
|
89
102
|
return `the HTML comments of templates: @vitejs/plugin-vue ${kept(vue)}, where ${configFile} ${kept(own)}; set \`"comments": ${vue}\` in ${configFile}`;
|
|
90
103
|
}
|
|
104
|
+
/** Why the server would space the text of templates otherwise than the client, when plugin-vue
|
|
105
|
+
* trims it at the edges, `vue`, where the configuration, `configFile`, keeps it, or the other way
|
|
106
|
+
* round. */
|
|
107
|
+
function whitespaceMismatch(root, vue, configFile = CONFIG_FILE) {
|
|
108
|
+
if (vue === null)
|
|
109
|
+
return null;
|
|
110
|
+
const own = loadConfig(root, configFile, ignore).whitespace ?? "condense";
|
|
111
|
+
if (own === vue)
|
|
112
|
+
return null;
|
|
113
|
+
if (own === "trim-edges") {
|
|
114
|
+
return `the whitespace at the edges of templates' text: ${configFile} trims it with \`"whitespace": "trim-edges"\`, where @vitejs/plugin-vue keeps it; add \`trimEdges\` from \`ferrovue/whitespace\` to plugin-vue's \`template.compilerOptions.nodeTransforms\``;
|
|
115
|
+
}
|
|
116
|
+
return `the whitespace at the edges of templates' text: @vitejs/plugin-vue trims it with \`trimEdges\`, where ${configFile} keeps it; set \`"whitespace": "trim-edges"\` in ${configFile}`;
|
|
117
|
+
}
|
|
91
118
|
/** Why the class names of the server's CSS modules would differ from those Vite gives the client,
|
|
92
119
|
* when a component has `<style module>` and Vite's `css`, run from `cwd`, names them otherwise than
|
|
93
120
|
* `cssModules` in the configuration, `configFile`. */
|
|
@@ -126,13 +153,13 @@ const ISLANDS_ID = `\0${ISLANDS}`;
|
|
|
126
153
|
const ROUTES = "ferrovue/routes";
|
|
127
154
|
const ROUTES_ID = `\0${ROUTES}`;
|
|
128
155
|
function islandsModule(root, islands) {
|
|
129
|
-
const entries = Object.entries(islands).map(([name, file]) => ` ${JSON.stringify(name)}: () => import(${JSON.stringify(resolve(root, file)
|
|
156
|
+
const entries = Object.entries(islands).map(([name, file]) => ` ${JSON.stringify(name)}: () => import(${JSON.stringify(slashed(resolve(root, file)))}),`);
|
|
130
157
|
return `export default {\n${entries.join("\n")}\n};\n`;
|
|
131
158
|
}
|
|
132
159
|
function routesTree(root, routes, indent, lazy) {
|
|
133
160
|
return routes
|
|
134
161
|
.map((r) => {
|
|
135
|
-
const page = r.file ? `() => import(${JSON.stringify(resolve(root, r.file)
|
|
162
|
+
const page = r.file ? `() => import(${JSON.stringify(slashed(resolve(root, r.file)))})` : null;
|
|
136
163
|
const fields = [
|
|
137
164
|
`path: ${JSON.stringify(r.path)}`,
|
|
138
165
|
...(r.name !== undefined ? [`name: ${JSON.stringify(r.name)}`] : []),
|
|
@@ -212,6 +239,7 @@ export default function ferrovue(options = {}) {
|
|
|
212
239
|
};
|
|
213
240
|
let vue = null;
|
|
214
241
|
let comments = null;
|
|
242
|
+
let whitespace = null;
|
|
215
243
|
let aliases = [];
|
|
216
244
|
let css;
|
|
217
245
|
let building = true;
|
|
@@ -239,6 +267,7 @@ export default function ferrovue(options = {}) {
|
|
|
239
267
|
readsImportedTypes(config);
|
|
240
268
|
vue = vueScopeIds(config);
|
|
241
269
|
comments = vueComments(config);
|
|
270
|
+
whitespace = vueWhitespace(config);
|
|
242
271
|
aliases = config.resolve.alias;
|
|
243
272
|
css = config.css;
|
|
244
273
|
building = config.command === "build";
|
|
@@ -246,7 +275,10 @@ export default function ferrovue(options = {}) {
|
|
|
246
275
|
buildStart() {
|
|
247
276
|
try {
|
|
248
277
|
regenerate((w) => this.warn(formatWarning(w)));
|
|
249
|
-
const mismatch = scopeIdMismatch(root, vue, configFile) ??
|
|
278
|
+
const mismatch = scopeIdMismatch(root, vue, configFile) ??
|
|
279
|
+
cssModulesMismatch(root, css, process.cwd(), configFile) ??
|
|
280
|
+
whitespaceMismatch(root, whitespace, configFile) ??
|
|
281
|
+
(building ? commentsMismatch(root, comments, configFile) : null);
|
|
250
282
|
if (mismatch && building)
|
|
251
283
|
this.error(`ferrovue: ${mismatch}`);
|
|
252
284
|
if (mismatch)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** A node transform for `@vitejs/plugin-vue`'s `template.compilerOptions.nodeTransforms` that
|
|
2
|
+
* trims the whitespace at the start and end of an element's text, where it meets the element's
|
|
3
|
+
* tags, as `"whitespace": "trim-edges"` makes ferrovue's Rust render it. It trims the content of
|
|
4
|
+
* elements, components and `<template v-slot>`, and leaves `<pre>`, `<textarea>`, `v-pre` and an
|
|
5
|
+
* element whose `style` sets `white-space: pre`, `pre-wrap`, `pre-line` or `break-spaces` as they
|
|
6
|
+
* are, with everything inside them. */
|
|
7
|
+
export declare function trimEdges(node: object): void;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const ROOT = 0;
|
|
2
|
+
const ELEMENT = 1;
|
|
3
|
+
const TEXT = 2;
|
|
4
|
+
const PLAIN = 0;
|
|
5
|
+
const COMPONENT = 1;
|
|
6
|
+
const TEMPLATE = 3;
|
|
7
|
+
const LEADING = /^[ \t\n\f\r]+/;
|
|
8
|
+
const TRAILING = /[ \t\n\f\r]+$/;
|
|
9
|
+
const KEPT_TAGS = new Set(["pre", "textarea"]);
|
|
10
|
+
const PRE_STYLE = /(?:^|;)\s*white-space\s*:\s*(?:pre|pre-wrap|pre-line|break-spaces)\s*(?:!important\s*)?(?:;|$)/i;
|
|
11
|
+
const OPEN_TAG = /^<[^\s/>]+(?:[^"'>]|"[^"]*"|'[^']*')*/;
|
|
12
|
+
const QUOTED = /"[^"]*"|'[^']*'/g;
|
|
13
|
+
const V_PRE = /\sv-pre(?=[\s/=]|$)/;
|
|
14
|
+
const MARK = Symbol.for("ferrovue.whitespace");
|
|
15
|
+
function keeps(el) {
|
|
16
|
+
if (KEPT_TAGS.has(el.tag.toLowerCase()))
|
|
17
|
+
return true;
|
|
18
|
+
if (el.props.some((p) => p.type === 6 && p.name === "style" && p.value !== undefined && PRE_STYLE.test(p.value.content)))
|
|
19
|
+
return true;
|
|
20
|
+
const open = OPEN_TAG.exec(el.loc.source)?.[0] ?? "";
|
|
21
|
+
return V_PRE.test(open.replace(QUOTED, '""'));
|
|
22
|
+
}
|
|
23
|
+
function trims(el) {
|
|
24
|
+
if (el.tagType === PLAIN || el.tagType === COMPONENT)
|
|
25
|
+
return true;
|
|
26
|
+
return el.tagType === TEMPLATE && el.props.some((p) => p.type === 7 && p.name === "slot");
|
|
27
|
+
}
|
|
28
|
+
function trimChildren(el) {
|
|
29
|
+
const children = el.children;
|
|
30
|
+
const first = children[0];
|
|
31
|
+
if (first?.type === TEXT)
|
|
32
|
+
first.content = first.content.replace(LEADING, "");
|
|
33
|
+
const last = children.at(-1);
|
|
34
|
+
if (last?.type === TEXT)
|
|
35
|
+
last.content = last.content.replace(TRAILING, "");
|
|
36
|
+
el.children = children.filter((c) => c.type !== TEXT || c.content !== "");
|
|
37
|
+
}
|
|
38
|
+
function walk(node) {
|
|
39
|
+
for (const child of node.children ?? []) {
|
|
40
|
+
if (child.type !== ELEMENT || keeps(child))
|
|
41
|
+
continue;
|
|
42
|
+
if (trims(child))
|
|
43
|
+
trimChildren(child);
|
|
44
|
+
walk(child);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** A node transform for `@vitejs/plugin-vue`'s `template.compilerOptions.nodeTransforms` that
|
|
48
|
+
* trims the whitespace at the start and end of an element's text, where it meets the element's
|
|
49
|
+
* tags, as `"whitespace": "trim-edges"` makes ferrovue's Rust render it. It trims the content of
|
|
50
|
+
* elements, components and `<template v-slot>`, and leaves `<pre>`, `<textarea>`, `v-pre` and an
|
|
51
|
+
* element whose `style` sets `white-space: pre`, `pre-wrap`, `pre-line` or `break-spaces` as they
|
|
52
|
+
* are, with everything inside them. */
|
|
53
|
+
export function trimEdges(node) {
|
|
54
|
+
const n = node;
|
|
55
|
+
if (n.type === ROOT)
|
|
56
|
+
walk(n);
|
|
57
|
+
}
|
|
58
|
+
Object.defineProperty(trimEdges, MARK, { value: "trim-edges" });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ferrovue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Vue templates compiled to Rust render functions: server-side rendering with no JavaScript at run time",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -85,6 +85,10 @@
|
|
|
85
85
|
"types": "./dist/vite.d.ts",
|
|
86
86
|
"default": "./dist/vite.js"
|
|
87
87
|
},
|
|
88
|
+
"./whitespace": {
|
|
89
|
+
"types": "./dist/whitespace.d.ts",
|
|
90
|
+
"default": "./dist/whitespace.js"
|
|
91
|
+
},
|
|
88
92
|
"./volar": {
|
|
89
93
|
"types": "./dist/volar.d.ts",
|
|
90
94
|
"default": "./dist/volar.js"
|
|
@@ -96,8 +100,8 @@
|
|
|
96
100
|
"ferrovue": "./dist/cli.js"
|
|
97
101
|
},
|
|
98
102
|
"dependencies": {
|
|
99
|
-
"@babel/parser": "^7.29.
|
|
100
|
-
"@intlify/message-compiler": "11.4.
|
|
103
|
+
"@babel/parser": "^7.29.11",
|
|
104
|
+
"@intlify/message-compiler": "11.4.18",
|
|
101
105
|
"@vue/compiler-sfc": "3.5.44",
|
|
102
106
|
"@vue/shared": "3.5.44",
|
|
103
107
|
"generic-names": "^4.0.0",
|
|
@@ -114,14 +118,14 @@
|
|
|
114
118
|
"@vue/language-service": "3.3.13",
|
|
115
119
|
"@vue/server-renderer": "3.5.44",
|
|
116
120
|
"pinia": "^4.1.0",
|
|
117
|
-
"playwright": "1.
|
|
121
|
+
"playwright": "1.65.0",
|
|
118
122
|
"typescript": "^7.0.2",
|
|
119
123
|
"typescript-js": "npm:typescript@6.0.3",
|
|
120
|
-
"vite": "^8.3.
|
|
121
|
-
"vitest": "^5.0
|
|
124
|
+
"vite": "^8.3.6",
|
|
125
|
+
"vitest": "^5.1.0",
|
|
122
126
|
"vscode-uri": "3.2.0",
|
|
123
127
|
"vue": "3.5.44",
|
|
124
|
-
"vue-i18n": "11.4.
|
|
128
|
+
"vue-i18n": "11.4.18",
|
|
125
129
|
"vue-router": "^5.4.0"
|
|
126
130
|
},
|
|
127
131
|
"peerDependencies": {
|
package/schema.json
CHANGED
|
@@ -310,6 +310,16 @@
|
|
|
310
310
|
"markdownDescription": "Whether the HTML comments of templates render, as `@vitejs/plugin-vue`'s `template.compilerOptions.comments`: by default as Vue decides it, kept when `isProduction` is `false` and left out when it is `true`.",
|
|
311
311
|
"type": "boolean"
|
|
312
312
|
},
|
|
313
|
+
"whitespace": {
|
|
314
|
+
"description": "How the text of templates is spaced: `\"condense\"` (the default) as Vue spaces it, or `\"trim-edges\"`, which also trims the whitespace where text meets its element's tags. The client compiles the same way with `trimEdges` from `ferrovue/whitespace` in `@vitejs/plugin-vue`'s `template.compilerOptions.nodeTransforms`.",
|
|
315
|
+
"markdownDescription": "How the text of templates is spaced: `\"condense\"` (the default) as Vue spaces it, or `\"trim-edges\"`, which also trims the whitespace where text meets its element's tags. The client compiles the same way with `trimEdges` from `ferrovue/whitespace` in `@vitejs/plugin-vue`'s `template.compilerOptions.nodeTransforms`.",
|
|
316
|
+
"type": "string",
|
|
317
|
+
"enum": [
|
|
318
|
+
"condense",
|
|
319
|
+
"trim-edges"
|
|
320
|
+
],
|
|
321
|
+
"default": "condense"
|
|
322
|
+
},
|
|
313
323
|
"cssModules": {
|
|
314
324
|
"description": "CSS modules (`<style module>`): how Vite's `css.modules` names their classes, which ferrovue computes as postcss-modules does. Without it, `<style module>` is refused.",
|
|
315
325
|
"markdownDescription": "CSS modules (`<style module>`): how Vite's `css.modules` names their classes, which ferrovue computes as postcss-modules does. Without it, `<style module>` is refused.",
|