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