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.
Files changed (106) hide show
  1. package/README.md +225 -42
  2. package/dist/attrs.d.ts +3 -53
  3. package/dist/attrs.js +41 -469
  4. package/dist/calls.d.ts +4 -0
  5. package/dist/calls.js +221 -0
  6. package/dist/children.d.ts +16 -0
  7. package/dist/children.js +286 -0
  8. package/dist/classes.d.ts +13 -0
  9. package/dist/classes.js +176 -0
  10. package/dist/cli.js +31 -22
  11. package/dist/client.d.ts +35 -2
  12. package/dist/client.js +77 -14
  13. package/dist/compiler.d.ts +1 -1
  14. package/dist/compiler.js +31 -92
  15. package/dist/component.d.ts +3 -1
  16. package/dist/component.js +123 -104
  17. package/dist/constants.d.ts +42 -0
  18. package/dist/constants.js +292 -0
  19. package/dist/context.d.ts +29 -67
  20. package/dist/context.js +13 -38
  21. package/dist/diagnostics.d.ts +23 -0
  22. package/dist/diagnostics.js +30 -0
  23. package/dist/diff.d.ts +0 -3
  24. package/dist/diff.js +0 -9
  25. package/dist/emitter.d.ts +0 -8
  26. package/dist/emitter.js +0 -12
  27. package/dist/errors.d.ts +867 -0
  28. package/dist/errors.js +330 -0
  29. package/dist/expr.d.ts +1 -70
  30. package/dist/expr.js +142 -934
  31. package/dist/fallthrough.d.ts +9 -0
  32. package/dist/fallthrough.js +78 -0
  33. package/dist/fixture.d.ts +4 -10
  34. package/dist/fixture.js +38 -27
  35. package/dist/hydration.d.ts +15 -0
  36. package/dist/hydration.js +80 -0
  37. package/dist/index.d.ts +3 -0
  38. package/dist/index.js +2 -0
  39. package/dist/islands.d.ts +1 -0
  40. package/dist/islands.js +1 -0
  41. package/dist/link-router.d.ts +24 -0
  42. package/dist/link-router.js +36 -0
  43. package/dist/lists.d.ts +0 -11
  44. package/dist/lists.js +34 -60
  45. package/dist/loops.d.ts +3 -0
  46. package/dist/loops.js +168 -0
  47. package/dist/model.d.ts +46 -144
  48. package/dist/model.js +150 -18
  49. package/dist/narrowing.d.ts +27 -0
  50. package/dist/narrowing.js +157 -0
  51. package/dist/numbers.d.ts +8 -0
  52. package/dist/numbers.js +73 -0
  53. package/dist/parens.d.ts +0 -27
  54. package/dist/parens.js +0 -44
  55. package/dist/plugin.d.ts +84 -0
  56. package/dist/plugin.js +26 -0
  57. package/dist/plugins/client-only.d.ts +8 -0
  58. package/dist/plugins/client-only.js +107 -0
  59. package/dist/plugins/i18n.d.ts +23 -0
  60. package/dist/{i18n.js → plugins/i18n.js} +88 -31
  61. package/dist/plugins/index.d.ts +2 -0
  62. package/dist/plugins/index.js +10 -0
  63. package/dist/plugins/router-link.d.ts +8 -0
  64. package/dist/{router.js → plugins/router-link.js} +52 -91
  65. package/dist/plugins/router.d.ts +42 -0
  66. package/dist/plugins/router.js +256 -0
  67. package/dist/plugins/scoped.d.ts +12 -0
  68. package/dist/plugins/scoped.js +125 -0
  69. package/dist/plugins/shared-types.d.ts +2 -0
  70. package/dist/plugins/shared-types.js +7 -0
  71. package/dist/plugins/stores.d.ts +37 -0
  72. package/dist/plugins/stores.js +379 -0
  73. package/dist/plugins/teleport.d.ts +5 -0
  74. package/dist/plugins/teleport.js +55 -0
  75. package/dist/plugins/twins.d.ts +10 -0
  76. package/dist/plugins/twins.js +164 -0
  77. package/dist/routes.d.ts +10 -0
  78. package/dist/routes.js +6 -0
  79. package/dist/rust.d.ts +1 -7
  80. package/dist/rust.js +56 -174
  81. package/dist/script.d.ts +3 -6
  82. package/dist/script.js +127 -140
  83. package/dist/settle.d.ts +6 -0
  84. package/dist/settle.js +34 -0
  85. package/dist/slots.d.ts +13 -0
  86. package/dist/slots.js +160 -0
  87. package/dist/strings.d.ts +13 -0
  88. package/dist/strings.js +208 -0
  89. package/dist/styles.d.ts +14 -0
  90. package/dist/styles.js +181 -0
  91. package/dist/template.d.ts +1 -42
  92. package/dist/template.js +25 -749
  93. package/dist/testing.d.ts +2 -0
  94. package/dist/testing.js +4 -10
  95. package/dist/types.d.ts +2 -0
  96. package/dist/typescript.d.ts +2 -14
  97. package/dist/typescript.js +53 -59
  98. package/dist/vite.d.ts +2 -1
  99. package/dist/vite.js +11 -32
  100. package/package.json +15 -6
  101. package/dist/i18n.d.ts +0 -27
  102. package/dist/router.d.ts +0 -17
  103. package/dist/scoped.d.ts +0 -24
  104. package/dist/scoped.js +0 -177
  105. package/dist/stores.d.ts +0 -6
  106. package/dist/stores.js +0 -229
package/dist/template.js CHANGED
@@ -1,19 +1,16 @@
1
- /* The compiled template's statements: pushes, conditions, lists, child components and slots. */
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 { markHome } from "./typescript.js";
5
- import { asF64, boolOf, cond, expr, fieldVal, isObjectCall, known, narrowTo, presence, truthy } from "./expr.js";
6
- import { atom, bare, CMP, condition, occurrences, operand, OR, strArg, UNARY } from "./parens.js";
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 { attrOf, dollarAttrs, IGNORED_PROPS, interpolate, isAttrs, mergedParts, renderAttr, renderAttrs, renderClass, renderDynamicAttr, renderStyle } from "./attrs.js";
9
- import { passedKey } from "./scoped.js";
10
- import { isSSRSafeAttrName, propsToAttrMap } from "@vue/shared";
11
- import { routerLink } from "./router.js";
12
- import { rustTy } from "./rust.js";
13
- /** One `${...}` inside a pushed template literal. */
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
- const target = n.arguments[0];
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
- // `<Teleport>`: markers here, the content in its target's buffer, which the page writes.
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 routed = init?.type === "CallExpression" && init.callee.type === "Identifier" &&
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 (routed === "RouterLink" || routed === "RouterView") {
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
  }