ferrovue 0.2.0 → 0.3.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 +73 -4
- package/dist/attrs.d.ts +21 -5
- package/dist/attrs.js +305 -24
- package/dist/cli.js +131 -20
- package/dist/client.d.ts +11 -2
- package/dist/client.js +56 -8
- package/dist/compiler.d.ts +4 -0
- package/dist/compiler.js +17 -5
- package/dist/component.js +13 -0
- package/dist/context.d.ts +2 -2
- package/dist/context.js +23 -5
- package/dist/diff.d.ts +4 -0
- package/dist/diff.js +77 -0
- package/dist/expr.js +39 -1
- package/dist/islands.d.ts +5 -0
- package/dist/islands.js +4 -0
- package/dist/model.d.ts +21 -0
- package/dist/model.js +13 -0
- package/dist/router.js +52 -2
- package/dist/rust.d.ts +3 -0
- package/dist/rust.js +37 -10
- package/dist/scoped.d.ts +15 -1
- package/dist/scoped.js +88 -1
- package/dist/script.js +18 -2
- package/dist/stores.js +2 -0
- package/dist/template.d.ts +8 -5
- package/dist/template.js +131 -58
- package/dist/vite.js +45 -2
- package/package.json +5 -1
package/dist/model.d.ts
CHANGED
|
@@ -109,6 +109,12 @@ export declare const RUST_KEYWORDS: Set<string>;
|
|
|
109
109
|
/** Types the generated code names, which an interface of the same name would shadow. */
|
|
110
110
|
export declare const RUST_PRELUDE: Set<string>;
|
|
111
111
|
export declare function snake(js: string): string;
|
|
112
|
+
/** `camelize` in `@vue/shared`: how Vue matches an attribute a parent passes to a declared prop. */
|
|
113
|
+
export declare function camelize(key: string): string;
|
|
114
|
+
/** Whether a component declares the prop a parent passes as `key`, as `setFullProps` decides. */
|
|
115
|
+
export declare function declares(comp: Component, key: string): boolean;
|
|
116
|
+
/** Whether a component may be passed attributes beyond its props, and so takes `fv::Attrs`. */
|
|
117
|
+
export declare function takesAttrs(comp: Component): boolean;
|
|
112
118
|
export declare function rustStr(s: string): string;
|
|
113
119
|
/** One character as a Rust `char` literal. */
|
|
114
120
|
export declare function rustChar(ch: string): string;
|
|
@@ -129,6 +135,9 @@ export interface Component {
|
|
|
129
135
|
line: number;
|
|
130
136
|
column: number;
|
|
131
137
|
};
|
|
138
|
+
/** A root `<Transition>` or `<KeepAlive>` around anything but one plain element — a `v-if`, a
|
|
139
|
+
* `<template>` — whose content Vue's server gives no attributes and its client does. */
|
|
140
|
+
attrsDropped?: N;
|
|
132
141
|
props: Struct;
|
|
133
142
|
structs: Map<string, Struct>;
|
|
134
143
|
/** The local name `TrustedHtml` was imported under from `ferrovue/types`, if it was. */
|
|
@@ -183,6 +192,13 @@ export interface Component {
|
|
|
183
192
|
/** Whether its outlets may pass slot content a slot scope id, which the content then takes as
|
|
184
193
|
* `fv_sid`. Known once every component is read, as `inherits` is. */
|
|
185
194
|
passesSlotIds: boolean;
|
|
195
|
+
/** The attributes a parent may pass it beyond its props, by name: what falls through to its root,
|
|
196
|
+
* or to the element that binds `$attrs`. With any, its `render_scoped` takes them as
|
|
197
|
+
* `fv::Attrs`. Known once every component is read (`attrsFlow`). */
|
|
198
|
+
attrNames: Set<string>;
|
|
199
|
+
/** Whether its `$attrs` may hold scope ids: it is the root of a component that may be handed
|
|
200
|
+
* some, which passes them on as attributes. */
|
|
201
|
+
idsInAttrs: boolean;
|
|
186
202
|
}
|
|
187
203
|
export interface Scope {
|
|
188
204
|
comp: Component;
|
|
@@ -229,6 +245,11 @@ export interface Scope {
|
|
|
229
245
|
/** The Rust value of `_attrs`, the scope ids this render's root inherits, as `ssrRenderAttrs`
|
|
230
246
|
* writes them; `null` when the component inherits none. */
|
|
231
247
|
attrs: string | null;
|
|
248
|
+
/** The Rust `fv::Attrs` a parent passes this render beyond its props — `$attrs`, and what
|
|
249
|
+
* `_attrs` holds before the scope ids — or `null` when no parent passes any. */
|
|
250
|
+
fallthrough: string | null;
|
|
251
|
+
/** Setup bindings holding `useAttrs()`, which the template may bind as `$attrs`. */
|
|
252
|
+
attrsBindings: Set<string>;
|
|
232
253
|
/** The Rust value of `_scopeId` inside slot content: the slot scope id the content is given, or
|
|
233
254
|
* `null` where it is always empty. */
|
|
234
255
|
sid: string | null;
|
package/dist/model.js
CHANGED
|
@@ -12,6 +12,19 @@ export function snake(js) {
|
|
|
12
12
|
const s = js.replace(/[A-Z]/g, (c) => "_" + c.toLowerCase()).replace(/^_/, "");
|
|
13
13
|
return RUST_KEYWORDS.has(s) ? `r#${s}` : s;
|
|
14
14
|
}
|
|
15
|
+
/** `camelize` in `@vue/shared`: how Vue matches an attribute a parent passes to a declared prop. */
|
|
16
|
+
export function camelize(key) {
|
|
17
|
+
return key.replace(/-(\w)/g, (_, c) => c.toUpperCase());
|
|
18
|
+
}
|
|
19
|
+
/** Whether a component declares the prop a parent passes as `key`, as `setFullProps` decides. */
|
|
20
|
+
export function declares(comp, key) {
|
|
21
|
+
const camel = camelize(key);
|
|
22
|
+
return comp.props.fields.some((f) => camelize(f.js) === camel);
|
|
23
|
+
}
|
|
24
|
+
/** Whether a component may be passed attributes beyond its props, and so takes `fv::Attrs`. */
|
|
25
|
+
export function takesAttrs(comp) {
|
|
26
|
+
return comp.attrNames.size > 0;
|
|
27
|
+
}
|
|
15
28
|
export function rustStr(s) {
|
|
16
29
|
let out = '"';
|
|
17
30
|
for (const ch of s) {
|
package/dist/router.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
import { escapeHtml } from "@vue/shared";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { fail, GenError, rustStr } from "./model.js";
|
|
5
|
+
import { camelize, fail, GenError, rustStr } from "./model.js";
|
|
6
6
|
import { allRoutes, CONFIG_FILE, ctx } from "./context.js";
|
|
7
7
|
import { expr, lonely } from "./expr.js";
|
|
8
8
|
import { bare, condition, strArg } from "./parens.js";
|
|
9
9
|
import { Emitter } from "./emitter.js";
|
|
10
|
-
import { classItems, IGNORED_PROPS, mergeProps, renderDynamicAttr, renderStyle } from "./attrs.js";
|
|
10
|
+
import { attrOf, classItems, dollarAttrs, IGNORED_PROPS, isAttrs, mergedParts, mergeProps, renderDynamicAttr, renderStyle } from "./attrs.js";
|
|
11
11
|
import { slotBody, statements } from "./template.js";
|
|
12
12
|
export const ROUTER_LINK_PROPS = new Set(["to", "class", "activeClass", "exactActiveClass", "ariaCurrentValue"]);
|
|
13
13
|
/** `<RouterLink>` props that change only what a click does, which the server does not render. */
|
|
@@ -202,6 +202,22 @@ export function routerLink(s, e, n) {
|
|
|
202
202
|
e.open("");
|
|
203
203
|
resolveLink(s, e, to);
|
|
204
204
|
e.lit("<a");
|
|
205
|
+
/* Attributes this component is passed fall through to the link: merged into its virtual node's
|
|
206
|
+
* props with the template's, then onto the `<a>` vue-router renders. */
|
|
207
|
+
const parts = mergedParts(rawProps);
|
|
208
|
+
const passedOn = (p) => s.fallthrough !== null && ((isAttrs(p) && s.comp.inheritAttrs) || dollarAttrs(s, p));
|
|
209
|
+
const fallsThrough = parts.some(passedOn);
|
|
210
|
+
if (fallsThrough) {
|
|
211
|
+
for (const name of s.comp.attrNames) {
|
|
212
|
+
const key = camelize(name);
|
|
213
|
+
if ((ROUTER_LINK_PROPS.has(key) && key !== "class") || ROUTER_LINK_INERT.has(key) || key === "custom" || key === "viewTransition") {
|
|
214
|
+
fail(s.comp, `\`${name}\`, an attribute ${s.comp.name} may be passed, would reach its \`<RouterLink>\` as a prop`, n);
|
|
215
|
+
}
|
|
216
|
+
if (key === "href" || key === "ariaCurrent")
|
|
217
|
+
fail(s.comp, `\`${name}\`, an attribute ${s.comp.name} may be passed, would replace its \`<RouterLink>\`'s own`, n);
|
|
218
|
+
}
|
|
219
|
+
e.open(`if ${s.fallthrough}.is_empty()`);
|
|
220
|
+
}
|
|
205
221
|
e.open("if fv_link.exact");
|
|
206
222
|
e.lit(` aria-current="${escapeHtml(ariaCurrent)}"`);
|
|
207
223
|
e.close();
|
|
@@ -233,6 +249,40 @@ export function routerLink(s, e, n) {
|
|
|
233
249
|
}
|
|
234
250
|
else
|
|
235
251
|
e.stmt(`out.push_str(&fv::scope_attrs(${base ?? '""'}, ${scopeId === null ? '""' : rustStr(scopeId)}, ${slotted ?? '""'}));`);
|
|
252
|
+
if (fallsThrough) {
|
|
253
|
+
e.close(" else {");
|
|
254
|
+
// The template's attributes for the link, merged in their order with those passed on.
|
|
255
|
+
const sources = [];
|
|
256
|
+
for (const p of parts) {
|
|
257
|
+
if (passedOn(p))
|
|
258
|
+
sources.push(`${s.fallthrough}.list()`);
|
|
259
|
+
else if (p.type === "ObjectExpression") {
|
|
260
|
+
const entries = p.properties
|
|
261
|
+
.filter((q) => {
|
|
262
|
+
const raw = q.key?.type === "Identifier" ? q.key.name : String(q.key?.value);
|
|
263
|
+
const key = camelize(raw);
|
|
264
|
+
return key === "class" || (!ROUTER_LINK_PROPS.has(key) && !ROUTER_LINK_INERT.has(key) && !/^on[^a-z]/.test(raw) && !IGNORED_PROPS.has(raw));
|
|
265
|
+
})
|
|
266
|
+
.map((q) => {
|
|
267
|
+
const raw = q.key.type === "Identifier" ? q.key.name : String(q.key.value);
|
|
268
|
+
return `(${rustStr(raw)}, ${attrOf(s, raw, q.value, "vnode")})`;
|
|
269
|
+
});
|
|
270
|
+
if (entries.length)
|
|
271
|
+
sources.push(`&[${entries.join(", ")}]`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const ids = base === null && slotted === null
|
|
275
|
+
? rustStr(scopeId === null ? "" : ` ${scopeId}`)
|
|
276
|
+
: `&fv::scope_attrs(${base ?? '""'}, ${scopeId === null ? '""' : rustStr(scopeId)}, ${slotted ?? '""'})`;
|
|
277
|
+
// vue-router's own: `aria-current` (`null` when the link is not exact), `href`, its classes.
|
|
278
|
+
const linkOwn = [
|
|
279
|
+
`("aria-current", if fv_link.exact { fv::Attr::str(${rustStr(ariaCurrent)}) } else { fv::Attr::Undefined })`,
|
|
280
|
+
'("href", fv::Attr::str(&fv_link.href))',
|
|
281
|
+
`("class", fv::Attr::from(fv::class_names(&[${linkItems.join(", ")}])))`,
|
|
282
|
+
];
|
|
283
|
+
e.stmt(`fv::attrs_into(out, &[&[${linkOwn.join(", ")}], &fv::merge_props(&[${sources.join(", ")}])], 1, ${ids});`);
|
|
284
|
+
e.close();
|
|
285
|
+
}
|
|
236
286
|
e.lit(">");
|
|
237
287
|
if (slots && slots.type !== "NullLiteral") {
|
|
238
288
|
if (slots.type !== "ObjectExpression")
|
package/dist/rust.d.ts
CHANGED
|
@@ -7,6 +7,9 @@ export declare function structSource(st: Struct, comp: Component, doc: string):
|
|
|
7
7
|
export declare function header(source: string, edit?: string): string;
|
|
8
8
|
/** Rust expressions summing the lengths of the strings a value of type `ty` at `place` holds. */
|
|
9
9
|
export declare function textLen(comp: Component, place: string, ty: Ty, seen?: Set<string>): string[];
|
|
10
|
+
/** Whether the component gets an `island()`: it renders from its props alone, which `data-props` is
|
|
11
|
+
* all the client gets to mount it with. */
|
|
12
|
+
export declare function isIsland(comp: Component): boolean;
|
|
10
13
|
export declare function componentSource(comp: Component, ast: N[], ssr: string, components: Map<string, Component>): string;
|
|
11
14
|
export declare function modSource(comps: Component[]): string;
|
|
12
15
|
export declare function storesSource(dir: string): string;
|
package/dist/rust.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* The Rust source written for each component, and the modules beside them. */
|
|
2
2
|
import { parse as parseJs } from "@babel/parser";
|
|
3
3
|
import { basename } from "node:path";
|
|
4
|
-
import { fail, GenError, rustStr, snake, tagAst } from "./model.js";
|
|
4
|
+
import { fail, GenError, rustStr, snake, tagAst, takesAttrs } from "./model.js";
|
|
5
5
|
import { allRoutes, ctx } from "./context.js";
|
|
6
6
|
import { lookupStruct } from "./typescript.js";
|
|
7
7
|
import { childOf } from "./expr.js";
|
|
@@ -179,27 +179,40 @@ export function textLen(comp, place, ty, seen = new Set()) {
|
|
|
179
179
|
return [];
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
|
-
/** `render`, and for a component a parent may hand scope ids
|
|
183
|
-
* last, as `ssrRenderAttrs` writes them onto its root; `render`
|
|
182
|
+
/** `render`, and for a component a parent may hand scope ids or pass attributes to,
|
|
183
|
+
* `render_scoped`, which takes them last, as `ssrRenderAttrs` writes them onto its root; `render`
|
|
184
|
+
* hands it none. */
|
|
184
185
|
function renderSource(comp, life, args, e) {
|
|
185
186
|
const doc = "/// Write the component's server render into `out`.\n";
|
|
186
187
|
// Props or ids the render never reads, as when a component only passes its slot on.
|
|
187
188
|
const props = e.reads("props", 0) ? "props" : "_props";
|
|
188
|
-
if (!comp.inherits)
|
|
189
|
+
if (!comp.inherits && !takesAttrs(comp))
|
|
189
190
|
return `${doc}pub fn render(out: &mut String, ${props}: &Props${life}${extraParams(comp)}) {\n${e.lines.join("\n")}\n}`;
|
|
190
191
|
// A root that is a fragment, or a `<Teleport>`, takes no ids.
|
|
191
192
|
const attrs = e.reads("fv_attrs", 0) ? "fv_attrs" : "_fv_attrs";
|
|
193
|
+
// A component a parent may pass attributes takes them with the ids, as one `fv::Attrs`.
|
|
194
|
+
const [none, ty, what] = takesAttrs(comp)
|
|
195
|
+
? ["&fv::Attrs::NONE", "&fv::Attrs<'_>", "the attributes a parent passes beyond the props, and the scope ids it hands the root"]
|
|
196
|
+
: ['""', "&str", "the scope ids a parent hands the root: ` data-v-…` each"];
|
|
192
197
|
return `${doc}pub fn render(out: &mut String, props: &Props${life}${extraParams(comp)}) {
|
|
193
|
-
render_scoped(out, props${args},
|
|
198
|
+
render_scoped(out, props${args}, ${none});
|
|
194
199
|
}
|
|
195
200
|
|
|
196
|
-
/// [\`render\`], with
|
|
201
|
+
/// [\`render\`], with ${what}.
|
|
197
202
|
#[doc(hidden)]
|
|
198
|
-
pub fn render_scoped(out: &mut String, ${props}: &Props${life}${extraParams(comp)}, ${attrs}:
|
|
203
|
+
pub fn render_scoped(out: &mut String, ${props}: &Props${life}${extraParams(comp)}, ${attrs}: ${ty}) {
|
|
199
204
|
${e.lines.join("\n")}
|
|
200
205
|
}`;
|
|
201
206
|
}
|
|
207
|
+
/** Whether the component gets an `island()`: it renders from its props alone, which `data-props` is
|
|
208
|
+
* all the client gets to mount it with. */
|
|
209
|
+
export function isIsland(comp) {
|
|
210
|
+
return !takesSlots(comp) && !comp.usesRoute && !comp.usesStores && !comp.usesI18n && !comp.usesTeleports;
|
|
211
|
+
}
|
|
202
212
|
export function componentSource(comp, ast, ssr, components) {
|
|
213
|
+
if (takesAttrs(comp) && comp.inheritAttrs && comp.attrsDropped) {
|
|
214
|
+
fail(comp, `a root \`<Transition>\` or \`<KeepAlive>\` around a \`v-if\` in ${comp.name}, which a parent passes attributes: Vue's server drops them, where its client puts them on the element; set \`inheritAttrs: false\` and bind \`$attrs\` on the element`, comp.attrsDropped);
|
|
215
|
+
}
|
|
203
216
|
const { scope, lets } = scopeFor(comp, ast, components);
|
|
204
217
|
const program = parseJs(ssr, { sourceType: "module" }).program;
|
|
205
218
|
tagAst(program, "template");
|
|
@@ -229,6 +242,9 @@ export function componentSource(comp, ast, ssr, components) {
|
|
|
229
242
|
const life = structLifetime(comp.props, comp) ? "<'_>" : "";
|
|
230
243
|
const gen = life ? "<'p, 'a>" : "<'p>";
|
|
231
244
|
const named = life ? "<'a>" : "";
|
|
245
|
+
// What holds its props borrows only what they borrow.
|
|
246
|
+
const owned = life ? "<'a>" : "";
|
|
247
|
+
const ownedLife = life ? "'a" : "'static";
|
|
232
248
|
// An `interface Props` that `defineProps` takes is the props struct itself, written once below.
|
|
233
249
|
const structs = [...comp.structs.values()]
|
|
234
250
|
.filter((st) => st.name !== "Props" && !st.slot)
|
|
@@ -237,7 +253,7 @@ export function componentSource(comp, ast, ssr, components) {
|
|
|
237
253
|
// `Cow` is imported only where a field is one: a lifetime that comes from another component's props
|
|
238
254
|
// alone borrows through that type, not through a `Cow` written here.
|
|
239
255
|
const usesCow = /Cow</.test(structs + structSource(comp.props, comp, ""));
|
|
240
|
-
const plain =
|
|
256
|
+
const plain = isIsland(comp);
|
|
241
257
|
const slotFields = [
|
|
242
258
|
...comp.slotNames.map((n) => {
|
|
243
259
|
const outlet = `\`<slot${n === "default" ? "" : ` name="${n}"`}>\``;
|
|
@@ -288,6 +304,16 @@ pub fn html${gen}(props: &'p Props${named}) -> fv::Html<'p, Props${named}> {
|
|
|
288
304
|
pub fn island${gen}(props: &'p Props${named}) -> fv::Html<'p, Props${named}> {
|
|
289
305
|
fv::Html::island(NAME, props, render)
|
|
290
306
|
}
|
|
307
|
+
|
|
308
|
+
/// [\`html\`], holding the props: a handler that builds them can return it.
|
|
309
|
+
pub fn into_html${owned}(props: Props${named}) -> fv::Html<${ownedLife}, Props${named}> {
|
|
310
|
+
fv::Html::markup_owned(props, render)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/// [\`island\`], holding the props.
|
|
314
|
+
pub fn into_island${owned}(props: Props${named}) -> fv::Html<${ownedLife}, Props${named}> {
|
|
315
|
+
fv::Html::island_owned(NAME, props, render)
|
|
316
|
+
}
|
|
291
317
|
`
|
|
292
318
|
: `/// The component's markup, for a maud page that shows it.
|
|
293
319
|
pub fn html${gen}(props: &'p Props${named}${params}) -> fv::Html<'p, Props${named}, impl Fn(&mut String, &Props${named}) + 'p> {
|
|
@@ -393,8 +419,9 @@ impl Fixture {
|
|
|
393
419
|
//! The component renderers, one module per \`.vue\` file.
|
|
394
420
|
|
|
395
421
|
// The modules pass rustc's default warnings and clippy's default lints, with one exception:
|
|
396
|
-
// \`dead_code\`. Every component gets the whole of its API (\`render\`, \`html\`, \`island\`,
|
|
397
|
-
// constructor and a setter per optional prop) and an app calls only what
|
|
422
|
+
// \`dead_code\`. Every component gets the whole of its API (\`render\`, \`html\`, \`island\`, their
|
|
423
|
+
// \`into_\` forms, \`NAME\`, a constructor and a setter per optional prop) and an app calls only what
|
|
424
|
+
// it needs.
|
|
398
425
|
#![allow(dead_code)]
|
|
399
426
|
|
|
400
427
|
${comps.map((c) => `pub mod ${c.module};`).join("\n")}${ctx.routes ? "\npub mod route_table;" : ""}${ctx.stores.size ? "\npub mod stores;" : ""}${ctx.typeStructs.size ? "\npub mod types;" : ""}${ctx.i18n ? "\npub mod i18n;" : ""}
|
package/dist/scoped.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Component, type N } from "./model.ts";
|
|
2
2
|
/** Set every component's `inherits` and `passesSlotIds`, each a least fixed point: a component's
|
|
3
3
|
* slot content may be given an id when one of its outlets passes one, or forwards what the slot
|
|
4
4
|
* content it sits in was given; a root may be handed ids by a scoped parent, by a parent that
|
|
@@ -8,3 +8,17 @@ export declare function scopeFlow(read: {
|
|
|
8
8
|
ssr: string;
|
|
9
9
|
children: Map<string, string>;
|
|
10
10
|
}[]): void;
|
|
11
|
+
/** Whether `n` is the template's `$attrs`: `_ctx.$attrs`, or a setup binding of `useAttrs()`. */
|
|
12
|
+
export declare function isDollarAttrs(n: N, bindings: Set<string>): boolean;
|
|
13
|
+
/** Whether a key a parent passes a child reaches it as an attribute, unless it is a prop: not one
|
|
14
|
+
* Vue reserves, not a listener (which the server drops), not a model's modifiers. */
|
|
15
|
+
export declare function passedKey(child: Component, key: string): boolean;
|
|
16
|
+
/** Set every component's `attrNames` and `idsInAttrs`: the attributes its parents' calls pass it
|
|
17
|
+
* beyond its props, and those passed on to it — by a parent whose root it is, or that binds
|
|
18
|
+
* `$attrs` to it — from that parent's own, a least fixed point. Runs after `scopeFlow`. */
|
|
19
|
+
export declare function attrsFlow(read: {
|
|
20
|
+
comp: Component;
|
|
21
|
+
ssr: string;
|
|
22
|
+
children: Map<string, string>;
|
|
23
|
+
attrsBindings: Set<string>;
|
|
24
|
+
}[]): void;
|
package/dist/scoped.js
CHANGED
|
@@ -8,8 +8,13 @@
|
|
|
8
8
|
* child's `_attrs`: the id of the component that created it, what its parent passes on when the
|
|
9
9
|
* child is that parent's root, and the slot scope ids of the slot content it is rendered in
|
|
10
10
|
* (`renderComponentSubTree`). A component with `:slotted()` styles gives its slot content
|
|
11
|
-
* `data-v-…-s` as well, which the content writes onto its elements as `_scopeId`.
|
|
11
|
+
* `data-v-…-s` as well, which the content writes onto its elements as `_scopeId`.
|
|
12
|
+
*
|
|
13
|
+
* The same `_attrs` carries the attributes a parent passes a child beyond its props, ahead of the
|
|
14
|
+
* ids: which components may be passed some (`attrsFlow`) decides whether `fv_attrs` is an
|
|
15
|
+
* `fv::Attrs` holding both, or the ids alone. */
|
|
12
16
|
import { parse as parseJs } from "@babel/parser";
|
|
17
|
+
import { declares } from "./model.js";
|
|
13
18
|
/** The child components and outlets in one component's compiled template. */
|
|
14
19
|
function flowOf(comp, ssr, children) {
|
|
15
20
|
const calls = [];
|
|
@@ -88,3 +93,85 @@ export function scopeFlow(read) {
|
|
|
88
93
|
}
|
|
89
94
|
}
|
|
90
95
|
}
|
|
96
|
+
/** Whether `n` is the template's `$attrs`: `_ctx.$attrs`, or a setup binding of `useAttrs()`. */
|
|
97
|
+
export function isDollarAttrs(n, bindings) {
|
|
98
|
+
if (n?.type !== "MemberExpression" || n.object.type !== "Identifier")
|
|
99
|
+
return false;
|
|
100
|
+
const prop = n.computed ? n.property.value : n.property.name;
|
|
101
|
+
if (n.object.name === "_ctx")
|
|
102
|
+
return prop === "$attrs";
|
|
103
|
+
return n.object.name === "$setup" && prop !== undefined && bindings.has(prop);
|
|
104
|
+
}
|
|
105
|
+
/** Whether a key a parent passes a child reaches it as an attribute, unless it is a prop: not one
|
|
106
|
+
* Vue reserves, not a listener (which the server drops), not a model's modifiers. */
|
|
107
|
+
export function passedKey(child, key) {
|
|
108
|
+
if (["", "key", "ref", "ref_for", "ref_key", "innerHTML", "textContent"].includes(key) || /^on[^a-z]/.test(key))
|
|
109
|
+
return false;
|
|
110
|
+
return !(key.endsWith("Modifiers") && declares(child, key === "modelModifiers" ? "modelValue" : key.slice(0, -"Modifiers".length)));
|
|
111
|
+
}
|
|
112
|
+
/** Set every component's `attrNames` and `idsInAttrs`: the attributes its parents' calls pass it
|
|
113
|
+
* beyond its props, and those passed on to it — by a parent whose root it is, or that binds
|
|
114
|
+
* `$attrs` to it — from that parent's own, a least fixed point. Runs after `scopeFlow`. */
|
|
115
|
+
export function attrsFlow(read) {
|
|
116
|
+
const byName = new Map(read.map((r) => [r.comp.name, r.comp]));
|
|
117
|
+
const flows = read.map((r) => {
|
|
118
|
+
const calls = [];
|
|
119
|
+
const self = new Set();
|
|
120
|
+
const walk = (n) => {
|
|
121
|
+
if (!n || typeof n !== "object")
|
|
122
|
+
return;
|
|
123
|
+
if (Array.isArray(n)) {
|
|
124
|
+
n.forEach(walk);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (n.type === "VariableDeclarator" && n.init?.type === "CallExpression" && n.init.callee.name === "_resolveComponent") {
|
|
128
|
+
if (n.init.arguments[0]?.value === r.comp.name && n.init.arguments[1])
|
|
129
|
+
self.add(n.id.name);
|
|
130
|
+
}
|
|
131
|
+
if (n.type === "CallExpression" && n.callee.type === "Identifier" && n.callee.name === "_ssrRenderComponent") {
|
|
132
|
+
const target = n.arguments[0];
|
|
133
|
+
const child = target?.type === "MemberExpression" && target.object.name === "$setup"
|
|
134
|
+
? r.children.get(target.computed ? target.property.value : target.property.name)
|
|
135
|
+
: target?.type === "Identifier" && self.has(target.name)
|
|
136
|
+
? r.comp.name
|
|
137
|
+
: undefined;
|
|
138
|
+
const props = n.arguments[1];
|
|
139
|
+
const parts = props?.type === "CallExpression" && props.callee.name === "_mergeProps" ? props.arguments : props ? [props] : [];
|
|
140
|
+
if (child) {
|
|
141
|
+
calls.push({
|
|
142
|
+
child,
|
|
143
|
+
keys: parts.filter((p) => p.type === "ObjectExpression").flatMap((p) => p.properties.filter((q) => q.type === "ObjectProperty" && !q.computed).map((q) => q.key.name ?? String(q.key.value))),
|
|
144
|
+
passesAttrs: parts.some((p) => p.type === "Identifier" && p.name === "_attrs"),
|
|
145
|
+
passesDollar: parts.some((p) => isDollarAttrs(p, r.attrsBindings)),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const [key, v] of Object.entries(n))
|
|
150
|
+
if (key !== "loc" && key !== "start" && key !== "end")
|
|
151
|
+
walk(v);
|
|
152
|
+
};
|
|
153
|
+
walk(parseJs(r.ssr, { sourceType: "module" }).program);
|
|
154
|
+
return { comp: r.comp, calls };
|
|
155
|
+
});
|
|
156
|
+
for (let changed = true; changed;) {
|
|
157
|
+
changed = false;
|
|
158
|
+
for (const f of flows) {
|
|
159
|
+
for (const c of f.calls) {
|
|
160
|
+
const child = byName.get(c.child);
|
|
161
|
+
if (!child)
|
|
162
|
+
continue;
|
|
163
|
+
const names = c.keys.filter((k) => passedKey(child, k));
|
|
164
|
+
if ((c.passesAttrs && f.comp.inheritAttrs) || c.passesDollar)
|
|
165
|
+
names.push(...f.comp.attrNames);
|
|
166
|
+
for (const name of names) {
|
|
167
|
+
if (declares(child, name) || child.attrNames.has(name))
|
|
168
|
+
continue;
|
|
169
|
+
child.attrNames.add(name);
|
|
170
|
+
changed = true;
|
|
171
|
+
}
|
|
172
|
+
if (c.passesAttrs && f.comp.inherits && !child.idsInAttrs)
|
|
173
|
+
child.idsInAttrs = changed = true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
package/dist/script.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* `<script setup>` read: what the server evaluates, and what stays on the client. */
|
|
2
2
|
import { basename } from "node:path";
|
|
3
|
-
import { fail, GenError, snake, STR } from "./model.js";
|
|
3
|
+
import { fail, GenError, snake, STR, takesAttrs } from "./model.js";
|
|
4
4
|
import { CONFIG_FILE, ctx } from "./context.js";
|
|
5
5
|
import { definePropsType } from "./typescript.js";
|
|
6
6
|
import { expr, fieldVal, storeGetter, theRoute } from "./expr.js";
|
|
@@ -92,7 +92,10 @@ export function scopeFor(comp, ast, components) {
|
|
|
92
92
|
i18nT: new Set(),
|
|
93
93
|
fill: false,
|
|
94
94
|
vnode: false,
|
|
95
|
-
|
|
95
|
+
// A component a parent may pass attributes takes them with the scope ids, as one `fv::Attrs`.
|
|
96
|
+
attrs: comp.inherits ? (takesAttrs(comp) ? "fv_attrs.ids()" : "fv_attrs") : null,
|
|
97
|
+
fallthrough: takesAttrs(comp) ? "fv_attrs" : null,
|
|
98
|
+
attrsBindings: new Set(),
|
|
96
99
|
sid: null,
|
|
97
100
|
};
|
|
98
101
|
const lets = [];
|
|
@@ -100,6 +103,7 @@ export function scopeFor(comp, ast, components) {
|
|
|
100
103
|
let storeToRefsName = null;
|
|
101
104
|
let useRouteName = null;
|
|
102
105
|
let useI18nName = null;
|
|
106
|
+
let useAttrsName = null;
|
|
103
107
|
/** Setup bindings holding a store, by name. */
|
|
104
108
|
const storeValues = new Map();
|
|
105
109
|
for (const st of ast) {
|
|
@@ -142,6 +146,12 @@ export function scopeFor(comp, ast, components) {
|
|
|
142
146
|
scope.clientOnly.set(sp.local.name, `\`${name}\` from vue-i18n does not run on the server`);
|
|
143
147
|
}
|
|
144
148
|
}
|
|
149
|
+
else if (from === "vue") {
|
|
150
|
+
for (const sp of st.specifiers) {
|
|
151
|
+
if (sp.type === "ImportSpecifier" && (sp.imported.name ?? sp.imported.value) === "useAttrs")
|
|
152
|
+
useAttrsName = sp.local.name;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
145
155
|
else if (from === "pinia") {
|
|
146
156
|
for (const sp of st.specifiers) {
|
|
147
157
|
const name = sp.type === "ImportSpecifier" ? (sp.imported.name ?? sp.imported.value) : null;
|
|
@@ -252,6 +262,12 @@ export function scopeFor(comp, ast, components) {
|
|
|
252
262
|
scope.setup.set(local, theRoute(scope, d));
|
|
253
263
|
continue;
|
|
254
264
|
}
|
|
265
|
+
// `const attrs = useAttrs()`: `$attrs`, which the template may bind whole.
|
|
266
|
+
if (useAttrsName !== null && init0?.type === "CallExpression" && init0.callee.type === "Identifier" && init0.callee.name === useAttrsName) {
|
|
267
|
+
scope.attrsBindings.add(local);
|
|
268
|
+
scope.clientOnly.set(local, "`useAttrs()` is bound whole, with `v-bind`; a value read from it has no type");
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
255
271
|
// `const model = defineModel<string>()`: the model's prop.
|
|
256
272
|
const model = comp.models.get(local);
|
|
257
273
|
if (model !== undefined) {
|
package/dist/stores.js
CHANGED
package/dist/template.d.ts
CHANGED
|
@@ -4,8 +4,11 @@ import { Emitter } from "./emitter.ts";
|
|
|
4
4
|
export declare function slot(s: Scope, e: Emitter, n: N): void;
|
|
5
5
|
/** `isComment` in `@vue/server-renderer`: a chunk that is only comments and whitespace. */
|
|
6
6
|
export declare function isComment(text: string): boolean;
|
|
7
|
-
/** Whether a `_push` argument is content to `ssrRenderSlot
|
|
8
|
-
|
|
7
|
+
/** Whether a `_push` argument is content to `ssrRenderSlot` rather than only comments — `"run"`
|
|
8
|
+
* when that depends on the values it interpolates (`${of1}<!--[-->` is a comment when `of1` writes
|
|
9
|
+
* nothing). Vue asks it of each pushed string, so the generated code asks `fv::is_comment` of what
|
|
10
|
+
* that push wrote. */
|
|
11
|
+
export declare function pushesContent(n: N): boolean | "run";
|
|
9
12
|
export declare function push(s: Scope, e: Emitter, n: N): void;
|
|
10
13
|
export declare function renderChild(s: Scope, e: Emitter, n: N): void;
|
|
11
14
|
/** `name: value` in a struct literal, or the name alone when the value is a variable of that name. */
|
|
@@ -15,8 +18,8 @@ export declare function takesSlots(c: Component): boolean;
|
|
|
15
18
|
/** The arguments after `props` that a component's `render` takes. */
|
|
16
19
|
export declare function extraParams(c: Component): string;
|
|
17
20
|
/** `render(out, props[, slots][, route])` for a child, with the slot content this template gives
|
|
18
|
-
* it as closures; `render_scoped`, with the scope ids its root is handed last
|
|
19
|
-
* be handed some. */
|
|
21
|
+
* it as closures; `render_scoped`, with the scope ids its root is handed last — and the attributes
|
|
22
|
+
* it is passed, as `fv::Attrs` — for a child that may be handed some. */
|
|
20
23
|
export declare function callChild(s: Scope, e: Emitter, child: Component, propsCode: string, slots: N, attrs: string | null): void;
|
|
21
24
|
/** The statements a parent's slot content pushes: the `if (_push)` half of its `_withCtx`. */
|
|
22
25
|
export declare function slotBody(s: Scope, value: N): N[];
|
|
@@ -28,7 +31,7 @@ export declare function slotContent(s: Scope, value: N): {
|
|
|
28
31
|
};
|
|
29
32
|
/** Whether slot content always pushes something that is not a comment: a push of content outside
|
|
30
33
|
* any `if` or loop. Then nothing has to be decided at run time. */
|
|
31
|
-
export declare function staticallyFilled(
|
|
34
|
+
export declare function staticallyFilled(body: N[]): boolean;
|
|
32
35
|
/** `RowSlotProps`, `RowSlot`: the names of a scoped slot's props struct and content type. */
|
|
33
36
|
export declare function slotTypeName(slotName: string, suffix: string): string;
|
|
34
37
|
/** Whether a scoped slot prop's Rust type borrows, and so needs the props' lifetime `'v`. */
|