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/rust.js CHANGED
@@ -1,17 +1,17 @@
1
- /* The Rust source written for each component, and the modules beside them. */
2
1
  import { parse as parseJs } from "@babel/parser";
3
2
  import { basename } from "node:path";
4
- import { fail, GenError, rustStr, snake, tagAst } from "./model.js";
5
- import { allRoutes, ctx } from "./context.js";
3
+ import { blankComponent, fail, GenError, rustStr, snake, tagAst, takesAttrs } from "./model.js";
4
+ import { ctx } from "./context.js";
6
5
  import { lookupStruct } from "./typescript.js";
7
6
  import { childOf } from "./expr.js";
8
7
  import { Emitter } from "./emitter.js";
9
- import { extraParams, fieldInit, slotFieldBorrows, slotFieldTy, slotTypeName, statements, takesSlots } from "./template.js";
10
- import { storeHome } from "./stores.js";
8
+ import { statements } from "./template.js";
9
+ import { slotFieldBorrows, slotFieldTy, slotTypeName } from "./slots.js";
10
+ import { extraParams, fieldInit, takesSlots } from "./children.js";
11
+ import { paramsOf, renderParams, slotFieldsOf } from "./plugin.js";
11
12
  import { scopeFor } from "./script.js";
12
13
  export function needsLifetime(ty, comp, seen = new Set()) {
13
14
  switch (ty.k) {
14
- // A record's keys are strings.
15
15
  case "str":
16
16
  case "record":
17
17
  return true;
@@ -19,7 +19,6 @@ export function needsLifetime(ty, comp, seen = new Set()) {
19
19
  case "list":
20
20
  return needsLifetime(ty.of, comp, seen);
21
21
  case "struct": {
22
- // A struct that holds itself borrows only if something else in it does.
23
22
  if (seen.has(ty.name))
24
23
  return false;
25
24
  const { st, owner } = lookupStruct(comp, ty);
@@ -30,7 +29,6 @@ export function needsLifetime(ty, comp, seen = new Set()) {
30
29
  const child = childOf(ty.name);
31
30
  return structLifetime(child.props, child);
32
31
  }
33
- // A configured type that borrows says so by naming the props' lifetime: `Trusted<'a>`.
34
32
  case "html":
35
33
  return ctx.trustedHtml?.includes("'a") ?? false;
36
34
  default:
@@ -72,8 +70,6 @@ export function rustTy(ty, comp) {
72
70
  throw new GenError("no Rust type for `undefined`");
73
71
  }
74
72
  }
75
- /** A constructor parameter for a field of type \`ty\`, and how its argument becomes the field's
76
- * value: strings from anything that turns into a \`Cow\`, lists of them from any iterator. */
77
73
  function param(ty, comp, arg) {
78
74
  if (ty.k === "str")
79
75
  return { ty: "impl Into<Cow<'a, str>>", value: `${arg}.into()` };
@@ -82,12 +78,10 @@ function param(ty, comp, arg) {
82
78
  }
83
79
  return { ty: rustTy(ty, comp), value: arg };
84
80
  }
85
- /** \`new\` with every required field, and a chainable setter for each optional one, so building
86
- * props from Rust takes neither \`Cow\` nor \`None\`. */
87
81
  function builderSource(st, comp, life) {
88
82
  const required = st.fields.filter((f) => f.ty.k !== "opt");
89
83
  const optional = st.fields.filter((f) => f.ty.k === "opt");
90
- const arg = (f) => f.rust.replace(/^r#/, "");
84
+ const arg = (f) => f.rust;
91
85
  const params = required.map((f) => `${arg(f)}: ${param(f.ty, comp, arg(f)).ty}`).join(", ");
92
86
  const inits = st.fields
93
87
  .map((f) => (f.ty.k === "opt" ? `${f.rust}: None` : fieldInit(f.rust, param(f.ty, comp, arg(f)).value)))
@@ -105,7 +99,6 @@ function builderSource(st, comp, life) {
105
99
  })
106
100
  .join("\n");
107
101
  const usesA = /'a/.test(params + setters);
108
- // A struct without strings has no lifetime for its constructor to name.
109
102
  const impl = life ? "impl<'a>" : usesA ? "impl<'a>" : "impl";
110
103
  return `${impl} ${st.name}${life} {
111
104
  /// ${st.name} with ${required.length ? "its required fields" : "nothing set"}${optional.length ? ", every optional one absent" : ""}.
@@ -126,9 +119,8 @@ export function structSource(st, comp, doc) {
126
119
  return `${attrs}\n pub ${f.rust}: ${rustTy(f.ty, comp)},`;
127
120
  })
128
121
  .join("\n");
129
- /* `Deserialize` only for the conformance suite. Props go out as JSON and never come back in, and a
130
- * `Deserialize` in production would be a way to make a `TrustedHtml` value out of any string. */
131
- return `${doc}#[derive(Debug, Clone, serde::Serialize)]
122
+ const derives = st.fields.some((f) => f.ty.k !== "opt") ? "Debug, Clone" : "Debug, Clone, Default";
123
+ return `${doc}#[derive(${derives}, serde::Serialize)]
132
124
  #[cfg_attr(test, derive(serde::Deserialize))]
133
125
  pub struct ${st.name}${life} {
134
126
  ${fields}
@@ -142,7 +134,6 @@ export function header(source, edit = "the `.vue` file") {
142
134
  // \`ferrovue\`.
143
135
  `;
144
136
  }
145
- /** Rust expressions summing the lengths of the strings a value of type `ty` at `place` holds. */
146
137
  export function textLen(comp, place, ty, seen = new Set()) {
147
138
  switch (ty.k) {
148
139
  case "str":
@@ -160,7 +151,6 @@ export function textLen(comp, place, ty, seen = new Set()) {
160
151
  }
161
152
  return [];
162
153
  case "struct": {
163
- // A recursive structure is counted to its first level: the reservation is an estimate.
164
154
  if (seen.has(ty.name))
165
155
  return [];
166
156
  const { st, owner } = lookupStruct(comp, ty);
@@ -179,27 +169,32 @@ export function textLen(comp, place, ty, seen = new Set()) {
179
169
  return [];
180
170
  }
181
171
  }
182
- /** `render`, and for a component a parent may hand scope ids to, `render_scoped`, which takes them
183
- * last, as `ssrRenderAttrs` writes them onto its root; `render` hands it none. */
184
172
  function renderSource(comp, life, args, e) {
185
173
  const doc = "/// Write the component's server render into `out`.\n";
186
- // Props or ids the render never reads, as when a component only passes its slot on.
187
174
  const props = e.reads("props", 0) ? "props" : "_props";
188
- if (!comp.inherits)
175
+ if (!comp.inherits && !takesAttrs(comp))
189
176
  return `${doc}pub fn render(out: &mut String, ${props}: &Props${life}${extraParams(comp)}) {\n${e.lines.join("\n")}\n}`;
190
- // A root that is a fragment, or a `<Teleport>`, takes no ids.
191
177
  const attrs = e.reads("fv_attrs", 0) ? "fv_attrs" : "_fv_attrs";
178
+ const [none, ty, what] = takesAttrs(comp)
179
+ ? ["&fv::Attrs::NONE", "&fv::Attrs<'_>", "the attributes a parent passes beyond the props, and the scope ids it hands the root"]
180
+ : ['""', "&str", "the scope ids a parent hands the root: ` data-v-…` each"];
192
181
  return `${doc}pub fn render(out: &mut String, props: &Props${life}${extraParams(comp)}) {
193
- render_scoped(out, props${args}, "");
182
+ render_scoped(out, props${args}, ${none});
194
183
  }
195
184
 
196
- /// [\`render\`], with the scope ids a parent hands the root: \` data-v-…\` each.
185
+ /// [\`render\`], with ${what}.
197
186
  #[doc(hidden)]
198
- pub fn render_scoped(out: &mut String, ${props}: &Props${life}${extraParams(comp)}, ${attrs}: &str) {
187
+ pub fn render_scoped(out: &mut String, ${props}: &Props${life}${extraParams(comp)}, ${attrs}: ${ty}) {
199
188
  ${e.lines.join("\n")}
200
189
  }`;
201
190
  }
191
+ export function isIsland(comp) {
192
+ return !takesSlots(comp) && comp.takes.size === 0;
193
+ }
202
194
  export function componentSource(comp, ast, ssr, components) {
195
+ if (takesAttrs(comp) && comp.inheritAttrs && comp.attrsDropped) {
196
+ fail(comp, `a root \`<Transition>\` or \`<KeepAlive>\` around a \`v-if\` in ${comp.name}, which a parent passes attributes: Vue's server drops them, where its client puts them on the element; set \`inheritAttrs: false\` and bind \`$attrs\` on the element`, comp.attrsDropped);
197
+ }
203
198
  const { scope, lets } = scopeFor(comp, ast, components);
204
199
  const program = parseJs(ssr, { sourceType: "module" }).program;
205
200
  tagAst(program, "template");
@@ -210,18 +205,12 @@ export function componentSource(comp, ast, ssr, components) {
210
205
  for (const l of lets)
211
206
  e.stmt(l);
212
207
  statements(scope, e, fn.body.body);
213
- // A setup value the render never reads is not computed. Checked from the last up, since one may
214
- // read another declared before it.
215
208
  for (let i = lets.length - 1; i >= 0; i--) {
216
209
  const name = /^let (\w+)/.exec(lets[i])[1];
217
210
  if (!e.reads(name, i + 1))
218
211
  e.lines.splice(i, 1);
219
212
  }
220
213
  e.flush();
221
- /* The literal markup, a loop's once per item, what the helpers can write, and every string the
222
- * props hold. Literals in untaken branches overcount and escaping or a string used twice
223
- * undercounts, so it is an estimate — close enough that the buffer is sized once, where
224
- * reserving the literals alone leaves it to grow again at the first long label. */
225
214
  const text = textLen(comp, "props", { k: "struct", name: "Props" });
226
215
  const fixed = e.literalBytes + scope.helperBytes.n;
227
216
  const reserve = [...(fixed || !(e.perItem.length + text.length) ? [String(fixed)] : []), ...e.perItem, ...text];
@@ -229,22 +218,21 @@ export function componentSource(comp, ast, ssr, components) {
229
218
  const life = structLifetime(comp.props, comp) ? "<'_>" : "";
230
219
  const gen = life ? "<'p, 'a>" : "<'p>";
231
220
  const named = life ? "<'a>" : "";
232
- // An `interface Props` that `defineProps` takes is the props struct itself, written once below.
221
+ const owned = life ? "<'a>" : "";
222
+ const ownedLife = life ? "'a" : "'static";
233
223
  const structs = [...comp.structs.values()]
234
224
  .filter((st) => st.name !== "Props" && !st.slot)
235
225
  .map((st) => structSource(st, comp, `/// \`${st.name}\` in \`${basename(comp.file)}\`.\n`))
236
226
  .join("\n");
237
- // `Cow` is imported only where a field is one: a lifetime that comes from another component's props
238
- // alone borrows through that type, not through a `Cow` written here.
239
227
  const usesCow = /Cow</.test(structs + structSource(comp.props, comp, ""));
240
- const plain = !takesSlots(comp) && !comp.usesRoute && !comp.usesStores && !comp.usesI18n && !comp.usesTeleports;
228
+ const plain = isIsland(comp);
241
229
  const slotFields = [
242
230
  ...comp.slotNames.map((n) => {
243
231
  const outlet = `\`<slot${n === "default" ? "" : ` name="${n}"`}>\``;
244
232
  const type = comp.slotShapes.has(n) ? `&'s ${slotTypeName(n, "Slot")}<'s>` : "fv::Slot<'s>";
245
233
  return ` /// ${outlet}\n pub ${snake(n)}: Option<${type}>,`;
246
234
  }),
247
- ...(comp.routerView ? [" /// The page `<RouterView>` shows.\n pub router_view: fv::Slot<'s>,"] : []),
235
+ ...slotFieldsOf(comp).map((f) => ` /// ${f.doc}\n pub ${f.rust}: fv::Slot<'s>,`),
248
236
  ];
249
237
  const slotTypes = [...comp.slotShapes.entries()]
250
238
  .map(([n, shape]) => {
@@ -265,19 +253,15 @@ pub type ${slotTypeName(n, "Slot")}<'s> = dyn ${borrows ? "for<'v> " : ""}Fn(&mu
265
253
  .join("");
266
254
  const slotsStruct = takesSlots(comp)
267
255
  ? `${slotTypes}/// What a parent puts in the slots \`${basename(comp.file)}\` renders.
268
- #[derive(Clone, Copy${comp.routerView ? "" : ", Default"})]
256
+ #[derive(Clone, Copy${slotFieldsOf(comp).length ? "" : ", Default"})]
269
257
  pub struct Slots<'s> {
270
258
  ${slotFields.join("\n")}
271
259
  }
272
260
 
273
261
  `
274
262
  : "";
275
- const args = (takesSlots(comp) ? ", fv_slots" : "") + (comp.usesRoute ? ", fv_route" : "") + (comp.usesStores ? ", fv_stores" : "") + (comp.usesI18n ? ", fv_i18n" : "") + (comp.usesTeleports ? ", fv_teleports" : "");
276
- const params = (takesSlots(comp) ? ", fv_slots: Slots<'p>" : "") +
277
- (comp.usesRoute ? ", fv_route: &'p fv::Route<'p>" : "") +
278
- (comp.usesStores ? ", fv_stores: &'p super::stores::Stores<'p>" : "") +
279
- (comp.usesI18n ? ", fv_i18n: &'p fv::I18n" : "") +
280
- (comp.usesTeleports ? ", fv_teleports: &'p fv::Teleports" : "");
263
+ const args = (takesSlots(comp) ? ", fv_slots" : "") + paramsOf(comp).map((p) => `, ${p.name}`).join("");
264
+ const params = (takesSlots(comp) ? ", fv_slots: Slots<'p>" : "") + paramsOf(comp).map((p) => `, ${p.name}: ${p.pageTy}`).join("");
281
265
  const wrappers = plain
282
266
  ? `/// The component's markup, for a maud page that shows it without hydrating it.
283
267
  pub fn html${gen}(props: &'p Props${named}) -> fv::Html<'p, Props${named}> {
@@ -288,6 +272,16 @@ pub fn html${gen}(props: &'p Props${named}) -> fv::Html<'p, Props${named}> {
288
272
  pub fn island${gen}(props: &'p Props${named}) -> fv::Html<'p, Props${named}> {
289
273
  fv::Html::island(NAME, props, render)
290
274
  }
275
+
276
+ /// [\`html\`], holding the props: a handler that builds them can return it.
277
+ pub fn into_html${owned}(props: Props${named}) -> fv::Html<${ownedLife}, Props${named}> {
278
+ fv::Html::markup_owned(props, render)
279
+ }
280
+
281
+ /// [\`island\`], holding the props.
282
+ pub fn into_island${owned}(props: Props${named}) -> fv::Html<${ownedLife}, Props${named}> {
283
+ fv::Html::island_owned(NAME, props, render)
284
+ }
291
285
  `
292
286
  : `/// The component's markup, for a maud page that shows it.
293
287
  pub fn html${gen}(props: &'p Props${named}${params}) -> fv::Html<'p, Props${named}, impl Fn(&mut String, &Props${named}) + 'p> {
@@ -305,20 +299,22 @@ ${slotsStruct}${renderSource(comp, life, args, e)}
305
299
 
306
300
  ${wrappers}`;
307
301
  }
308
- export function modSource(comps) {
302
+ function readsFixture(c) {
303
+ return takesSlots(c) || paramsOf(c).some((p) => p.test.fixture);
304
+ }
305
+ export function modSource(comps, modules) {
309
306
  const arms = comps
310
307
  .map((c) => {
311
308
  const lines = [`let props: ${c.module}::Props = serde_json::from_str(json).map_err(|e| e.to_string())?;`];
312
309
  let args = "";
313
- if (takesSlots(c) || c.usesRoute || c.usesStores || c.usesI18n)
310
+ if (readsFixture(c))
314
311
  lines.push("let fixture: Fixture = serde_json::from_str(json).map_err(|e| e.to_string())?;");
315
312
  if (takesSlots(c)) {
316
- const names = [...c.slotNames, ...(c.routerView ? ["routerView"] : [])];
313
+ const names = [...c.slotNames, ...slotFieldsOf(c).map((f) => f.js)];
317
314
  for (const n of names) {
318
315
  const local = `s_${snake(n).replace(/^r#/, "")}`;
319
316
  const shape = c.slotShapes.get(n);
320
317
  if (shape) {
321
- // A fixture's content for a scoped slot is static: it is given the props and ignores them.
322
318
  const life = shape.fields.some((f) => slotFieldBorrows(f.ty)) ? "<'_>" : "";
323
319
  lines.push(`let ${local} = |out: &mut String, _: &${c.module}::${shape.name}${life}${c.passesSlotIds ? ", _: &str" : ""}| -> bool { out.push_str(fixture.slot(${rustStr(n)}).unwrap_or_default()); true };`);
324
320
  }
@@ -330,34 +326,23 @@ export function modSource(comps) {
330
326
  const value = c.slotShapes.has(n) ? `&${local} as &${c.module}::${slotTypeName(n, "Slot")}` : `ferrovue::Slot::new(&${local})`;
331
327
  return `${snake(n)}: fixture.slot(${rustStr(n)}).map(|_| ${value})`;
332
328
  });
333
- if (c.routerView)
334
- fields.push("router_view: ferrovue::Slot::new(&s_router_view)");
329
+ for (const f of slotFieldsOf(c))
330
+ fields.push(`${f.rust}: ferrovue::Slot::new(&s_${snake(f.js).replace(/^r#/, "")})`);
335
331
  args += `, ${c.module}::Slots { ${fields.join(", ")} }`;
336
332
  }
337
- if (c.usesRoute) {
338
- lines.push("let router = route_table::router();", "let route = router.at(&fixture.route);");
339
- args += ", &route";
340
- }
341
- if (c.usesStores) {
342
- lines.push("let state: stores::Stores = serde_json::from_value(fixture.stores.clone()).map_err(|e| e.to_string())?;");
343
- args += ", &state";
344
- }
345
- if (c.usesI18n) {
346
- lines.push("let i18n = i18n::i18n(fixture.locale.as_deref().unwrap_or(i18n::LOCALE));");
347
- args += ", &i18n";
348
- }
349
- if (c.usesTeleports) {
350
- lines.push("let teleports = ferrovue::Teleports::new();");
351
- args += ", &teleports";
333
+ for (const p of paramsOf(c)) {
334
+ lines.push(...p.test.lines);
335
+ args += `, ${p.test.arg}`;
352
336
  }
353
337
  lines.push(`${c.module}::render(&mut out, &props${args});`);
354
- // What was teleported follows the render, as the conformance suite records Vue's.
355
- if (c.usesTeleports)
356
- lines.push("teleports_into(&mut out, teleports);");
338
+ for (const p of paramsOf(c))
339
+ lines.push(...(p.test.after ?? []));
357
340
  return ` ${rustStr(c.name)} => {\n${lines.map((l) => " " + l).join("\n")}\n }`;
358
341
  })
359
342
  .join("\n");
360
- const fixture = comps.some((c) => takesSlots(c) || c.usesRoute || c.usesStores || c.usesI18n)
343
+ const fields = renderParams().flatMap((p) => (p.fixtureField === undefined ? [] : [p.fixtureField]));
344
+ const defaults = renderParams().flatMap((p) => (p.fixtureDefault === undefined ? [] : [p.fixtureDefault]));
345
+ const fixture = comps.some(readsFixture)
361
346
  ? `
362
347
  /// What a fixture holds besides the props: each slot's content, and the location it renders at.
363
348
  #[cfg(test)]
@@ -365,25 +350,12 @@ export function modSource(comps) {
365
350
  struct Fixture {
366
351
  #[serde(rename = "$slots", default)]
367
352
  slots: std::collections::HashMap<String, String>,
368
- #[serde(rename = "$route", default = "Fixture::root")]
369
- route: String,
370
- #[serde(rename = "$stores", default = "Fixture::no_stores")]
371
- stores: serde_json::Value,
372
- #[serde(rename = "$locale", default)]
373
- locale: Option<String>,
353
+ ${fields.join("\n")}
374
354
  }
375
355
 
376
356
  #[cfg(test)]
377
357
  impl Fixture {
378
- fn root() -> String {
379
- "/".to_owned()
380
- }
381
-
382
- fn no_stores() -> serde_json::Value {
383
- serde_json::Value::Object(Default::default())
384
- }
385
-
386
- fn slot(&self, name: &str) -> Option<&str> {
358
+ ${defaults.map((d) => `${d}\n\n`).join("")} fn slot(&self, name: &str) -> Option<&str> {
387
359
  self.slots.get(name).map(String::as_str)
388
360
  }
389
361
  }
@@ -393,29 +365,17 @@ impl Fixture {
393
365
  //! The component renderers, one module per \`.vue\` file.
394
366
 
395
367
  // The modules pass rustc's default warnings and clippy's default lints, with one exception:
396
- // \`dead_code\`. Every component gets the whole of its API (\`render\`, \`html\`, \`island\`, \`NAME\`, a
397
- // constructor and a setter per optional prop) and an app calls only what it needs.
368
+ // \`dead_code\`. Every component gets the whole of its API (\`render\`, \`html\`, \`island\`, their
369
+ // \`into_\` forms, \`NAME\`, a constructor and a setter per optional prop) and an app calls only what
370
+ // it needs.
398
371
  #![allow(dead_code)]
399
372
 
400
- ${comps.map((c) => `pub mod ${c.module};`).join("\n")}${ctx.routes ? "\npub mod route_table;" : ""}${ctx.stores.size ? "\npub mod stores;" : ""}${ctx.typeStructs.size ? "\npub mod types;" : ""}${ctx.i18n ? "\npub mod i18n;" : ""}
373
+ ${comps.map((c) => `pub mod ${c.module};`).join("\n")}${modules.map((m) => `\npub mod ${m.replace(/\.rs$/, "")};`).join("")}
401
374
  ${fixture}
402
- ${comps.some((c) => c.usesTeleports) ? `/// What was teleported, after a marker, as the conformance suite writes Vue's: \`{"target":"…"}\`.
403
- #[cfg(test)]
404
- fn teleports_into(out: &mut String, teleports: ferrovue::Teleports) {
405
- let targets = teleports.into_targets();
406
- if targets.is_empty() {
407
- return;
408
- }
409
- let pairs: Vec<String> = targets
410
- .iter()
411
- .map(|(t, html)| format!("{}:{}", serde_json::to_string(t).unwrap(), serde_json::to_string(html).unwrap()))
412
- .collect();
413
- out.push_str("<!--fv-teleports-->{");
414
- out.push_str(&pairs.join(","));
415
- out.push('}');
416
- }
417
-
418
- ` : ""}/// Render one component from its props as JSON, for the conformance suite.
375
+ ${renderParams()
376
+ .filter((p) => p.testSupport !== undefined && comps.some((c) => c.takes.has(p.name)))
377
+ .map((p) => `${p.testSupport}\n\n`)
378
+ .join("")}/// Render one component from its props as JSON, for the conformance suite.
419
379
  #[cfg(test)]
420
380
  pub fn render_json(component: &str, json: &str) -> Result<String, String> {
421
381
  let mut out = String::new();
@@ -427,28 +387,8 @@ ${arms}
427
387
  }
428
388
  `;
429
389
  }
430
- export function storesSource(dir) {
431
- const home = storeHome(dir);
432
- // Test-only `Default` lets a fixture name only the stores it reads; the rest are never looked at.
433
- const testDerive = (src) => src.replace("#[cfg_attr(test, derive(serde::Deserialize))]", "#[cfg_attr(test, derive(Default, serde::Deserialize))]\n#[cfg_attr(test, serde(default))]");
434
- const structs = [...ctx.storeStructs.values()]
435
- .map((st) => testDerive(structSource(st, home, `/// \`${st.name}\` in \`${ctx.storeFiles.get(st.name)}\`.\n`)))
436
- .join("\n");
437
- const all = {
438
- name: "Stores",
439
- fields: [...ctx.stores.values()].map((st) => ({ js: st.id, rust: st.field, ty: { k: "struct", name: st.state, store: true } })),
440
- };
441
- const top = testDerive(structSource(all, home, "/// Every store's state, keyed by id as `pinia.state.value` is: what the page sends the client.\n"));
442
- return `${header(dir, "the store files")}
443
- //! The Pinia stores' state, which components read while they render on the server.
444
-
445
- ${/Cow</.test(structs) ? "use std::borrow::Cow;\n\n" : ""}${structs}
446
- ${top}`;
447
- }
448
390
  export function typesSource() {
449
- const home = storeHome("types");
450
- home.structs = ctx.typeStructs;
451
- home.module = "types";
391
+ const home = blankComponent("types", "types", "types", ctx.typeStructs);
452
392
  const files = [...new Set(ctx.typeFiles.values())].toSorted();
453
393
  const structs = [...ctx.typeStructs.values()]
454
394
  .map((st) => structSource(st, home, `/// \`${st.name}\` in \`${ctx.typeFiles.get(st.name)}\`.\n`))
@@ -459,38 +399,3 @@ export function typesSource() {
459
399
 
460
400
  ${/Cow</.test(structs) ? "use std::borrow::Cow;\n\n" : ""}${structs}`;
461
401
  }
462
- /** Route definitions as \`ferrovue::RouteDef\` literals, children nested. */
463
- function routeDefs(routes, depth) {
464
- const pad = " ".repeat(depth);
465
- return routes
466
- .map((r) => {
467
- const name = r.name === undefined ? "None" : `Some(${rustStr(r.name)})`;
468
- const children = r.children?.length ? `&[\n${routeDefs(r.children, depth + 1)}\n${pad}]` : "&[]";
469
- return `${pad}ferrovue::RouteDef { path: ${rustStr(r.path)}, name: ${name}, children: ${children} },`;
470
- })
471
- .join("\n");
472
- }
473
- export function routesSource(routes, file) {
474
- return `${header(file, "the routes file")}
475
- //! The app's routes: what \`<RouterLink>\` resolves against and \`useRoute()\` reads.
476
-
477
- /// Each route: its vue-router path, its name if it has one, and the routes nested in it.
478
- pub const ROUTES: &[ferrovue::RouteDef<'static>] = &[
479
- ${routeDefs(routes, 1)}
480
- ];
481
-
482
- /// Every route's full path, nested ones included.
483
- pub const PATHS: &[&str] = &[
484
- ${allRoutes(routes).map((r) => ` ${rustStr(r.fullPath)},`).join("\n")}
485
- ];
486
-
487
- /// The history's base, which every link's \`href\` starts with.
488
- pub const BASE: &str = ${rustStr(ctx.routerBase)};
489
-
490
- /// The router these routes make: build it once, and resolve each request's location with
491
- /// [\`ferrovue::Router::at\`].
492
- pub fn router() -> ferrovue::Router {
493
- ferrovue::Router::tree(ROUTES).with_base(BASE)
494
- }
495
- `;
496
- }
package/dist/script.d.ts CHANGED
@@ -1,14 +1,8 @@
1
1
  import { type Component, type N, type Scope } from "./model.ts";
2
- /** Lifecycle hooks, which never run on the server: setup may register them freely. */
3
2
  export declare const CLIENT_HOOKS: Set<string>;
4
- /** Compiler macros and calls with no effect on the server's render. */
5
3
  export declare const INERT_CALLS: Set<string>;
6
- /** A statement in setup that is a call on its own: allowed only when it cannot change the render. */
7
4
  export declare function setupStatement(comp: Component, st: N): void;
8
- /** What a setup binding's value is computed from: `ref(x)` and `shallowRef(x)` hold `x`,
9
- * `computed(() => x)` is `x`, and a plain `const y = x` is `x`. */
10
5
  export declare function setupSource(init: N): N | null;
11
- /** The names a destructuring pattern binds. */
12
6
  export declare function patternNames(p: N): string[];
13
7
  export declare function scopeFor(comp: Component, ast: N[], components: Map<string, Component>): {
14
8
  scope: Scope;