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.
- package/README.md +103 -15
- package/dist/attrs.d.ts +8 -42
- package/dist/attrs.js +148 -297
- package/dist/calls.d.ts +4 -0
- package/dist/calls.js +219 -0
- package/dist/children.d.ts +9 -0
- package/dist/children.js +273 -0
- package/dist/classes.d.ts +13 -0
- package/dist/classes.js +176 -0
- package/dist/cli.js +121 -27
- package/dist/client.d.ts +11 -2
- package/dist/client.js +45 -9
- package/dist/compiler.d.ts +4 -0
- package/dist/compiler.js +37 -90
- package/dist/component.js +19 -82
- package/dist/context.d.ts +6 -68
- package/dist/context.js +25 -38
- package/dist/diff.d.ts +1 -0
- package/dist/diff.js +68 -0
- package/dist/emitter.d.ts +0 -8
- package/dist/emitter.js +0 -12
- package/dist/expr.d.ts +0 -70
- package/dist/expr.js +30 -855
- package/dist/fallthrough.d.ts +9 -0
- package/dist/fallthrough.js +78 -0
- package/dist/fixture.d.ts +2 -1
- package/dist/fixture.js +37 -22
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/islands.d.ts +6 -0
- package/dist/islands.js +5 -0
- package/dist/lists.d.ts +0 -11
- package/dist/lists.js +4 -39
- package/dist/loops.d.ts +3 -0
- package/dist/loops.js +164 -0
- package/dist/model.d.ts +22 -130
- package/dist/model.js +37 -9
- package/dist/narrowing.d.ts +19 -0
- package/dist/narrowing.js +126 -0
- package/dist/numbers.d.ts +8 -0
- package/dist/numbers.js +73 -0
- package/dist/parens.d.ts +0 -27
- package/dist/parens.js +0 -44
- package/dist/plugin.d.ts +84 -0
- package/dist/plugin.js +26 -0
- package/dist/plugins/i18n.d.ts +23 -0
- package/dist/{i18n.js → plugins/i18n.js} +76 -19
- package/dist/plugins/index.d.ts +2 -0
- package/dist/plugins/index.js +8 -0
- package/dist/plugins/router-link.d.ts +8 -0
- package/dist/{router.js → plugins/router-link.js} +65 -57
- package/dist/plugins/router.d.ts +42 -0
- package/dist/plugins/router.js +256 -0
- package/dist/plugins/scoped.d.ts +12 -0
- package/dist/{scoped.js → plugins/scoped.js} +55 -20
- package/dist/plugins/shared-types.d.ts +2 -0
- package/dist/plugins/shared-types.js +7 -0
- package/dist/plugins/stores.d.ts +37 -0
- package/dist/plugins/stores.js +377 -0
- package/dist/plugins/teleport.d.ts +5 -0
- package/dist/plugins/teleport.js +55 -0
- package/dist/rust.d.ts +2 -5
- package/dist/rust.js +66 -161
- package/dist/script.d.ts +0 -6
- package/dist/script.js +22 -121
- package/dist/slots.d.ts +13 -0
- package/dist/slots.js +160 -0
- package/dist/strings.d.ts +13 -0
- package/dist/strings.js +208 -0
- package/dist/styles.d.ts +14 -0
- package/dist/styles.js +181 -0
- package/dist/template.d.ts +2 -40
- package/dist/template.js +40 -691
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +1 -8
- package/dist/types.d.ts +2 -0
- package/dist/typescript.d.ts +0 -13
- package/dist/typescript.js +8 -41
- package/dist/vite.d.ts +1 -0
- package/dist/vite.js +35 -14
- package/package.json +11 -2
- package/dist/i18n.d.ts +0 -27
- package/dist/router.d.ts +0 -17
- package/dist/scoped.d.ts +0 -10
- package/dist/stores.d.ts +0 -6
- 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 {
|
|
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 {
|
|
10
|
-
import {
|
|
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
|
|
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
|
-
|
|
130
|
-
|
|
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
|
|
185
|
+
/// [\`render\`], with ${what}.
|
|
197
186
|
#[doc(hidden)]
|
|
198
|
-
pub fn render_scoped(out: &mut String, ${props}: &Props${life}${extraParams(comp)}, ${attrs}:
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
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
|
-
|
|
334
|
-
fields.push(
|
|
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
|
-
|
|
338
|
-
lines.push(
|
|
339
|
-
args +=
|
|
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
|
-
|
|
355
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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\`,
|
|
397
|
-
// constructor and a setter per optional prop) and an app calls only what
|
|
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")}${
|
|
373
|
+
${comps.map((c) => `pub mod ${c.module};`).join("\n")}${modules.map((m) => `\npub mod ${m.replace(/\.rs$/, "")};`).join("")}
|
|
401
374
|
${fixture}
|
|
402
|
-
${
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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 =
|
|
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;
|