ferrovue 0.3.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 (85) hide show
  1. package/README.md +32 -13
  2. package/dist/attrs.d.ts +2 -52
  3. package/dist/attrs.js +18 -448
  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 +0 -17
  11. package/dist/client.js +0 -12
  12. package/dist/compiler.js +25 -90
  13. package/dist/component.js +10 -86
  14. package/dist/context.d.ts +4 -66
  15. package/dist/context.js +2 -33
  16. package/dist/diff.d.ts +0 -3
  17. package/dist/diff.js +0 -9
  18. package/dist/emitter.d.ts +0 -8
  19. package/dist/emitter.js +0 -12
  20. package/dist/expr.d.ts +0 -70
  21. package/dist/expr.js +27 -890
  22. package/dist/fallthrough.d.ts +9 -0
  23. package/dist/fallthrough.js +78 -0
  24. package/dist/fixture.d.ts +2 -1
  25. package/dist/fixture.js +37 -22
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.js +1 -0
  28. package/dist/islands.d.ts +1 -0
  29. package/dist/islands.js +1 -0
  30. package/dist/lists.d.ts +0 -11
  31. package/dist/lists.js +4 -39
  32. package/dist/loops.d.ts +3 -0
  33. package/dist/loops.js +164 -0
  34. package/dist/model.d.ts +14 -143
  35. package/dist/model.js +27 -12
  36. package/dist/narrowing.d.ts +19 -0
  37. package/dist/narrowing.js +126 -0
  38. package/dist/numbers.d.ts +8 -0
  39. package/dist/numbers.js +73 -0
  40. package/dist/parens.d.ts +0 -27
  41. package/dist/parens.js +0 -44
  42. package/dist/plugin.d.ts +84 -0
  43. package/dist/plugin.js +26 -0
  44. package/dist/plugins/i18n.d.ts +23 -0
  45. package/dist/{i18n.js → plugins/i18n.js} +76 -19
  46. package/dist/plugins/index.d.ts +2 -0
  47. package/dist/plugins/index.js +8 -0
  48. package/dist/plugins/router-link.d.ts +8 -0
  49. package/dist/{router.js → plugins/router-link.js} +19 -61
  50. package/dist/plugins/router.d.ts +42 -0
  51. package/dist/plugins/router.js +256 -0
  52. package/dist/plugins/scoped.d.ts +12 -0
  53. package/dist/plugins/scoped.js +125 -0
  54. package/dist/plugins/shared-types.d.ts +2 -0
  55. package/dist/plugins/shared-types.js +7 -0
  56. package/dist/plugins/stores.d.ts +37 -0
  57. package/dist/plugins/stores.js +377 -0
  58. package/dist/plugins/teleport.d.ts +5 -0
  59. package/dist/plugins/teleport.js +55 -0
  60. package/dist/rust.d.ts +1 -7
  61. package/dist/rust.js +38 -160
  62. package/dist/script.d.ts +0 -6
  63. package/dist/script.js +13 -128
  64. package/dist/slots.d.ts +13 -0
  65. package/dist/slots.js +160 -0
  66. package/dist/strings.d.ts +13 -0
  67. package/dist/strings.js +208 -0
  68. package/dist/styles.d.ts +14 -0
  69. package/dist/styles.js +181 -0
  70. package/dist/template.d.ts +1 -42
  71. package/dist/template.js +14 -738
  72. package/dist/testing.d.ts +1 -0
  73. package/dist/testing.js +1 -8
  74. package/dist/types.d.ts +2 -0
  75. package/dist/typescript.d.ts +0 -13
  76. package/dist/typescript.js +8 -41
  77. package/dist/vite.d.ts +1 -0
  78. package/dist/vite.js +1 -23
  79. package/package.json +7 -2
  80. package/dist/i18n.d.ts +0 -27
  81. package/dist/router.d.ts +0 -17
  82. package/dist/scoped.d.ts +0 -24
  83. package/dist/scoped.js +0 -177
  84. package/dist/stores.d.ts +0 -6
  85. package/dist/stores.js +0 -229
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, takesAttrs } 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,18 +169,12 @@ 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 or pass attributes to,
183
- * `render_scoped`, which takes them last, as `ssrRenderAttrs` writes them onto its root; `render`
184
- * hands it none. */
185
172
  function renderSource(comp, life, args, e) {
186
173
  const doc = "/// Write the component's server render into `out`.\n";
187
- // Props or ids the render never reads, as when a component only passes its slot on.
188
174
  const props = e.reads("props", 0) ? "props" : "_props";
189
175
  if (!comp.inherits && !takesAttrs(comp))
190
176
  return `${doc}pub fn render(out: &mut String, ${props}: &Props${life}${extraParams(comp)}) {\n${e.lines.join("\n")}\n}`;
191
- // A root that is a fragment, or a `<Teleport>`, takes no ids.
192
177
  const attrs = e.reads("fv_attrs", 0) ? "fv_attrs" : "_fv_attrs";
193
- // A component a parent may pass attributes takes them with the ids, as one `fv::Attrs`.
194
178
  const [none, ty, what] = takesAttrs(comp)
195
179
  ? ["&fv::Attrs::NONE", "&fv::Attrs<'_>", "the attributes a parent passes beyond the props, and the scope ids it hands the root"]
196
180
  : ['""', "&str", "the scope ids a parent hands the root: ` data-v-…` each"];
@@ -204,10 +188,8 @@ pub fn render_scoped(out: &mut String, ${props}: &Props${life}${extraParams(comp
204
188
  ${e.lines.join("\n")}
205
189
  }`;
206
190
  }
207
- /** Whether the component gets an `island()`: it renders from its props alone, which `data-props` is
208
- * all the client gets to mount it with. */
209
191
  export function isIsland(comp) {
210
- return !takesSlots(comp) && !comp.usesRoute && !comp.usesStores && !comp.usesI18n && !comp.usesTeleports;
192
+ return !takesSlots(comp) && comp.takes.size === 0;
211
193
  }
212
194
  export function componentSource(comp, ast, ssr, components) {
213
195
  if (takesAttrs(comp) && comp.inheritAttrs && comp.attrsDropped) {
@@ -223,18 +205,12 @@ export function componentSource(comp, ast, ssr, components) {
223
205
  for (const l of lets)
224
206
  e.stmt(l);
225
207
  statements(scope, e, fn.body.body);
226
- // A setup value the render never reads is not computed. Checked from the last up, since one may
227
- // read another declared before it.
228
208
  for (let i = lets.length - 1; i >= 0; i--) {
229
209
  const name = /^let (\w+)/.exec(lets[i])[1];
230
210
  if (!e.reads(name, i + 1))
231
211
  e.lines.splice(i, 1);
232
212
  }
233
213
  e.flush();
234
- /* The literal markup, a loop's once per item, what the helpers can write, and every string the
235
- * props hold. Literals in untaken branches overcount and escaping or a string used twice
236
- * undercounts, so it is an estimate — close enough that the buffer is sized once, where
237
- * reserving the literals alone leaves it to grow again at the first long label. */
238
214
  const text = textLen(comp, "props", { k: "struct", name: "Props" });
239
215
  const fixed = e.literalBytes + scope.helperBytes.n;
240
216
  const reserve = [...(fixed || !(e.perItem.length + text.length) ? [String(fixed)] : []), ...e.perItem, ...text];
@@ -242,16 +218,12 @@ export function componentSource(comp, ast, ssr, components) {
242
218
  const life = structLifetime(comp.props, comp) ? "<'_>" : "";
243
219
  const gen = life ? "<'p, 'a>" : "<'p>";
244
220
  const named = life ? "<'a>" : "";
245
- // What holds its props borrows only what they borrow.
246
221
  const owned = life ? "<'a>" : "";
247
222
  const ownedLife = life ? "'a" : "'static";
248
- // An `interface Props` that `defineProps` takes is the props struct itself, written once below.
249
223
  const structs = [...comp.structs.values()]
250
224
  .filter((st) => st.name !== "Props" && !st.slot)
251
225
  .map((st) => structSource(st, comp, `/// \`${st.name}\` in \`${basename(comp.file)}\`.\n`))
252
226
  .join("\n");
253
- // `Cow` is imported only where a field is one: a lifetime that comes from another component's props
254
- // alone borrows through that type, not through a `Cow` written here.
255
227
  const usesCow = /Cow</.test(structs + structSource(comp.props, comp, ""));
256
228
  const plain = isIsland(comp);
257
229
  const slotFields = [
@@ -260,7 +232,7 @@ export function componentSource(comp, ast, ssr, components) {
260
232
  const type = comp.slotShapes.has(n) ? `&'s ${slotTypeName(n, "Slot")}<'s>` : "fv::Slot<'s>";
261
233
  return ` /// ${outlet}\n pub ${snake(n)}: Option<${type}>,`;
262
234
  }),
263
- ...(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>,`),
264
236
  ];
265
237
  const slotTypes = [...comp.slotShapes.entries()]
266
238
  .map(([n, shape]) => {
@@ -281,19 +253,15 @@ pub type ${slotTypeName(n, "Slot")}<'s> = dyn ${borrows ? "for<'v> " : ""}Fn(&mu
281
253
  .join("");
282
254
  const slotsStruct = takesSlots(comp)
283
255
  ? `${slotTypes}/// What a parent puts in the slots \`${basename(comp.file)}\` renders.
284
- #[derive(Clone, Copy${comp.routerView ? "" : ", Default"})]
256
+ #[derive(Clone, Copy${slotFieldsOf(comp).length ? "" : ", Default"})]
285
257
  pub struct Slots<'s> {
286
258
  ${slotFields.join("\n")}
287
259
  }
288
260
 
289
261
  `
290
262
  : "";
291
- const args = (takesSlots(comp) ? ", fv_slots" : "") + (comp.usesRoute ? ", fv_route" : "") + (comp.usesStores ? ", fv_stores" : "") + (comp.usesI18n ? ", fv_i18n" : "") + (comp.usesTeleports ? ", fv_teleports" : "");
292
- const params = (takesSlots(comp) ? ", fv_slots: Slots<'p>" : "") +
293
- (comp.usesRoute ? ", fv_route: &'p fv::Route<'p>" : "") +
294
- (comp.usesStores ? ", fv_stores: &'p super::stores::Stores<'p>" : "") +
295
- (comp.usesI18n ? ", fv_i18n: &'p fv::I18n" : "") +
296
- (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("");
297
265
  const wrappers = plain
298
266
  ? `/// The component's markup, for a maud page that shows it without hydrating it.
299
267
  pub fn html${gen}(props: &'p Props${named}) -> fv::Html<'p, Props${named}> {
@@ -331,20 +299,22 @@ ${slotsStruct}${renderSource(comp, life, args, e)}
331
299
 
332
300
  ${wrappers}`;
333
301
  }
334
- 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) {
335
306
  const arms = comps
336
307
  .map((c) => {
337
308
  const lines = [`let props: ${c.module}::Props = serde_json::from_str(json).map_err(|e| e.to_string())?;`];
338
309
  let args = "";
339
- if (takesSlots(c) || c.usesRoute || c.usesStores || c.usesI18n)
310
+ if (readsFixture(c))
340
311
  lines.push("let fixture: Fixture = serde_json::from_str(json).map_err(|e| e.to_string())?;");
341
312
  if (takesSlots(c)) {
342
- const names = [...c.slotNames, ...(c.routerView ? ["routerView"] : [])];
313
+ const names = [...c.slotNames, ...slotFieldsOf(c).map((f) => f.js)];
343
314
  for (const n of names) {
344
315
  const local = `s_${snake(n).replace(/^r#/, "")}`;
345
316
  const shape = c.slotShapes.get(n);
346
317
  if (shape) {
347
- // A fixture's content for a scoped slot is static: it is given the props and ignores them.
348
318
  const life = shape.fields.some((f) => slotFieldBorrows(f.ty)) ? "<'_>" : "";
349
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 };`);
350
320
  }
@@ -356,34 +326,23 @@ export function modSource(comps) {
356
326
  const value = c.slotShapes.has(n) ? `&${local} as &${c.module}::${slotTypeName(n, "Slot")}` : `ferrovue::Slot::new(&${local})`;
357
327
  return `${snake(n)}: fixture.slot(${rustStr(n)}).map(|_| ${value})`;
358
328
  });
359
- if (c.routerView)
360
- 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#/, "")})`);
361
331
  args += `, ${c.module}::Slots { ${fields.join(", ")} }`;
362
332
  }
363
- if (c.usesRoute) {
364
- lines.push("let router = route_table::router();", "let route = router.at(&fixture.route);");
365
- args += ", &route";
366
- }
367
- if (c.usesStores) {
368
- lines.push("let state: stores::Stores = serde_json::from_value(fixture.stores.clone()).map_err(|e| e.to_string())?;");
369
- args += ", &state";
370
- }
371
- if (c.usesI18n) {
372
- lines.push("let i18n = i18n::i18n(fixture.locale.as_deref().unwrap_or(i18n::LOCALE));");
373
- args += ", &i18n";
374
- }
375
- if (c.usesTeleports) {
376
- lines.push("let teleports = ferrovue::Teleports::new();");
377
- args += ", &teleports";
333
+ for (const p of paramsOf(c)) {
334
+ lines.push(...p.test.lines);
335
+ args += `, ${p.test.arg}`;
378
336
  }
379
337
  lines.push(`${c.module}::render(&mut out, &props${args});`);
380
- // What was teleported follows the render, as the conformance suite records Vue's.
381
- if (c.usesTeleports)
382
- lines.push("teleports_into(&mut out, teleports);");
338
+ for (const p of paramsOf(c))
339
+ lines.push(...(p.test.after ?? []));
383
340
  return ` ${rustStr(c.name)} => {\n${lines.map((l) => " " + l).join("\n")}\n }`;
384
341
  })
385
342
  .join("\n");
386
- 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)
387
346
  ? `
388
347
  /// What a fixture holds besides the props: each slot's content, and the location it renders at.
389
348
  #[cfg(test)]
@@ -391,25 +350,12 @@ export function modSource(comps) {
391
350
  struct Fixture {
392
351
  #[serde(rename = "$slots", default)]
393
352
  slots: std::collections::HashMap<String, String>,
394
- #[serde(rename = "$route", default = "Fixture::root")]
395
- route: String,
396
- #[serde(rename = "$stores", default = "Fixture::no_stores")]
397
- stores: serde_json::Value,
398
- #[serde(rename = "$locale", default)]
399
- locale: Option<String>,
353
+ ${fields.join("\n")}
400
354
  }
401
355
 
402
356
  #[cfg(test)]
403
357
  impl Fixture {
404
- fn root() -> String {
405
- "/".to_owned()
406
- }
407
-
408
- fn no_stores() -> serde_json::Value {
409
- serde_json::Value::Object(Default::default())
410
- }
411
-
412
- fn slot(&self, name: &str) -> Option<&str> {
358
+ ${defaults.map((d) => `${d}\n\n`).join("")} fn slot(&self, name: &str) -> Option<&str> {
413
359
  self.slots.get(name).map(String::as_str)
414
360
  }
415
361
  }
@@ -424,25 +370,12 @@ impl Fixture {
424
370
  // it needs.
425
371
  #![allow(dead_code)]
426
372
 
427
- ${comps.map((c) => `pub mod ${c.module};`).join("\n")}${ctx.routes ? "\npub mod route_table;" : ""}${ctx.stores.size ? "\npub mod stores;" : ""}${ctx.typeStructs.size ? "\npub mod types;" : ""}${ctx.i18n ? "\npub mod i18n;" : ""}
373
+ ${comps.map((c) => `pub mod ${c.module};`).join("\n")}${modules.map((m) => `\npub mod ${m.replace(/\.rs$/, "")};`).join("")}
428
374
  ${fixture}
429
- ${comps.some((c) => c.usesTeleports) ? `/// What was teleported, after a marker, as the conformance suite writes Vue's: \`{"target":"…"}\`.
430
- #[cfg(test)]
431
- fn teleports_into(out: &mut String, teleports: ferrovue::Teleports) {
432
- let targets = teleports.into_targets();
433
- if targets.is_empty() {
434
- return;
435
- }
436
- let pairs: Vec<String> = targets
437
- .iter()
438
- .map(|(t, html)| format!("{}:{}", serde_json::to_string(t).unwrap(), serde_json::to_string(html).unwrap()))
439
- .collect();
440
- out.push_str("<!--fv-teleports-->{");
441
- out.push_str(&pairs.join(","));
442
- out.push('}');
443
- }
444
-
445
- ` : ""}/// 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.
446
379
  #[cfg(test)]
447
380
  pub fn render_json(component: &str, json: &str) -> Result<String, String> {
448
381
  let mut out = String::new();
@@ -454,28 +387,8 @@ ${arms}
454
387
  }
455
388
  `;
456
389
  }
457
- export function storesSource(dir) {
458
- const home = storeHome(dir);
459
- // Test-only `Default` lets a fixture name only the stores it reads; the rest are never looked at.
460
- const testDerive = (src) => src.replace("#[cfg_attr(test, derive(serde::Deserialize))]", "#[cfg_attr(test, derive(Default, serde::Deserialize))]\n#[cfg_attr(test, serde(default))]");
461
- const structs = [...ctx.storeStructs.values()]
462
- .map((st) => testDerive(structSource(st, home, `/// \`${st.name}\` in \`${ctx.storeFiles.get(st.name)}\`.\n`)))
463
- .join("\n");
464
- const all = {
465
- name: "Stores",
466
- fields: [...ctx.stores.values()].map((st) => ({ js: st.id, rust: st.field, ty: { k: "struct", name: st.state, store: true } })),
467
- };
468
- 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"));
469
- return `${header(dir, "the store files")}
470
- //! The Pinia stores' state, which components read while they render on the server.
471
-
472
- ${/Cow</.test(structs) ? "use std::borrow::Cow;\n\n" : ""}${structs}
473
- ${top}`;
474
- }
475
390
  export function typesSource() {
476
- const home = storeHome("types");
477
- home.structs = ctx.typeStructs;
478
- home.module = "types";
391
+ const home = blankComponent("types", "types", "types", ctx.typeStructs);
479
392
  const files = [...new Set(ctx.typeFiles.values())].toSorted();
480
393
  const structs = [...ctx.typeStructs.values()]
481
394
  .map((st) => structSource(st, home, `/// \`${st.name}\` in \`${ctx.typeFiles.get(st.name)}\`.\n`))
@@ -486,38 +399,3 @@ export function typesSource() {
486
399
 
487
400
  ${/Cow</.test(structs) ? "use std::borrow::Cow;\n\n" : ""}${structs}`;
488
401
  }
489
- /** Route definitions as \`ferrovue::RouteDef\` literals, children nested. */
490
- function routeDefs(routes, depth) {
491
- const pad = " ".repeat(depth);
492
- return routes
493
- .map((r) => {
494
- const name = r.name === undefined ? "None" : `Some(${rustStr(r.name)})`;
495
- const children = r.children?.length ? `&[\n${routeDefs(r.children, depth + 1)}\n${pad}]` : "&[]";
496
- return `${pad}ferrovue::RouteDef { path: ${rustStr(r.path)}, name: ${name}, children: ${children} },`;
497
- })
498
- .join("\n");
499
- }
500
- export function routesSource(routes, file) {
501
- return `${header(file, "the routes file")}
502
- //! The app's routes: what \`<RouterLink>\` resolves against and \`useRoute()\` reads.
503
-
504
- /// Each route: its vue-router path, its name if it has one, and the routes nested in it.
505
- pub const ROUTES: &[ferrovue::RouteDef<'static>] = &[
506
- ${routeDefs(routes, 1)}
507
- ];
508
-
509
- /// Every route's full path, nested ones included.
510
- pub const PATHS: &[&str] = &[
511
- ${allRoutes(routes).map((r) => ` ${rustStr(r.fullPath)},`).join("\n")}
512
- ];
513
-
514
- /// The history's base, which every link's \`href\` starts with.
515
- pub const BASE: &str = ${rustStr(ctx.routerBase)};
516
-
517
- /// The router these routes make: build it once, and resolve each request's location with
518
- /// [\`ferrovue::Router::at\`].
519
- pub fn router() -> ferrovue::Router {
520
- ferrovue::Router::tree(ROUTES).with_base(BASE)
521
- }
522
- `;
523
- }
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;