ferrovue 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +225 -42
- package/dist/attrs.d.ts +3 -53
- package/dist/attrs.js +41 -469
- package/dist/calls.d.ts +4 -0
- package/dist/calls.js +221 -0
- package/dist/children.d.ts +16 -0
- package/dist/children.js +286 -0
- package/dist/classes.d.ts +13 -0
- package/dist/classes.js +176 -0
- package/dist/cli.js +31 -22
- package/dist/client.d.ts +35 -2
- package/dist/client.js +77 -14
- package/dist/compiler.d.ts +1 -1
- package/dist/compiler.js +31 -92
- package/dist/component.d.ts +3 -1
- package/dist/component.js +123 -104
- package/dist/constants.d.ts +42 -0
- package/dist/constants.js +292 -0
- package/dist/context.d.ts +29 -67
- package/dist/context.js +13 -38
- package/dist/diagnostics.d.ts +23 -0
- package/dist/diagnostics.js +30 -0
- package/dist/diff.d.ts +0 -3
- package/dist/diff.js +0 -9
- package/dist/emitter.d.ts +0 -8
- package/dist/emitter.js +0 -12
- package/dist/errors.d.ts +867 -0
- package/dist/errors.js +330 -0
- package/dist/expr.d.ts +1 -70
- package/dist/expr.js +142 -934
- package/dist/fallthrough.d.ts +9 -0
- package/dist/fallthrough.js +78 -0
- package/dist/fixture.d.ts +4 -10
- package/dist/fixture.js +38 -27
- package/dist/hydration.d.ts +15 -0
- package/dist/hydration.js +80 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/islands.d.ts +1 -0
- package/dist/islands.js +1 -0
- package/dist/link-router.d.ts +24 -0
- package/dist/link-router.js +36 -0
- package/dist/lists.d.ts +0 -11
- package/dist/lists.js +34 -60
- package/dist/loops.d.ts +3 -0
- package/dist/loops.js +168 -0
- package/dist/model.d.ts +46 -144
- package/dist/model.js +150 -18
- package/dist/narrowing.d.ts +27 -0
- package/dist/narrowing.js +157 -0
- package/dist/numbers.d.ts +8 -0
- package/dist/numbers.js +73 -0
- package/dist/parens.d.ts +0 -27
- package/dist/parens.js +0 -44
- package/dist/plugin.d.ts +84 -0
- package/dist/plugin.js +26 -0
- package/dist/plugins/client-only.d.ts +8 -0
- package/dist/plugins/client-only.js +107 -0
- package/dist/plugins/i18n.d.ts +23 -0
- package/dist/{i18n.js → plugins/i18n.js} +88 -31
- package/dist/plugins/index.d.ts +2 -0
- package/dist/plugins/index.js +10 -0
- package/dist/plugins/router-link.d.ts +8 -0
- package/dist/{router.js → plugins/router-link.js} +52 -91
- package/dist/plugins/router.d.ts +42 -0
- package/dist/plugins/router.js +256 -0
- package/dist/plugins/scoped.d.ts +12 -0
- package/dist/plugins/scoped.js +125 -0
- package/dist/plugins/shared-types.d.ts +2 -0
- package/dist/plugins/shared-types.js +7 -0
- package/dist/plugins/stores.d.ts +37 -0
- package/dist/plugins/stores.js +379 -0
- package/dist/plugins/teleport.d.ts +5 -0
- package/dist/plugins/teleport.js +55 -0
- package/dist/plugins/twins.d.ts +10 -0
- package/dist/plugins/twins.js +164 -0
- package/dist/routes.d.ts +10 -0
- package/dist/routes.js +6 -0
- package/dist/rust.d.ts +1 -7
- package/dist/rust.js +56 -174
- package/dist/script.d.ts +3 -6
- package/dist/script.js +127 -140
- package/dist/settle.d.ts +6 -0
- package/dist/settle.js +34 -0
- package/dist/slots.d.ts +13 -0
- package/dist/slots.js +160 -0
- package/dist/strings.d.ts +13 -0
- package/dist/strings.js +208 -0
- package/dist/styles.d.ts +14 -0
- package/dist/styles.js +181 -0
- package/dist/template.d.ts +1 -42
- package/dist/template.js +25 -749
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +4 -10
- package/dist/types.d.ts +2 -0
- package/dist/typescript.d.ts +2 -14
- package/dist/typescript.js +53 -59
- package/dist/vite.d.ts +2 -1
- package/dist/vite.js +11 -32
- package/package.json +15 -6
- package/dist/i18n.d.ts +0 -27
- package/dist/router.d.ts +0 -17
- package/dist/scoped.d.ts +0 -24
- package/dist/scoped.js +0 -177
- package/dist/stores.d.ts +0 -6
- package/dist/stores.js +0 -229
package/dist/template.js
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
import { camelize, declares, fail, GenError, INT, rustStr, sameTy, snake, STR, takesAttrs } from "./model.js";
|
|
1
|
+
import { fail } from "./model.js";
|
|
3
2
|
import { ctx } from "./context.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { expr } from "./expr.js";
|
|
4
|
+
import { boolOf, cond, known, narrowTo, presence, truthy } from "./narrowing.js";
|
|
5
|
+
import { CMP, condition, occurrences, operand } from "./parens.js";
|
|
7
6
|
import { Emitter } from "./emitter.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
7
|
+
import { interpolate, renderAttr, renderAttrs, renderDynamicAttr } from "./attrs.js";
|
|
8
|
+
import { renderStyle } from "./styles.js";
|
|
9
|
+
import { renderClass } from "./classes.js";
|
|
10
|
+
import { renderChild } from "./children.js";
|
|
11
|
+
import { slotOutlet } from "./slots.js";
|
|
12
|
+
import { list } from "./loops.js";
|
|
14
13
|
export function slot(s, e, n) {
|
|
15
|
-
// The slot scope id slot content is given, written onto its elements: nothing unless the
|
|
16
|
-
// component it is given to passes one.
|
|
17
14
|
if (n.type === "Identifier" && n.name === "_scopeId") {
|
|
18
15
|
if (s.sid !== null)
|
|
19
16
|
e.stmt(`out.push_str(${s.sid});`);
|
|
@@ -23,22 +20,21 @@ export function slot(s, e, n) {
|
|
|
23
20
|
const a = n.arguments;
|
|
24
21
|
switch (n.callee.name) {
|
|
25
22
|
case "_ssrInterpolate":
|
|
26
|
-
interpolate(e, expr(s, a[0]));
|
|
23
|
+
interpolate(s, e, expr(s, a[0]), a[0]);
|
|
27
24
|
return;
|
|
28
25
|
case "_ssrRenderAttr":
|
|
29
26
|
if (a[0].type !== "StringLiteral")
|
|
30
|
-
fail(s.comp, "attribute names are literal", n);
|
|
27
|
+
fail(s.comp, "FV0417", "attribute names are literal", n);
|
|
31
28
|
renderAttr(s, e, a[0].value, expr(s, a[1]), a[1]);
|
|
32
29
|
return;
|
|
33
|
-
// `:hidden`, whose rendering depends on the value's type: Vue cannot decide it at compile time.
|
|
34
30
|
case "_ssrRenderDynamicAttr":
|
|
35
31
|
if (a[0].type !== "StringLiteral")
|
|
36
|
-
fail(s.comp, "attribute names are literal", n);
|
|
32
|
+
fail(s.comp, "FV0417", "attribute names are literal", n);
|
|
37
33
|
renderDynamicAttr(s, e, a[0].value, expr(s, a[1]), n);
|
|
38
34
|
return;
|
|
39
35
|
case "_ssrRenderAttrs":
|
|
40
36
|
if (a.length > 1)
|
|
41
|
-
fail(s.comp, "`ssrRenderAttrs` with a tag argument", n);
|
|
37
|
+
fail(s.comp, "FV0006", "`ssrRenderAttrs` with a tag argument", n);
|
|
42
38
|
renderAttrs(s, e, a[0]);
|
|
43
39
|
return;
|
|
44
40
|
case "_ssrRenderClass":
|
|
@@ -49,8 +45,6 @@ export function slot(s, e, n) {
|
|
|
49
45
|
return;
|
|
50
46
|
}
|
|
51
47
|
}
|
|
52
|
-
/* `v-html`, which Vue compiles to the bare value with an empty-string fallback and no escaping.
|
|
53
|
-
* Only a `TrustedHtml` prop may arrive here; anything else would be a string written raw. */
|
|
54
48
|
if (n.type === "LogicalExpression" && n.operator === "??" && n.right.type === "StringLiteral" && n.right.value === "") {
|
|
55
49
|
const v = expr(s, n.left);
|
|
56
50
|
if (v.ty.k === "html") {
|
|
@@ -63,7 +57,7 @@ export function slot(s, e, n) {
|
|
|
63
57
|
e.close();
|
|
64
58
|
return;
|
|
65
59
|
}
|
|
66
|
-
fail(s.comp, "`v-html` renders only a `TrustedHtml` prop (from `ferrovue/types`)", n);
|
|
60
|
+
fail(s.comp, "FV1502", "`v-html` renders only a `TrustedHtml` prop (from `ferrovue/types`)", n);
|
|
67
61
|
}
|
|
68
62
|
if (n.type === "ConditionalExpression" && n.consequent.type === "StringLiteral" && n.alternate.type === "StringLiteral") {
|
|
69
63
|
const t = expr(s, n.test);
|
|
@@ -81,18 +75,13 @@ export function slot(s, e, n) {
|
|
|
81
75
|
e.close();
|
|
82
76
|
return;
|
|
83
77
|
}
|
|
84
|
-
fail(s.comp, "this expression cannot be rendered on the server", n);
|
|
78
|
+
fail(s.comp, "FV0006", "this expression cannot be rendered on the server", n);
|
|
85
79
|
}
|
|
86
|
-
/** `isComment` in `@vue/server-renderer`: a chunk that is only comments and whitespace. */
|
|
87
80
|
export function isComment(text) {
|
|
88
81
|
if (!/^<!--[\s\S]*-->$/.test(text))
|
|
89
82
|
return false;
|
|
90
83
|
return text.length <= 8 || !text.replace(/<!--[^]*?-->/gm, "").trim();
|
|
91
84
|
}
|
|
92
|
-
/** Whether a `_push` argument is content to `ssrRenderSlot` rather than only comments — `"run"`
|
|
93
|
-
* when that depends on the values it interpolates (`${of1}<!--[-->` is a comment when `of1` writes
|
|
94
|
-
* nothing). Vue asks it of each pushed string, so the generated code asks `fv::is_comment` of what
|
|
95
|
-
* that push wrote. */
|
|
96
85
|
export function pushesContent(n) {
|
|
97
86
|
if (n.type === "StringLiteral")
|
|
98
87
|
return !isComment(n.value);
|
|
@@ -100,24 +89,18 @@ export function pushesContent(n) {
|
|
|
100
89
|
const quasis = n.quasis.map((q) => q.value.cooked);
|
|
101
90
|
if (n.expressions.length === 0)
|
|
102
91
|
return !isComment(quasis[0]);
|
|
103
|
-
// A comment starts with `<!--` and ends with `-->`: literal text that cannot decides it now.
|
|
104
92
|
const first = quasis[0];
|
|
105
93
|
const last = quasis.at(-1);
|
|
106
94
|
if (!"<!--".startsWith(first) && !first.startsWith("<!--"))
|
|
107
95
|
return true;
|
|
108
96
|
if (!"-->".endsWith(last) && !last.endsWith("-->"))
|
|
109
97
|
return true;
|
|
110
|
-
/* An interpolated value is escaped, so it writes no `<` or `>`: every comment starts and ends in
|
|
111
|
-
* the literal text — unless a value is raw (`v-html`), or a marker could be split across text
|
|
112
|
-
* and a value. Then literal text left over once the comments are taken out is content, whatever
|
|
113
|
-
* the values write. */
|
|
114
98
|
const raw = n.expressions.some((x) => x.type !== "CallExpression" && !(x.type === "Identifier" && x.name === "_scopeId"));
|
|
115
99
|
const split = quasis.some((q, i) => (i > 0 && /^(?:-|--|!--|->|>)/.test(q)) || (i < quasis.length - 1 && /(?:<|<!|<!-|-|--)$/.test(q)));
|
|
116
100
|
if (!raw && !split && /\S/.test(quasis.join("\0").replace(/<!--[^]*?-->/g, "").replaceAll("\0", "")))
|
|
117
101
|
return true;
|
|
118
102
|
return "run";
|
|
119
103
|
}
|
|
120
|
-
// A component's render is a buffer, never a comment.
|
|
121
104
|
return true;
|
|
122
105
|
}
|
|
123
106
|
export function push(s, e, n) {
|
|
@@ -132,7 +115,6 @@ export function push(s, e, n) {
|
|
|
132
115
|
pushed(s, e, n);
|
|
133
116
|
e.stmt("filled |= !fv::is_comment(&out[fv_chunk..]);");
|
|
134
117
|
}
|
|
135
|
-
/** What one `_push` writes. */
|
|
136
118
|
function pushed(s, e, n) {
|
|
137
119
|
const text = n.type === "StringLiteral" ? n.value : n.type === "TemplateLiteral" && !n.expressions.length ? n.quasis[0].value.cooked : null;
|
|
138
120
|
if (s.vnode && text === "<!---->") {
|
|
@@ -152,513 +134,11 @@ function pushed(s, e, n) {
|
|
|
152
134
|
return;
|
|
153
135
|
}
|
|
154
136
|
if (n.type === "CallExpression" && n.callee.type === "Identifier" && n.callee.name === "_ssrRenderComponent") {
|
|
155
|
-
|
|
156
|
-
const routed = target.type === "Identifier"
|
|
157
|
-
? s.router.get(target.name)
|
|
158
|
-
: target.type === "MemberExpression" && target.object.name === "$setup"
|
|
159
|
-
? s.router.get(target.computed ? target.property.value : target.property.name)
|
|
160
|
-
: undefined;
|
|
161
|
-
if (routed === "RouterLink")
|
|
162
|
-
routerLink(s, e, n);
|
|
163
|
-
else if (routed === "RouterView") {
|
|
164
|
-
// vue-router renders the page as its own root, which takes this component's id.
|
|
165
|
-
if (s.comp.scopeId !== null)
|
|
166
|
-
fail(s.comp, "`<RouterView>` in a component with `<style scoped>` gives the page this component's id, which the server's page does not carry", n);
|
|
167
|
-
e.stmt("fv_slots.router_view.render_to(out);");
|
|
168
|
-
}
|
|
169
|
-
else
|
|
137
|
+
if (!ctx.plugins.some((p) => p.component?.(s, e, n)))
|
|
170
138
|
renderChild(s, e, n);
|
|
171
139
|
return;
|
|
172
140
|
}
|
|
173
|
-
fail(s.comp, "this cannot be pushed", n);
|
|
174
|
-
}
|
|
175
|
-
/** The scope ids a child's root is handed, as a Rust `&str` (`null` for none), as
|
|
176
|
-
* `renderComponentSubTree` gathers them into its `attrs`: what this component passes on when the
|
|
177
|
-
* child is its root, unless the child sets `inheritAttrs: false`; this component's own id, the id of
|
|
178
|
-
* the instance that created the child's virtual node — slot content's included, which renders as
|
|
179
|
-
* the component that wrote it; and the slot scope id of the slot content the child is rendered in. */
|
|
180
|
-
function childAttrs(s, child, passesAttrs, inSlot, n) {
|
|
181
|
-
const base = passesAttrs && child.inheritAttrs ? s.attrs : null;
|
|
182
|
-
const own = s.comp.scopeId;
|
|
183
|
-
const slotted = inSlot ? s.sid : null;
|
|
184
|
-
let code;
|
|
185
|
-
if (base === null && slotted === null)
|
|
186
|
-
code = own === null ? null : rustStr(` ${own}`);
|
|
187
|
-
else if (own === null && slotted === null)
|
|
188
|
-
code = base;
|
|
189
|
-
else
|
|
190
|
-
code = `&fv::scope_attrs(${base ?? '""'}, ${own === null ? '""' : rustStr(own)}, ${slotted ?? '""'})`;
|
|
191
|
-
if (code !== null && !child.inherits)
|
|
192
|
-
fail(s.comp, `${child.name} is handed scope ids its render does not take`, n);
|
|
193
|
-
return code;
|
|
194
|
-
}
|
|
195
|
-
export function renderChild(s, e, n) {
|
|
196
|
-
const [target, rawProps, slots, , slotScopeId] = n.arguments;
|
|
197
|
-
let local = null;
|
|
198
|
-
if (target.type === "MemberExpression" && target.object.name === "$setup") {
|
|
199
|
-
local = target.computed ? target.property.value : target.property.name;
|
|
200
|
-
}
|
|
201
|
-
const isSelf = target.type === "Identifier" && target.name === s.selfAlias.name;
|
|
202
|
-
const childName = isSelf ? s.comp.name : local ? s.children.get(local) : undefined;
|
|
203
|
-
const child = childName ? s.components.get(childName) : undefined;
|
|
204
|
-
if (!child)
|
|
205
|
-
fail(s.comp, "a child component must be an imported island", n);
|
|
206
|
-
if (child.routerView) {
|
|
207
|
-
fail(s.comp, `${child.name} holds \`<RouterView>\`: the server renders it at the top, never as a child`, n);
|
|
208
|
-
}
|
|
209
|
-
// What the props merge: object literals — a `{ ref_for: true }` marker inside a `v-for` among
|
|
210
|
-
// them, which renders nothing — the parent's `_attrs` when the child is its root, and `$attrs`.
|
|
211
|
-
const parts = !rawProps || rawProps.type === "NullLiteral" ? [] : mergedParts(rawProps);
|
|
212
|
-
const merges = parts.length > 0 && parts[0] !== rawProps;
|
|
213
|
-
const passesAttrs = parts.some(isAttrs);
|
|
214
|
-
const passed = (p) => s.fallthrough !== null && ((isAttrs(p) && s.comp.inheritAttrs) || dollarAttrs(s, p));
|
|
215
|
-
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));
|
|
216
|
-
const objects = parts.filter((p) => !isAttrs(p) && !dollarAttrs(s, p) && !marker(p));
|
|
217
|
-
// Attributes this component is passed reach the child's props too, where a prop would take them.
|
|
218
|
-
if (parts.some(passed)) {
|
|
219
|
-
for (const name of s.comp.attrNames) {
|
|
220
|
-
if (declares(child, name)) {
|
|
221
|
-
fail(s.comp, `\`${name}\`, an attribute ${s.comp.name} may be passed, would reach ${child.name} as its prop \`${camelize(name)}\`, which an attribute has no type for`, n);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
const ids = childAttrs(s, child, passesAttrs, !!slotScopeId, n);
|
|
226
|
-
// `v-bind="x"`, where `x` is exactly the child's own `Props`: handed over as it is.
|
|
227
|
-
if (objects.length === 1 && objects[0].type !== "ObjectExpression") {
|
|
228
|
-
const v = expr(s, objects[0]);
|
|
229
|
-
// The component itself, however the template reached it: its own `Props` struct is the type.
|
|
230
|
-
const own = child.name === s.comp.name && v.ty.k === "struct" && v.ty.name === "Props";
|
|
231
|
-
if (own || (v.ty.k === "child" && v.ty.name === child.name)) {
|
|
232
|
-
callChild(s, e, child, v.code, slots, childAttrsArg(s, child, parts, merges, null, ids));
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
fail(s.comp, `child props must be an object literal, or \`v-bind\` of ${child.name}'s own \`Props\``, n);
|
|
236
|
-
}
|
|
237
|
-
for (const p of objects) {
|
|
238
|
-
if (p.type !== "ObjectExpression")
|
|
239
|
-
fail(s.comp, `child props must be an object literal, or \`v-bind\` of ${child.name}'s own \`Props\``, p);
|
|
240
|
-
}
|
|
241
|
-
const given = new Map();
|
|
242
|
-
// Keys the child does not declare are attributes, which fall through to its root.
|
|
243
|
-
const fallthrough = new Set();
|
|
244
|
-
for (const obj of objects) {
|
|
245
|
-
for (const p of obj.properties) {
|
|
246
|
-
if (p.type !== "ObjectProperty" || p.computed)
|
|
247
|
-
fail(s.comp, "child props hold plain keys", p);
|
|
248
|
-
const key = p.key.type === "Identifier" ? p.key.name : String(p.key.value);
|
|
249
|
-
// Listeners, `v-model`'s `onUpdate:…` among them, run on the client alone; a model's modifiers
|
|
250
|
-
// change only what an update writes.
|
|
251
|
-
if (!passedKey(child, key))
|
|
252
|
-
continue;
|
|
253
|
-
const field = child.props.fields.find((f) => camelize(f.js) === camelize(key));
|
|
254
|
-
if (field) {
|
|
255
|
-
given.set(field.js, p.value);
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
if (array(key))
|
|
259
|
-
fail(s.comp, `an attribute named \`${key}\`, which JavaScript would put before the others`, p);
|
|
260
|
-
const name = propsToAttrMap[key] ?? key.toLowerCase();
|
|
261
|
-
if (key !== "class" && key !== "style" && !isSSRSafeAttrName(name))
|
|
262
|
-
fail(s.comp, `unsafe attribute name \`${name}\``, p);
|
|
263
|
-
fallthrough.add(p);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
const inits = child.props.fields.map((f) => {
|
|
267
|
-
const node = given.get(f.js);
|
|
268
|
-
if (!node) {
|
|
269
|
-
if (f.ty.k !== "opt")
|
|
270
|
-
fail(s.comp, `${child.name} requires \`${f.js}\``, n);
|
|
271
|
-
return `${f.rust}: None`;
|
|
272
|
-
}
|
|
273
|
-
// Each side's own types named by the component that declares them, so that a type the parent
|
|
274
|
-
// imports from the child's `.vue` file is the child's type.
|
|
275
|
-
const v = expr(s, node);
|
|
276
|
-
return fieldInit(f.rust, ownInto(s.comp, { ...v, ty: markHome(v.ty, s.comp.name) }, markHome(f.ty, child.name), node));
|
|
277
|
-
});
|
|
278
|
-
callChild(s, e, child, `&super::${child.module}::Props { ${inits.join(", ")} }`, slots, childAttrsArg(s, child, parts, merges, fallthrough, ids));
|
|
279
|
-
}
|
|
280
|
-
/** Whether a name is an array index, which a JavaScript object lists before its other keys. */
|
|
281
|
-
function array(key) {
|
|
282
|
-
return /^(0|[1-9]\d*)$/.test(key) && Number(key) < 2 ** 32 - 1;
|
|
283
|
-
}
|
|
284
|
-
/** The last argument of a child's `render_scoped`: for a child a parent may pass attributes, the
|
|
285
|
-
* `fv::Attrs` this call passes — the keys of its props objects it does not declare, and what this
|
|
286
|
-
* component was passed, merged in their order — with the scope ids; for any other, the ids. */
|
|
287
|
-
function childAttrsArg(s, child, parts, merges, fallthrough, ids) {
|
|
288
|
-
const sources = [];
|
|
289
|
-
for (const p of parts) {
|
|
290
|
-
if (s.fallthrough !== null && ((isAttrs(p) && s.comp.inheritAttrs) || dollarAttrs(s, p))) {
|
|
291
|
-
sources.push(`${s.fallthrough}.list()`);
|
|
292
|
-
}
|
|
293
|
-
else if (p.type === "ObjectExpression" && fallthrough !== null) {
|
|
294
|
-
const entries = p.properties.filter((q) => fallthrough.has(q)).map((q) => {
|
|
295
|
-
const key = q.key.type === "Identifier" ? q.key.name : String(q.key.value);
|
|
296
|
-
return `(${rustStr(key)}, ${attrOf(s, key, q.value, "vnode")})`;
|
|
297
|
-
});
|
|
298
|
-
if (entries.length)
|
|
299
|
-
sources.push(`&[${entries.join(", ")}]`);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
if (!takesAttrs(child)) {
|
|
303
|
-
if (sources.length)
|
|
304
|
-
throw new GenError(`${s.comp.file}: ${child.name} is passed attributes it does not take`);
|
|
305
|
-
return ids;
|
|
306
|
-
}
|
|
307
|
-
if (!sources.length)
|
|
308
|
-
return ids === null ? "&fv::Attrs::NONE" : `&fv::Attrs::scoped(${ids})`;
|
|
309
|
-
// One object literal, or `_attrs` or `$attrs` alone, is the child's as it is: only `_mergeProps`
|
|
310
|
-
// normalises what it merges, even one object.
|
|
311
|
-
if (sources.length === 1 && !merges)
|
|
312
|
-
return `&fv::Attrs::new(${sources[0]}, ${ids ?? '""'})`;
|
|
313
|
-
return `&fv::Attrs::merged(&[${sources.join(", ")}], ${ids ?? '""'})`;
|
|
314
|
-
}
|
|
315
|
-
/** `name: value` in a struct literal, or the name alone when the value is a variable of that name. */
|
|
316
|
-
export function fieldInit(name, value) {
|
|
317
|
-
return value === name ? name : `${name}: ${bare(value)}`;
|
|
318
|
-
}
|
|
319
|
-
/** A borrow of an object or a list a template reads. A variable already holds a borrow: a loop's
|
|
320
|
-
* item, a narrowed value, a setup binding, a scoped slot's prop. */
|
|
321
|
-
function borrowed(code) {
|
|
322
|
-
return code.startsWith("&") || /^\w+$/.test(code) || /^fv_sp\d+\.\w+$/.test(code) ? code : `&${operand(code, UNARY)}`;
|
|
323
|
-
}
|
|
324
|
-
/** Whether a component takes a `Slots` argument. */
|
|
325
|
-
export function takesSlots(c) {
|
|
326
|
-
return c.slotNames.length > 0 || c.routerView;
|
|
327
|
-
}
|
|
328
|
-
/** The arguments after `props` that a component's `render` takes. */
|
|
329
|
-
export function extraParams(c) {
|
|
330
|
-
return ((takesSlots(c) ? ", fv_slots: Slots<'_>" : "") +
|
|
331
|
-
(c.usesRoute ? ", fv_route: &fv::Route<'_>" : "") +
|
|
332
|
-
(c.usesStores ? ", fv_stores: &super::stores::Stores<'_>" : "") +
|
|
333
|
-
(c.usesI18n ? ", fv_i18n: &fv::I18n" : "") +
|
|
334
|
-
(c.usesTeleports ? ", fv_teleports: &fv::Teleports" : ""));
|
|
335
|
-
}
|
|
336
|
-
/** `render(out, props[, slots][, route])` for a child, with the slot content this template gives
|
|
337
|
-
* it as closures; `render_scoped`, with the scope ids its root is handed last — and the attributes
|
|
338
|
-
* it is passed, as `fv::Attrs` — for a child that may be handed some. */
|
|
339
|
-
export function callChild(s, e, child, propsCode, slots, attrs) {
|
|
340
|
-
const given = new Map();
|
|
341
|
-
if (slots && slots.type !== "NullLiteral") {
|
|
342
|
-
if (slots.type !== "ObjectExpression")
|
|
343
|
-
fail(s.comp, "slots must be an object literal", slots);
|
|
344
|
-
for (const p of slots.properties) {
|
|
345
|
-
const key = p.key?.type === "Identifier" ? p.key.name : p.key?.value;
|
|
346
|
-
if (key === "_")
|
|
347
|
-
continue;
|
|
348
|
-
if (p.type !== "ObjectProperty" || p.computed)
|
|
349
|
-
fail(s.comp, "slots hold plain keys", p);
|
|
350
|
-
if (!child.slotNames.includes(key))
|
|
351
|
-
fail(s.comp, `${child.name} has no slot \`${key}\``, p);
|
|
352
|
-
given.set(key, p.value);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
const m = `super::${child.module}`;
|
|
356
|
-
const scoped = child.inherits || takesAttrs(child);
|
|
357
|
-
const route = (child.usesRoute ? ", fv_route" : "") + (child.usesStores ? ", fv_stores" : "") + (child.usesI18n ? ", fv_i18n" : "") + (child.usesTeleports ? ", fv_teleports" : "") +
|
|
358
|
-
(scoped ? `, ${attrs ?? (takesAttrs(child) ? "&fv::Attrs::NONE" : '""')}` : "");
|
|
359
|
-
const render = scoped ? "render_scoped" : "render";
|
|
360
|
-
if (!takesSlots(child)) {
|
|
361
|
-
e.stmt(`${m}::${render}(out, ${propsCode}${route});`);
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
e.open(`${m}::${render}(out, ${propsCode}, ${m}::Slots`);
|
|
365
|
-
for (const name of child.slotNames) {
|
|
366
|
-
const field = snake(name);
|
|
367
|
-
const value = given.get(name);
|
|
368
|
-
if (!value) {
|
|
369
|
-
e.stmt(`${field}: None,`);
|
|
370
|
-
continue;
|
|
371
|
-
}
|
|
372
|
-
const { body, param } = slotContent(s, value);
|
|
373
|
-
const shape = child.slotShapes.get(name);
|
|
374
|
-
const takesNone = param?.type === "Identifier" && param.name === "_";
|
|
375
|
-
// The slot scope id the child's outlets give the content, when they may give one.
|
|
376
|
-
const sid = child.passesSlotIds ? `fv_sid${++ctx.narrowCount}` : null;
|
|
377
|
-
const sidParam = sid === null ? "" : `, ${sid}: &str`;
|
|
378
|
-
/** The content's statements, then whether it pushed anything but comments. */
|
|
379
|
-
const content = (inner) => {
|
|
380
|
-
if (staticallyFilled(body)) {
|
|
381
|
-
statements({ ...inner, fill: false }, e, body);
|
|
382
|
-
e.stmt("true");
|
|
383
|
-
}
|
|
384
|
-
else {
|
|
385
|
-
e.stmt("let mut filled = false;");
|
|
386
|
-
statements({ ...inner, fill: true }, e, body);
|
|
387
|
-
e.stmt("filled");
|
|
388
|
-
}
|
|
389
|
-
};
|
|
390
|
-
/** A parameter the content never reads, named so that Rust does not warn of it. */
|
|
391
|
-
const unread = (opened, binding) => {
|
|
392
|
-
if (binding !== null && !e.reads(binding, opened + 1))
|
|
393
|
-
e.replace(opened, `${binding}:`, `_${binding}:`);
|
|
394
|
-
};
|
|
395
|
-
if (shape) {
|
|
396
|
-
// A scoped slot: content given the outlet's props, bound as the parent destructured them.
|
|
397
|
-
const sp = `fv_sp${++ctx.narrowCount}`;
|
|
398
|
-
const ty = { k: "struct", name: shape.name, home: child.name };
|
|
399
|
-
const locals = new Map(s.locals);
|
|
400
|
-
if (param?.type === "ObjectPattern") {
|
|
401
|
-
for (const p of param.properties) {
|
|
402
|
-
if (p.type !== "ObjectProperty" || p.computed || p.value.type !== "Identifier") {
|
|
403
|
-
fail(s.comp, "slot props are destructured into plain names, without defaults", p);
|
|
404
|
-
}
|
|
405
|
-
locals.set(p.value.name, fieldVal(s.comp, sp, ty, p.key.name ?? p.key.value, p));
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
else if (param?.type === "Identifier" && !takesNone) {
|
|
409
|
-
locals.set(param.name, { code: sp, ty });
|
|
410
|
-
}
|
|
411
|
-
else if (!takesNone)
|
|
412
|
-
fail(s.comp, "slot props are a name or an object pattern", param);
|
|
413
|
-
const life = shape.fields.some((f) => slotFieldBorrows(f.ty)) ? "<'_>" : "";
|
|
414
|
-
e.open(`${field}: Some(&|out: &mut String, ${sp}: &${m}::${shape.name}${life}${sidParam}| -> bool`);
|
|
415
|
-
const opened = e.lines.length - 1;
|
|
416
|
-
content({ ...s, locals, sid });
|
|
417
|
-
unread(opened, sp);
|
|
418
|
-
unread(opened, sid);
|
|
419
|
-
e.close("),");
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
422
|
-
if (!takesNone)
|
|
423
|
-
fail(s.comp, `\`<slot${name === "default" ? "" : ` name="${name}"`}>\` in ${child.name} passes no props`, param);
|
|
424
|
-
const inner = { ...s, sid };
|
|
425
|
-
if (sid !== null) {
|
|
426
|
-
e.open(`${field}: Some(fv::Slot::slotted(&|out: &mut String${sidParam}| -> bool`);
|
|
427
|
-
const opened = e.lines.length - 1;
|
|
428
|
-
content(inner);
|
|
429
|
-
unread(opened, sid);
|
|
430
|
-
e.close(")),");
|
|
431
|
-
}
|
|
432
|
-
else if (staticallyFilled(body)) {
|
|
433
|
-
e.open(`${field}: Some(fv::Slot::new(&|out: &mut String|`);
|
|
434
|
-
statements({ ...inner, fill: false }, e, body);
|
|
435
|
-
e.close(")),");
|
|
436
|
-
}
|
|
437
|
-
else {
|
|
438
|
-
e.open(`${field}: Some(fv::Slot::markup(&|out: &mut String| -> bool`);
|
|
439
|
-
e.stmt("let mut filled = false;");
|
|
440
|
-
statements({ ...inner, fill: true }, e, body);
|
|
441
|
-
e.stmt("filled");
|
|
442
|
-
e.close(")),");
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
e.close(`${route});`);
|
|
446
|
-
}
|
|
447
|
-
/** The statements a parent's slot content pushes: the `if (_push)` half of its `_withCtx`. */
|
|
448
|
-
export function slotBody(s, value) {
|
|
449
|
-
return slotContent(s, value).body;
|
|
450
|
-
}
|
|
451
|
-
/** A parent's slot content: what it pushes, and the parameter its scoped slot props are bound to
|
|
452
|
-
* (`_` when it takes none). */
|
|
453
|
-
export function slotContent(s, value) {
|
|
454
|
-
const fn = value.type === "CallExpression" && value.callee.type === "Identifier" && value.callee.name === "_withCtx"
|
|
455
|
-
? value.arguments[0]
|
|
456
|
-
: null;
|
|
457
|
-
if (fn?.type !== "ArrowFunctionExpression" || fn.body.type !== "BlockStatement") {
|
|
458
|
-
fail(s.comp, "unexpected slot content", value);
|
|
459
|
-
}
|
|
460
|
-
const branch = fn.body.body.length === 1 ? fn.body.body[0] : null;
|
|
461
|
-
if (branch?.type !== "IfStatement" || branch.test.type !== "Identifier" || branch.test.name !== "_push") {
|
|
462
|
-
fail(s.comp, "unexpected slot content", fn);
|
|
463
|
-
}
|
|
464
|
-
const body = branch.consequent.type === "BlockStatement" ? branch.consequent.body : [branch.consequent];
|
|
465
|
-
return { body, param: fn.params[0] };
|
|
466
|
-
}
|
|
467
|
-
/** Whether slot content always pushes something that is not a comment: a push of content outside
|
|
468
|
-
* any `if` or loop. Then nothing has to be decided at run time. */
|
|
469
|
-
export function staticallyFilled(body) {
|
|
470
|
-
return body.some((st) => st.type === "ExpressionStatement" && st.expression.type === "CallExpression" &&
|
|
471
|
-
st.expression.callee.type === "Identifier" && st.expression.callee.name === "_push" &&
|
|
472
|
-
pushesContent(st.expression.arguments[0]) === true);
|
|
473
|
-
}
|
|
474
|
-
/** `RowSlotProps`, `RowSlot`: the names of a scoped slot's props struct and content type. */
|
|
475
|
-
export function slotTypeName(slotName, suffix) {
|
|
476
|
-
return slotName.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("") + suffix;
|
|
477
|
-
}
|
|
478
|
-
/** Whether a scoped slot prop's Rust type borrows, and so needs the props' lifetime `'v`. */
|
|
479
|
-
export function slotFieldBorrows(ty) {
|
|
480
|
-
const copied = (t) => t.k === "int" || t.k === "float" || t.k === "bool";
|
|
481
|
-
return !(copied(ty) || (ty.k === "opt" && copied(ty.of)));
|
|
482
|
-
}
|
|
483
|
-
/** A scoped slot prop's Rust type: a scalar copied, anything else borrowed from the render for `'v`. */
|
|
484
|
-
export function slotFieldTy(ty, comp) {
|
|
485
|
-
const owned = (t) => rustTy(t, comp).replace(/'a\b/g, "'v");
|
|
486
|
-
switch (ty.k) {
|
|
487
|
-
case "int":
|
|
488
|
-
return "i64";
|
|
489
|
-
case "float":
|
|
490
|
-
return "f64";
|
|
491
|
-
case "bool":
|
|
492
|
-
return "bool";
|
|
493
|
-
case "str":
|
|
494
|
-
return "&'v str";
|
|
495
|
-
case "list":
|
|
496
|
-
return `&'v [${owned(ty.of)}]`;
|
|
497
|
-
case "struct":
|
|
498
|
-
case "child":
|
|
499
|
-
case "html":
|
|
500
|
-
return `&'v ${owned(ty)}`;
|
|
501
|
-
case "opt":
|
|
502
|
-
return `Option<${slotFieldTy(ty.of, comp)}>`;
|
|
503
|
-
default:
|
|
504
|
-
throw new GenError(`no Rust type for a slot prop of type ${JSON.stringify(ty)}`);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
/** A value an outlet passes, as the scoped slot prop field holds it. */
|
|
508
|
-
export function slotFieldValue(s, v, n) {
|
|
509
|
-
switch (v.ty.k) {
|
|
510
|
-
case "str":
|
|
511
|
-
return strArg(v.code);
|
|
512
|
-
case "int":
|
|
513
|
-
case "float":
|
|
514
|
-
case "bool":
|
|
515
|
-
return bare(v.code);
|
|
516
|
-
case "list":
|
|
517
|
-
// An array literal is a Rust array of `&str`, and a computed list holds its items in a form
|
|
518
|
-
// of its own: neither is the list a slot prop borrows.
|
|
519
|
-
if (v.code.startsWith("["))
|
|
520
|
-
fail(s.comp, "a slot prop is not an array literal: pass a list the component holds", n);
|
|
521
|
-
if (v.iter !== undefined)
|
|
522
|
-
fail(s.comp, "a slot prop is not a computed list: pass a list the component holds", n);
|
|
523
|
-
return borrowed(v.code);
|
|
524
|
-
case "struct":
|
|
525
|
-
case "child":
|
|
526
|
-
case "html":
|
|
527
|
-
return borrowed(v.code);
|
|
528
|
-
case "opt":
|
|
529
|
-
if (v.ty.of.k === "list")
|
|
530
|
-
return `${atom(v.code)}.map(|v| &v[..])`;
|
|
531
|
-
if (v.ty.of.k === "opt" || v.ty.of.k === "undef")
|
|
532
|
-
break;
|
|
533
|
-
return v.code;
|
|
534
|
-
}
|
|
535
|
-
return fail(s.comp, "a slot prop is a string, a number, a boolean, an object or a list", n);
|
|
536
|
-
}
|
|
537
|
-
/** `<slot name="x" :prop="…">fallback</slot>`: `ssrRenderSlot`, with the props a scoped slot passes. */
|
|
538
|
-
export function slotOutlet(s, e, c) {
|
|
539
|
-
const [, nameNode, slotProps, fallback] = c.arguments;
|
|
540
|
-
if (nameNode?.type !== "StringLiteral")
|
|
541
|
-
fail(s.comp, "a slot's name is literal", c);
|
|
542
|
-
if (slotProps?.type !== "ObjectExpression")
|
|
543
|
-
fail(s.comp, "a slot's props are attributes or a `v-bind` object literal", c);
|
|
544
|
-
const slotName = nameNode.value;
|
|
545
|
-
const field = `fv_slots.${snake(slotName)}`;
|
|
546
|
-
const outlet = `\`<slot${slotName === "default" ? "" : ` name="${slotName}"`}>\``;
|
|
547
|
-
let fn = "fv::slot_into";
|
|
548
|
-
let args = field;
|
|
549
|
-
if (slotProps.properties.length) {
|
|
550
|
-
const fields = [];
|
|
551
|
-
const values = [];
|
|
552
|
-
for (const p of slotProps.properties) {
|
|
553
|
-
if (p.type !== "ObjectProperty" || p.computed)
|
|
554
|
-
fail(s.comp, "slot props hold plain names", p);
|
|
555
|
-
const key = p.key.name ?? p.key.value;
|
|
556
|
-
if (!/^[A-Za-z_$][\w$]*$/.test(key))
|
|
557
|
-
fail(s.comp, `slot prop \`${key}\` is not a plain name; write it in camelCase`, p);
|
|
558
|
-
const v = expr(s, p.value);
|
|
559
|
-
fields.push({ js: key, rust: snake(key), ty: v.ty });
|
|
560
|
-
values.push(fieldInit(snake(key), slotFieldValue(s, v, p.value)));
|
|
561
|
-
}
|
|
562
|
-
const name = slotTypeName(slotName, "SlotProps");
|
|
563
|
-
const shape = { name, fields, slot: true };
|
|
564
|
-
const had = s.comp.slotShapes.get(slotName);
|
|
565
|
-
if (had && JSON.stringify(had.fields) !== JSON.stringify(fields)) {
|
|
566
|
-
fail(s.comp, `every ${outlet} passes the same props, of the same types`, c);
|
|
567
|
-
}
|
|
568
|
-
if (!had) {
|
|
569
|
-
if (s.comp.structs.has(name) && !s.comp.structs.get(name).slot)
|
|
570
|
-
fail(s.comp, `\`${name}\` names this slot's props; rename the interface`, c);
|
|
571
|
-
s.comp.slotShapes.set(slotName, shape);
|
|
572
|
-
s.comp.structs.set(name, shape);
|
|
573
|
-
}
|
|
574
|
-
fn = "fv::scoped_slot_into";
|
|
575
|
-
args = `${field}, &${name} { ${values.join(", ")} }`;
|
|
576
|
-
}
|
|
577
|
-
else if (s.comp.slotShapes.has(slotName)) {
|
|
578
|
-
fail(s.comp, `every ${outlet} passes the same props, of the same types`, c);
|
|
579
|
-
}
|
|
580
|
-
// The slot scope id: `"data-v-…-s"` from a component with `:slotted()` styles, followed inside
|
|
581
|
-
// slot content by the id that content was given, or that id alone.
|
|
582
|
-
const id = c.arguments[6];
|
|
583
|
-
let slotted = null;
|
|
584
|
-
if (id?.type === "StringLiteral")
|
|
585
|
-
slotted = rustStr(id.value);
|
|
586
|
-
else if (id?.type === "BinaryExpression" && id.operator === "+" && id.left.type === "StringLiteral" && id.right.type === "Identifier" && id.right.name === "_scopeId") {
|
|
587
|
-
slotted = s.sid === null ? rustStr(id.left.value) : `&[${rustStr(id.left.value)}, ${s.sid}].concat()`;
|
|
588
|
-
}
|
|
589
|
-
else if (id?.type === "Identifier" && id.name === "_scopeId")
|
|
590
|
-
slotted = s.sid;
|
|
591
|
-
else if (id && id.type !== "NullLiteral")
|
|
592
|
-
fail(s.comp, "unexpected slot scope id", id);
|
|
593
|
-
if (s.comp.passesSlotIds) {
|
|
594
|
-
fn = fn === "fv::slot_into" ? "fv::slot_into_slotted" : "fv::scoped_slot_into_slotted";
|
|
595
|
-
args += `, ${slotted ?? '""'}`;
|
|
596
|
-
}
|
|
597
|
-
else if (slotted !== null)
|
|
598
|
-
fail(s.comp, "a slot scope id passed to content that does not take one", c);
|
|
599
|
-
const call = (rest) => (s.fill ? `if ${fn}(out, ${args}, ${rest}) { filled = true; }` : `${fn}(out, ${args}, ${rest});`);
|
|
600
|
-
if (fallback?.type === "NullLiteral" || !fallback) {
|
|
601
|
-
e.stmt(call("None"));
|
|
602
|
-
return;
|
|
603
|
-
}
|
|
604
|
-
if (fallback.type !== "ArrowFunctionExpression" || fallback.body.type !== "BlockStatement") {
|
|
605
|
-
fail(s.comp, "unexpected slot fallback", fallback);
|
|
606
|
-
}
|
|
607
|
-
// The fallback writes to the same buffer as the outlet, so inside slot content it fills it too.
|
|
608
|
-
e.open(`${s.fill ? "if " : ""}${fn}(out, ${args}, Some(&mut |out: &mut String|`);
|
|
609
|
-
statements(s, e, fallback.body.body);
|
|
610
|
-
e.close(s.fill ? ")) { filled = true; }" : "));");
|
|
611
|
-
}
|
|
612
|
-
/** A value into a child's props field, whose strings are `Cow`s. */
|
|
613
|
-
export function ownInto(comp, v, want, node) {
|
|
614
|
-
if (want.k === "str") {
|
|
615
|
-
if (v.ty.k !== "str")
|
|
616
|
-
fail(comp, "a string prop needs a string", node);
|
|
617
|
-
return `std::borrow::Cow::Borrowed(${bare(v.code)})`;
|
|
618
|
-
}
|
|
619
|
-
if (want.k === "opt" && want.of.k === "str") {
|
|
620
|
-
if (v.ty.k === "undef")
|
|
621
|
-
return "None";
|
|
622
|
-
if (v.ty.k === "str")
|
|
623
|
-
return `Some(std::borrow::Cow::Borrowed(${bare(v.code)}))`;
|
|
624
|
-
if (v.ty.k === "opt" && v.ty.of.k === "str")
|
|
625
|
-
return `${atom(v.code)}.map(std::borrow::Cow::Borrowed)`;
|
|
626
|
-
}
|
|
627
|
-
if (sameTy(v.ty, want) && (want.k === "int" || want.k === "float" || want.k === "bool"))
|
|
628
|
-
return v.code;
|
|
629
|
-
if (want.k === "opt" && sameTy(v.ty, want.of) && (v.ty.k === "int" || v.ty.k === "float" || v.ty.k === "bool"))
|
|
630
|
-
return `Some(${bare(v.code)})`;
|
|
631
|
-
// An integer handed to a prop that takes a fraction.
|
|
632
|
-
if (want.k === "float" && v.ty.k === "int")
|
|
633
|
-
return asF64(v);
|
|
634
|
-
if (want.k === "opt" && want.of.k === "float" && v.ty.k === "int")
|
|
635
|
-
return `Some(${asF64(v)})`;
|
|
636
|
-
// A computed list: its items collected into the child's own list, objects cloned.
|
|
637
|
-
if (v.iter !== undefined && v.ty.k === "list" && want.k === "list" && sameTy(v.ty.of, want.of)) {
|
|
638
|
-
return want.of.k === "struct" || want.of.k === "child" ? `${v.iter}.cloned().collect()` : `${v.iter}.collect()`;
|
|
639
|
-
}
|
|
640
|
-
// An object, a list of objects, a record, or an optional one, of the type the child declares:
|
|
641
|
-
// cloned, which copies its strings only where they are owned.
|
|
642
|
-
const objecty = (t) => t.k === "struct" || t.k === "child" || t.k === "record" || ((t.k === "list" || t.k === "opt") && objecty(t.of));
|
|
643
|
-
if (objecty(want) && sameTy(v.ty, want))
|
|
644
|
-
return `${atom(v.code)}.${v.ty.k === "opt" ? "cloned" : "to_owned"}()`;
|
|
645
|
-
if (want.k === "opt" && objecty(want.of) && sameTy(v.ty, want.of))
|
|
646
|
-
return `Some(${atom(v.code)}.to_owned())`;
|
|
647
|
-
if (objecty(want) && v.ty.k === want.k && JSON.stringify(v.ty).includes('"struct"')) {
|
|
648
|
-
fail(comp, `a ${JSON.stringify(v.ty)} where the child takes a ${JSON.stringify(want)}: two components share a type only when both import it from one \`.ts\` file`, node);
|
|
649
|
-
}
|
|
650
|
-
// A list of strings or numbers: each item borrowed, or copied, into the child's own list.
|
|
651
|
-
if (want.k === "list" && v.ty.k === "list" && v.ty.of.k === "undef")
|
|
652
|
-
return "Vec::new()";
|
|
653
|
-
if (want.k === "opt" && want.of.k === "list" && v.ty.k === "list")
|
|
654
|
-
return `Some(${ownInto(comp, v, want.of, node)})`;
|
|
655
|
-
if (want.k === "list" && v.ty.k === "list" && sameTy(v.ty.of, want.of)) {
|
|
656
|
-
if (want.of.k === "str")
|
|
657
|
-
return `${atom(v.code)}.iter().map(|v| std::borrow::Cow::Borrowed(&**v)).collect()`;
|
|
658
|
-
if (want.of.k === "int" || want.of.k === "float" || want.of.k === "bool")
|
|
659
|
-
return `${atom(v.code)}.to_vec()`;
|
|
660
|
-
}
|
|
661
|
-
return fail(comp, `a ${JSON.stringify(v.ty)} into a ${JSON.stringify(want)} prop`, node);
|
|
141
|
+
fail(s.comp, "FV0006", "this cannot be pushed", n);
|
|
662
142
|
}
|
|
663
143
|
export function statements(s, e, body) {
|
|
664
144
|
for (const st of body) {
|
|
@@ -677,7 +157,6 @@ export function statements(s, e, body) {
|
|
|
677
157
|
slotOutlet(s, e, c);
|
|
678
158
|
continue;
|
|
679
159
|
}
|
|
680
|
-
// `<Suspense>`: its default content, rendered in place — ferrovue renders nothing async.
|
|
681
160
|
if (callee === "_ssrRenderSuspense") {
|
|
682
161
|
const def = c.arguments[1]?.properties?.find((p) => (p.key?.name ?? p.key?.value) === "default");
|
|
683
162
|
if (!def)
|
|
@@ -685,31 +164,15 @@ export function statements(s, e, body) {
|
|
|
685
164
|
else if (def.value.type === "ArrowFunctionExpression" && def.value.body.type === "BlockStatement")
|
|
686
165
|
statements(s, e, def.value.body.body);
|
|
687
166
|
else
|
|
688
|
-
fail(s.comp, "unexpected `<Suspense>` content", c);
|
|
167
|
+
fail(s.comp, "FV0006", "unexpected `<Suspense>` content", c);
|
|
689
168
|
continue;
|
|
690
169
|
}
|
|
691
|
-
|
|
692
|
-
if (callee === "_ssrRenderTeleport") {
|
|
693
|
-
const [, content, target, disabled] = c.arguments;
|
|
694
|
-
if (s.fill)
|
|
695
|
-
fail(s.comp, "a `<Teleport>` in slot content whose emptiness is decided at run time", st);
|
|
696
|
-
const to = expr(s, target);
|
|
697
|
-
if (to.ty.k !== "str")
|
|
698
|
-
fail(s.comp, "a `<Teleport>`'s `to` is a string", target);
|
|
699
|
-
const off = disabled ? bare(cond(s, disabled)) : "false";
|
|
700
|
-
if (content?.type !== "ArrowFunctionExpression" || content.body.type !== "BlockStatement")
|
|
701
|
-
fail(s.comp, "unexpected `<Teleport>` content", st);
|
|
702
|
-
e.open(`fv::teleport_into(out, fv_teleports, ${strArg(to.code)}, ${off}, &|out: &mut String|`);
|
|
703
|
-
statements(s, e, content.body.body);
|
|
704
|
-
e.close(");");
|
|
170
|
+
if (ctx.plugins.some((p) => p.statement?.(s, e, c, st)))
|
|
705
171
|
continue;
|
|
706
|
-
}
|
|
707
172
|
if (callee === "_ssrRenderVNode") {
|
|
708
|
-
fail(s.comp, "`<component :is>` chooses its component at run time; write the choices out with `v-if`", st);
|
|
173
|
+
fail(s.comp, "FV0418", "`<component :is>` chooses its component at run time; write the choices out with `v-if`", st);
|
|
709
174
|
}
|
|
710
175
|
}
|
|
711
|
-
/* `const _component_X = _resolveComponent("X", true)`: a component that uses itself, which is
|
|
712
|
-
* how a tree renders. Any other component resolved by name is one this compiler cannot see. */
|
|
713
176
|
if (st.type === "VariableDeclaration" && st.declarations.length === 1) {
|
|
714
177
|
const d = st.declarations[0];
|
|
715
178
|
const init = d.init;
|
|
@@ -719,25 +182,19 @@ export function statements(s, e, body) {
|
|
|
719
182
|
s.selfAlias.name = d.id.name;
|
|
720
183
|
continue;
|
|
721
184
|
}
|
|
722
|
-
const
|
|
185
|
+
const named = init?.type === "CallExpression" && init.callee.type === "Identifier" &&
|
|
723
186
|
init.callee.name === "_resolveComponent" && init.arguments.length === 1 &&
|
|
724
187
|
init.arguments[0]?.type === "StringLiteral" ? init.arguments[0].value : null;
|
|
725
|
-
if (
|
|
726
|
-
s.router.set(d.id.name, routed);
|
|
188
|
+
if (named !== null && ctx.plugins.some((p) => p.resolveComponent?.(s, d.id.name, named)))
|
|
727
189
|
continue;
|
|
728
|
-
}
|
|
729
|
-
// `const _directive_focus = _resolveDirective("focus")`: a globally registered directive.
|
|
730
190
|
if (init?.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "_resolveDirective" && init.arguments[0]?.type === "StringLiteral") {
|
|
731
191
|
s.directives.set(d.id.name, init.arguments[0].value);
|
|
732
192
|
continue;
|
|
733
193
|
}
|
|
734
|
-
fail(s.comp, "a component the template resolves by name must be imported, or be this one", st);
|
|
194
|
+
fail(s.comp, "FV0513", "a component the template resolves by name must be imported, or be this one", st);
|
|
735
195
|
}
|
|
736
196
|
if (st.type === "IfStatement") {
|
|
737
197
|
const branch = (b) => (b.type === "BlockStatement" ? b.body : [b]);
|
|
738
|
-
/* `a && b && …`, where a leading operand is an optional value: present for the whole branch,
|
|
739
|
-
* so it is bound and narrowed there, as TypeScript narrows it — a Rust let-chain, which is why
|
|
740
|
-
* generated code needs edition 2024. */
|
|
741
198
|
if (st.test.type === "LogicalExpression" && st.test.operator === "&&") {
|
|
742
199
|
const operands = [];
|
|
743
200
|
const flatten = (n) => {
|
|
@@ -765,7 +222,6 @@ export function statements(s, e, body) {
|
|
|
765
222
|
parts.push(operand(cond(inner, op), CMP));
|
|
766
223
|
}
|
|
767
224
|
}
|
|
768
|
-
// A condition known now: a `false` decides the branch, a `true` adds nothing.
|
|
769
225
|
if (parts.includes("false")) {
|
|
770
226
|
if (st.alternate)
|
|
771
227
|
statements(s, e, branch(st.alternate));
|
|
@@ -777,9 +233,7 @@ export function statements(s, e, body) {
|
|
|
777
233
|
e.open(`if ${parts.join(" && ")}`);
|
|
778
234
|
const at = e.lines.length - 1;
|
|
779
235
|
statements({ ...s, narrowed }, e, branch(st.consequent));
|
|
780
|
-
// A value the branch never reads is only tested for presence.
|
|
781
236
|
for (const [name, p] of bound) {
|
|
782
|
-
// Read by the branch, or by a later condition of the same chain, which binds it once.
|
|
783
237
|
const inHead = occurrences(e.lines[at], name) > 1;
|
|
784
238
|
if (!inHead && !e.reads(name, at + 1))
|
|
785
239
|
e.replace(at, p.pattern(name), operand(p.present, CMP));
|
|
@@ -802,9 +256,6 @@ export function statements(s, e, body) {
|
|
|
802
256
|
statements(s, e, branch(taken));
|
|
803
257
|
continue;
|
|
804
258
|
}
|
|
805
|
-
/* An optional value tested for presence is narrowed inside the branch, as TypeScript narrows
|
|
806
|
-
* it: bound by `if let`, so `user.name` under `v-if="user"` reads the bound value. A negated
|
|
807
|
-
* test — `!user`, `user === undefined` — narrows the `v-else` instead, written first. */
|
|
808
259
|
const p = presence(s, st.test);
|
|
809
260
|
if (p && (!p.negated || st.alternate)) {
|
|
810
261
|
const name = `n${++ctx.narrowCount}`;
|
|
@@ -832,181 +283,6 @@ export function statements(s, e, body) {
|
|
|
832
283
|
e.close();
|
|
833
284
|
continue;
|
|
834
285
|
}
|
|
835
|
-
fail(s.comp, `\`${st.type}\` in the compiled template`, st);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
export function list(s, e, c) {
|
|
839
|
-
const fn = c.arguments[1];
|
|
840
|
-
if (fn.type !== "ArrowFunctionExpression" || fn.body.type !== "BlockStatement") {
|
|
841
|
-
fail(s.comp, "unexpected `ssrRenderList` callback", c);
|
|
842
|
-
}
|
|
843
|
-
// `([key, value], i) in Object.entries(r)`, which walks the record as `(value, key, i) in r` does.
|
|
844
|
-
if (isObjectCall(c.arguments[0], "entries")) {
|
|
845
|
-
const [pair, index] = fn.params;
|
|
846
|
-
const r = c.arguments[0].arguments.length === 1 ? expr(s, c.arguments[0].arguments[0]) : null;
|
|
847
|
-
if (r?.ty.k !== "record")
|
|
848
|
-
return fail(s.comp, "`Object.entries()` takes a `Record<string, T>`", c.arguments[0]);
|
|
849
|
-
if (pair?.type !== "ArrayPattern" || pair.elements.length > 2 || pair.elements.some((x) => x?.type !== "Identifier")) {
|
|
850
|
-
return fail(s.comp, "a `v-for` over `Object.entries()` names its items `[key, value]`", pair ?? c);
|
|
851
|
-
}
|
|
852
|
-
recordLoop(s, e, r, fn, pair.elements[1], pair.elements[0], index);
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
const src = expr(s, c.arguments[0]);
|
|
856
|
-
if (src.ty.k === "record") {
|
|
857
|
-
const [value, key, index] = fn.params;
|
|
858
|
-
recordLoop(s, e, src, fn, value, key, index);
|
|
859
|
-
return;
|
|
860
|
-
}
|
|
861
|
-
const [item, index] = fn.params;
|
|
862
|
-
const idx = index ? snake(index.name) : null;
|
|
863
|
-
// The item's name in Rust: its own when it is a plain name, a placeholder when it is destructured.
|
|
864
|
-
const itemName = item.type === "Identifier" ? snake(item.name) : `fv_item${++ctx.narrowCount}`;
|
|
865
|
-
const inner = new Map(s.locals);
|
|
866
|
-
let of;
|
|
867
|
-
// What the loop walks, and what its head binds each item to.
|
|
868
|
-
let walk;
|
|
869
|
-
let bound;
|
|
870
|
-
let itemLet = -1;
|
|
871
|
-
const loop = (it, i) => (i ? `for (${i}, ${it}) in ${atom(walk)}.enumerate()` : `for ${it} in ${walk}`);
|
|
872
|
-
if (src.ty.k === "list" && src.iter !== undefined) {
|
|
873
|
-
// A computed list: its items come as they are — a string as a `Cow`, read as a `&str`.
|
|
874
|
-
of = src.ty.of;
|
|
875
|
-
walk = src.iter;
|
|
876
|
-
bound = of.k === "str" ? `${itemName}_cow` : itemName;
|
|
877
|
-
e.open(loop(bound, idx));
|
|
878
|
-
if (of.k === "str") {
|
|
879
|
-
e.stmt(`let ${itemName}: &str = &${bound};`);
|
|
880
|
-
itemLet = e.lines.length - 1;
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
else if (src.ty.k === "int") {
|
|
884
|
-
// `v-for="n in 5"`: 1 to 5, as `renderList` counts a number.
|
|
885
|
-
of = INT;
|
|
886
|
-
walk = `1..=${operand(src.code, OR)}`;
|
|
887
|
-
bound = itemName;
|
|
888
|
-
e.open(loop(bound, idx));
|
|
889
|
-
}
|
|
890
|
-
else if (src.ty.k === "list") {
|
|
891
|
-
of = src.ty.of;
|
|
892
|
-
if (of.k === "undef")
|
|
893
|
-
fail(s.comp, "`v-for` over an empty array literal", c);
|
|
894
|
-
const itemCode = `${itemName}_ref`;
|
|
895
|
-
walk = `${atom(src.code)}.iter()`;
|
|
896
|
-
bound = itemCode;
|
|
897
|
-
e.open(loop(bound, idx));
|
|
898
|
-
if (of.k === "str")
|
|
899
|
-
e.stmt(`let ${itemName}: &str = ${itemCode};`);
|
|
900
|
-
// An object — a struct, or another component's props — is borrowed; only a scalar is copied.
|
|
901
|
-
else if (of.k === "struct" || of.k === "child")
|
|
902
|
-
e.stmt(`let ${itemName} = ${itemCode};`);
|
|
903
|
-
else
|
|
904
|
-
e.stmt(`let ${itemName} = *${itemName}_ref;`);
|
|
905
|
-
itemLet = e.lines.length - 1;
|
|
906
|
-
}
|
|
907
|
-
else {
|
|
908
|
-
return fail(s.comp, "`v-for` walks an array, or counts to a number", c);
|
|
909
|
-
}
|
|
910
|
-
let idxLet = -1;
|
|
911
|
-
if (idx) {
|
|
912
|
-
e.stmt(`let ${idx} = ${idx} as i64;`);
|
|
913
|
-
idxLet = e.lines.length - 1;
|
|
914
|
-
}
|
|
915
|
-
const bodyFrom = e.lines.length;
|
|
916
|
-
if (item.type === "Identifier")
|
|
917
|
-
inner.set(item.name, { code: itemName, ty: of, ...(src.lone ? { lone: true } : {}) });
|
|
918
|
-
else if (item.type === "ObjectPattern") {
|
|
919
|
-
// `v-for="{ name, id: key } in items"`: each name is that field of the item.
|
|
920
|
-
for (const p of item.properties) {
|
|
921
|
-
if (p.type !== "ObjectProperty" || p.computed || p.value.type !== "Identifier") {
|
|
922
|
-
fail(s.comp, "a destructured `v-for` item binds plain names, without defaults", p);
|
|
923
|
-
}
|
|
924
|
-
inner.set(p.value.name, fieldVal(s.comp, itemName, of, p.key.name ?? p.key.value, p));
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
else
|
|
928
|
-
fail(s.comp, "a `v-for` item is a name or an object pattern", item);
|
|
929
|
-
if (index)
|
|
930
|
-
inner.set(index.name, { code: idx, ty: INT });
|
|
931
|
-
const before = e.literalBytes;
|
|
932
|
-
const fromProps = src.ty.k === "list" && src.iter === undefined && /^\(?props\./.test(src.code);
|
|
933
|
-
statements({ ...s, locals: inner, loop: fromProps ? { item: itemName, over: src.code } : undefined }, e, fn.body.body);
|
|
934
|
-
// An item or index the body never reads is not bound, and an index never counted: the loop only
|
|
935
|
-
// counts.
|
|
936
|
-
const unusedItem = !e.reads(itemName, bodyFrom);
|
|
937
|
-
const unusedIdx = idx !== null && !e.reads(idx, bodyFrom);
|
|
938
|
-
const head = bodyFrom - 1 - (idx ? 1 : 0) - (itemLet >= 0 ? 1 : 0);
|
|
939
|
-
e.replace(head, loop(bound, idx), loop(unusedItem ? "_" : bound, unusedIdx ? null : idx));
|
|
940
|
-
// Removed from the last line up, so the earlier indices stay right.
|
|
941
|
-
if (unusedIdx)
|
|
942
|
-
e.lines.splice(idxLet, 1);
|
|
943
|
-
if (unusedItem && itemLet >= 0)
|
|
944
|
-
e.lines.splice(itemLet, 1);
|
|
945
|
-
e.close();
|
|
946
|
-
// The body's markup is written once per item, not once.
|
|
947
|
-
const body = e.literalBytes - before;
|
|
948
|
-
e.literalBytes = before;
|
|
949
|
-
// Counted up front only when the list is reachable from there: a list of the props, or a list in
|
|
950
|
-
// each item of one, counted over all of them. One a `v-if` narrowed, or a loop deeper down, exists
|
|
951
|
-
// only inside its block, and the reservation is an estimate either way.
|
|
952
|
-
if (body > 0 && fromProps)
|
|
953
|
-
e.perItem.push(`${body} * ${atom(src.code)}.len()`);
|
|
954
|
-
else if (body > 0 && src.ty.k === "list" && s.loop && src.code.startsWith(`${s.loop.item}.`)) {
|
|
955
|
-
const outer = s.loop;
|
|
956
|
-
e.perItem.push(`${body} * ${atom(outer.over)}.iter().map(|${outer.item}| ${src.code}.len()).sum::<usize>()`);
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
/** `v-for="(value, key, index) in r"` over a record: its entries in JavaScript's order of keys,
|
|
960
|
-
* which is the order `renderList` walks `Object.keys`. */
|
|
961
|
-
function recordLoop(s, e, r, fn, value, key, index) {
|
|
962
|
-
if (r.ty.k !== "record")
|
|
963
|
-
return;
|
|
964
|
-
const of = r.ty.of;
|
|
965
|
-
for (const p of [key, index])
|
|
966
|
-
if (p && p.type !== "Identifier")
|
|
967
|
-
fail(s.comp, "a record's key and index in `v-for` are plain names", p);
|
|
968
|
-
const n = ++ctx.narrowCount;
|
|
969
|
-
const v = value?.type === "Identifier" ? snake(value.name) : `fv_value${n}`;
|
|
970
|
-
const k = key ? snake(key.name) : `fv_key${n}`;
|
|
971
|
-
const i = index ? snake(index.name) : `fv_index${n}`;
|
|
972
|
-
e.open(`for (${i}, (${k}, ${v}_ref)) in ${atom(r.code)}.iter().enumerate()`);
|
|
973
|
-
const head = e.lines.length - 1;
|
|
974
|
-
const indent = /^ */.exec(e.lines[head])[0];
|
|
975
|
-
const inner = new Map(s.locals);
|
|
976
|
-
// A value is read as a list item is: a string as a `&str`, a number copied, an object borrowed.
|
|
977
|
-
e.stmt(of.k === "str" ? `let ${v}: &str = ${v}_ref;` : of.k === "int" || of.k === "float" || of.k === "bool" ? `let ${v} = *${v}_ref;` : `let ${v} = ${v}_ref;`);
|
|
978
|
-
e.stmt(`let ${i} = ${i} as i64;`);
|
|
979
|
-
const bodyFrom = e.lines.length;
|
|
980
|
-
if (value?.type === "Identifier")
|
|
981
|
-
inner.set(value.name, { code: v, ty: of });
|
|
982
|
-
else if (value?.type === "ObjectPattern" && (of.k === "struct" || of.k === "child")) {
|
|
983
|
-
for (const p of value.properties) {
|
|
984
|
-
if (p.type !== "ObjectProperty" || p.computed || p.value.type !== "Identifier") {
|
|
985
|
-
fail(s.comp, "a destructured `v-for` item binds plain names, without defaults", p);
|
|
986
|
-
}
|
|
987
|
-
inner.set(p.value.name, fieldVal(s.comp, v, of, p.key.name ?? p.key.value, p));
|
|
988
|
-
}
|
|
286
|
+
fail(s.comp, "FV0006", `\`${st.type}\` in the compiled template`, st);
|
|
989
287
|
}
|
|
990
|
-
else if (value)
|
|
991
|
-
fail(s.comp, "a `v-for` item is a name, or an object pattern of an object", value);
|
|
992
|
-
if (key)
|
|
993
|
-
inner.set(key.name, { code: k, ty: STR });
|
|
994
|
-
if (index)
|
|
995
|
-
inner.set(index.name, { code: i, ty: INT });
|
|
996
|
-
const before = e.literalBytes;
|
|
997
|
-
statements({ ...s, locals: inner }, e, fn.body.body);
|
|
998
|
-
// Whatever the body never reads is not bound, from the last line up.
|
|
999
|
-
const reads = (name) => e.reads(name, bodyFrom);
|
|
1000
|
-
const [readsV, readsK, readsI] = [reads(v), reads(k), reads(i)];
|
|
1001
|
-
if (!readsI)
|
|
1002
|
-
e.lines.splice(head + 2, 1);
|
|
1003
|
-
if (!readsV)
|
|
1004
|
-
e.lines.splice(head + 1, 1);
|
|
1005
|
-
const pair = `(${readsK ? k : "_"}, ${readsV ? `${v}_ref` : "_"})`;
|
|
1006
|
-
e.lines[head] = `${indent}for ${readsI ? `(${i}, ${pair}) in ${atom(r.code)}.iter().enumerate()` : `${pair} in ${atom(r.code)}.iter()`} {`;
|
|
1007
|
-
e.close();
|
|
1008
|
-
const body = e.literalBytes - before;
|
|
1009
|
-
e.literalBytes = before;
|
|
1010
|
-
if (body > 0 && /^\(?props\./.test(r.code))
|
|
1011
|
-
e.perItem.push(`${body} * ${atom(r.code)}.len()`);
|
|
1012
288
|
}
|