ferrovue 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +198 -34
- package/dist/attrs.d.ts +1 -1
- package/dist/attrs.js +24 -22
- package/dist/calls.js +17 -15
- package/dist/children.d.ts +9 -2
- package/dist/children.js +48 -35
- package/dist/classes.js +11 -11
- package/dist/cli.js +31 -5
- package/dist/client.d.ts +35 -2
- package/dist/client.js +77 -2
- package/dist/compiler.d.ts +1 -1
- package/dist/compiler.js +7 -3
- package/dist/component.d.ts +3 -1
- package/dist/component.js +114 -19
- package/dist/constants.d.ts +42 -0
- package/dist/constants.js +292 -0
- package/dist/context.d.ts +25 -1
- package/dist/context.js +11 -5
- package/dist/diagnostics.d.ts +23 -0
- package/dist/diagnostics.js +30 -0
- package/dist/errors.d.ts +867 -0
- package/dist/errors.js +330 -0
- package/dist/expr.d.ts +1 -0
- package/dist/expr.js +120 -49
- package/dist/fixture.d.ts +3 -10
- package/dist/fixture.js +2 -6
- package/dist/hydration.d.ts +15 -0
- package/dist/hydration.js +80 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/link-router.d.ts +24 -0
- package/dist/link-router.js +36 -0
- package/dist/lists.js +30 -21
- package/dist/loops.js +14 -10
- package/dist/model.d.ts +32 -1
- package/dist/model.js +123 -6
- package/dist/narrowing.d.ts +8 -0
- package/dist/narrowing.js +40 -9
- package/dist/plugins/client-only.d.ts +8 -0
- package/dist/plugins/client-only.js +107 -0
- package/dist/plugins/i18n.js +14 -14
- package/dist/plugins/index.js +3 -1
- package/dist/plugins/router-link.js +33 -30
- package/dist/plugins/router.js +9 -9
- package/dist/plugins/scoped.js +1 -1
- package/dist/plugins/shared-types.js +1 -1
- package/dist/plugins/stores.js +26 -24
- package/dist/plugins/teleport.js +3 -3
- package/dist/plugins/twins.d.ts +10 -0
- package/dist/plugins/twins.js +164 -0
- package/dist/routes.d.ts +10 -0
- package/dist/routes.js +6 -0
- package/dist/rust.js +18 -14
- package/dist/script.d.ts +3 -0
- package/dist/script.js +119 -17
- package/dist/settle.d.ts +6 -0
- package/dist/settle.js +34 -0
- package/dist/slots.js +18 -18
- package/dist/strings.js +11 -11
- package/dist/styles.js +13 -13
- package/dist/template.js +11 -11
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +3 -2
- package/dist/typescript.d.ts +2 -1
- package/dist/typescript.js +46 -19
- package/dist/vite.d.ts +1 -1
- package/dist/vite.js +10 -9
- package/package.json +9 -5
package/dist/expr.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BOOL, fail, FLOAT, GenError, INT, opt, rustStr, sameTy, snake, STR, UNDEF } from "./model.js";
|
|
1
|
+
import { absence, BOOL, fail, FLOAT, GenError, INT, joinAbsence, nothing, NULL, opt, rustStr, sameTy, snake, STR, UNDEF, withAbsence } from "./model.js";
|
|
2
2
|
import { ctx } from "./context.js";
|
|
3
3
|
import { lookupStruct, markHome } from "./typescript.js";
|
|
4
4
|
import { claim } from "./plugin.js";
|
|
@@ -6,8 +6,9 @@ import { atom, bare, binary, enclosed, logical, negate, not, occurrences, strArg
|
|
|
6
6
|
import { collected } from "./lists.js";
|
|
7
7
|
import { asCow, formatted, isTemporary, loneOf, meet, stringsEqual } from "./strings.js";
|
|
8
8
|
import { arithmetic, asF64, compare, intFromF64, isNumber, negatedOrder, numberVal } from "./numbers.js";
|
|
9
|
-
import { boolOf, choice, known, narrowing, narrowTo, pathOf, presence, truthy } from "./narrowing.js";
|
|
9
|
+
import { boolOf, checkNullTest, choice, known, narrowing, narrowTo, nullTest, pathOf, presence, truthy } from "./narrowing.js";
|
|
10
10
|
import { call, isObjectCall } from "./calls.js";
|
|
11
|
+
import { constOf, constVal } from "./constants.js";
|
|
11
12
|
export function describeTy(ty) {
|
|
12
13
|
switch (ty.k) {
|
|
13
14
|
case "str":
|
|
@@ -26,20 +27,26 @@ export function describeTy(ty) {
|
|
|
26
27
|
case "child":
|
|
27
28
|
return "an object";
|
|
28
29
|
case "opt":
|
|
30
|
+
if (ty.none === "null")
|
|
31
|
+
return `${describeTy(ty.of)} or \`null\``;
|
|
32
|
+
if (ty.none === "either")
|
|
33
|
+
return `${describeTy(ty.of)}, \`null\` or \`undefined\``;
|
|
29
34
|
return `an optional ${describeTy(ty.of).replace(/^an? /, "")}`;
|
|
30
35
|
case "undef":
|
|
31
36
|
return "`undefined`";
|
|
37
|
+
case "null":
|
|
38
|
+
return "`null`";
|
|
32
39
|
default:
|
|
33
40
|
return claim((p) => p.values?.describe?.(ty)) ?? "a value of another kind";
|
|
34
41
|
}
|
|
35
42
|
}
|
|
36
43
|
export function fieldVal(comp, base, ty, js, node) {
|
|
37
44
|
if (ty.k !== "struct")
|
|
38
|
-
fail(comp, `\`.${js}\` on a value that is not an object`, node);
|
|
45
|
+
fail(comp, "FV0604", `\`.${js}\` on a value that is not an object`, node);
|
|
39
46
|
const { st, owner } = lookupStruct(comp, ty);
|
|
40
47
|
const found = st?.fields.find((x) => x.js === js);
|
|
41
48
|
if (!found)
|
|
42
|
-
fail(comp, `\`${ty.name}\` has no field \`${js}\``, node);
|
|
49
|
+
fail(comp, "FV0605", `\`${ty.name}\` has no field \`${js}\``, node);
|
|
43
50
|
const f = owner === comp ? found : { ...found, ty: markHome(found.ty, owner.name) };
|
|
44
51
|
if (st.slot)
|
|
45
52
|
return { code: `${base}.${f.rust}`, ty: f.ty };
|
|
@@ -70,25 +77,49 @@ export function fieldVal(comp, base, ty, js, node) {
|
|
|
70
77
|
return { code: place, ty: f.ty };
|
|
71
78
|
}
|
|
72
79
|
}
|
|
80
|
+
export function holdsNothing(comp, v, want, node, where) {
|
|
81
|
+
const has = absence(v.ty);
|
|
82
|
+
if (has === null)
|
|
83
|
+
return;
|
|
84
|
+
const takes = absence(want);
|
|
85
|
+
if (takes === has || takes === "either")
|
|
86
|
+
return;
|
|
87
|
+
const is = has === "either" ? "may be `null` or `undefined`" : has === "null" ? "may be `null`" : "may be `undefined`";
|
|
88
|
+
const fix = takes === null ? "narrow it with `v-if` first" : takes === "null" ? "write `?? null` after it" : "write `?? undefined` after it";
|
|
89
|
+
fail(comp, "FV0606", `a value that ${is} where ${where} takes ${describeTy(want)}, which Vue would hand it as it is: ${fix}`, node);
|
|
90
|
+
}
|
|
73
91
|
export function coerce(comp, v, want, node) {
|
|
74
92
|
if (sameTy(v.ty, want))
|
|
75
93
|
return v.code;
|
|
76
|
-
|
|
94
|
+
holdsNothing(comp, v, want, node, "the function");
|
|
95
|
+
if (want.k === "opt" && nothing(v.ty))
|
|
77
96
|
return "None";
|
|
97
|
+
if (want.k === "opt" && v.ty.k === "opt" && sameTy(v.ty.of, want.of))
|
|
98
|
+
return v.code;
|
|
78
99
|
if (want.k === "opt" && sameTy(v.ty, want.of))
|
|
79
100
|
return `Some(${bare(v.code)})`;
|
|
80
101
|
if (want.k === "float" && v.ty.k === "int")
|
|
81
102
|
return asF64(v);
|
|
82
103
|
if (want.k === "opt" && want.of.k === "float" && v.ty.k === "int")
|
|
83
104
|
return `Some(${asF64(v)})`;
|
|
84
|
-
return fail(comp, `a ${JSON.stringify(v.ty)} where ${JSON.stringify(want)} is expected`, node);
|
|
105
|
+
return fail(comp, "FV0607", `a ${JSON.stringify(v.ty)} where ${JSON.stringify(want)} is expected`, node);
|
|
85
106
|
}
|
|
86
107
|
export function childOf(name) {
|
|
87
108
|
const c = ctx.components.get(name);
|
|
88
109
|
if (!c)
|
|
89
|
-
throw new GenError(`\`Props\` is imported from ${name}.vue, which is not among the components compiled`);
|
|
110
|
+
throw new GenError("FV0512", `\`Props\` is imported from ${name}.vue, which is not among the components compiled`);
|
|
90
111
|
return c;
|
|
91
112
|
}
|
|
113
|
+
function slotsObject(s, n) {
|
|
114
|
+
if (n.type === "Identifier")
|
|
115
|
+
return n.name === "$slots" || (s.slotsBindings.has(n.name) && !s.locals.has(n.name)) ? n.name : null;
|
|
116
|
+
if (n.type !== "MemberExpression" || n.computed || n.object.type !== "Identifier")
|
|
117
|
+
return null;
|
|
118
|
+
const [owner, name] = [n.object.name, n.property.name];
|
|
119
|
+
if (owner === "_ctx" && name === "$slots")
|
|
120
|
+
return "$slots";
|
|
121
|
+
return (owner === "$setup" || owner === "_ctx") && s.slotsBindings.has(name) ? name : null;
|
|
122
|
+
}
|
|
92
123
|
export function expr(s, n) {
|
|
93
124
|
const comp = s.comp;
|
|
94
125
|
const path = pathOf(n);
|
|
@@ -97,6 +128,9 @@ export function expr(s, n) {
|
|
|
97
128
|
if (narrowed)
|
|
98
129
|
return narrowed;
|
|
99
130
|
}
|
|
131
|
+
const named = constOf(s, n);
|
|
132
|
+
if (named)
|
|
133
|
+
return constVal(named.c, { comp, node: n, what: named.what });
|
|
100
134
|
if (n.type === "MemberExpression" && !n.computed && n.property.type === "Identifier" && n.property.name === "length") {
|
|
101
135
|
if (isObjectCall(n.object, "entries") && n.object.arguments.length === 1) {
|
|
102
136
|
const r = expr(s, n.object.arguments[0]);
|
|
@@ -129,7 +163,7 @@ export function expr(s, n) {
|
|
|
129
163
|
}
|
|
130
164
|
if (isNumber(a.ty) && isNumber(b.ty))
|
|
131
165
|
return arithmetic(a, "+", b, a.ty.k === "int" && b.ty.k === "int");
|
|
132
|
-
return fail(comp, "`+` joins a string to a string or a number, or adds two numbers", n);
|
|
166
|
+
return fail(comp, "FV0608", "`+` joins a string to a string or a number, or adds two numbers", n);
|
|
133
167
|
}
|
|
134
168
|
if (n.type === "BinaryExpression" && ["-", "*", "%", "/", "<", ">", "<=", ">="].includes(n.operator)) {
|
|
135
169
|
const a = expr(s, n.left);
|
|
@@ -145,7 +179,7 @@ export function expr(s, n) {
|
|
|
145
179
|
? "narrow an optional one with `v-if` first"
|
|
146
180
|
: `the other is ${describeTy(isNumber(a.ty) ? b.ty : a.ty)}`;
|
|
147
181
|
const between = comparison ? "two numbers, or two strings," : "two numbers";
|
|
148
|
-
return fail(comp, `\`${n.operator}\` is supported between ${between} that are present: ${why}`, n);
|
|
182
|
+
return fail(comp, "FV0609", `\`${n.operator}\` is supported between ${between} that are present: ${why}`, n);
|
|
149
183
|
}
|
|
150
184
|
if (["<", ">", "<=", ">="].includes(n.operator))
|
|
151
185
|
return compare(a, n.operator, b);
|
|
@@ -168,24 +202,26 @@ export function expr(s, n) {
|
|
|
168
202
|
return { code: "[]", ty: { k: "list", of: UNDEF } };
|
|
169
203
|
const values = n.elements.map((el) => {
|
|
170
204
|
if (!el || el.type === "SpreadElement")
|
|
171
|
-
fail(comp, "an array literal holds plain values", n);
|
|
205
|
+
fail(comp, "FV0610", "an array literal holds plain values", n);
|
|
172
206
|
return expr(s, el);
|
|
173
207
|
});
|
|
174
208
|
const of = values[0].ty;
|
|
175
209
|
if (!(of.k === "str" || of.k === "int" || of.k === "float" || of.k === "bool") || values.some((v) => !sameTy(v.ty, of))) {
|
|
176
|
-
fail(comp, "an array literal holds strings, numbers or booleans, all of one type", n);
|
|
210
|
+
fail(comp, "FV0611", "an array literal holds strings, numbers or booleans, all of one type", n);
|
|
177
211
|
}
|
|
178
212
|
return { code: `[${values.map((v) => bare(v.code)).join(", ")}]`, ty: { k: "list", of }, ...loneOf(...values) };
|
|
179
213
|
}
|
|
180
214
|
case "OptionalMemberExpression": {
|
|
181
215
|
if (n.computed)
|
|
182
|
-
return fail(comp, "computed member access", n);
|
|
216
|
+
return fail(comp, "FV0612", "computed member access", n);
|
|
183
217
|
const base = expr(s, n.object);
|
|
218
|
+
if (nothing(base.ty))
|
|
219
|
+
return { code: "None", ty: UNDEF };
|
|
184
220
|
if (base.ty.k !== "opt")
|
|
185
221
|
return fieldVal(comp, base.code, base.ty, n.property.name, n);
|
|
186
222
|
const f = fieldVal(comp, "v", base.ty.of, n.property.name, n);
|
|
187
223
|
if (f.ty.k === "opt")
|
|
188
|
-
return { code: `${atom(base.code)}.and_then(|v| ${f.code})`, ty: f.ty };
|
|
224
|
+
return { code: `${atom(base.code)}.and_then(|v| ${f.code})`, ty: opt(f.ty) };
|
|
189
225
|
return { code: `${atom(base.code)}.map(|v| ${f.code})`, ty: opt(f.ty) };
|
|
190
226
|
}
|
|
191
227
|
case "StringLiteral":
|
|
@@ -195,7 +231,7 @@ export function expr(s, n) {
|
|
|
195
231
|
case "BooleanLiteral":
|
|
196
232
|
return { code: String(n.value), ty: BOOL, konst: n.value };
|
|
197
233
|
case "NullLiteral":
|
|
198
|
-
return
|
|
234
|
+
return { code: "None", ty: NULL };
|
|
199
235
|
case "Identifier": {
|
|
200
236
|
if (n.name === "undefined")
|
|
201
237
|
return { code: "None", ty: UNDEF };
|
|
@@ -203,11 +239,11 @@ export function expr(s, n) {
|
|
|
203
239
|
if (local)
|
|
204
240
|
return local;
|
|
205
241
|
if (s.clientOnly.has(n.name)) {
|
|
206
|
-
return fail(comp, `\`${n.name}\` is set up in a way the server cannot evaluate: ${s.clientOnly.get(n.name)}`, n);
|
|
242
|
+
return fail(comp, "FV0613", `\`${n.name}\` is set up in a way the server cannot evaluate: ${s.clientOnly.get(n.name)}`, n);
|
|
207
243
|
}
|
|
208
244
|
if (n.name === s.propsIdent)
|
|
209
245
|
return { code: "props", ty: { k: "struct", name: "Props" } };
|
|
210
|
-
return fail(comp, `\`${n.name}\` is not available when rendering on the server`, n);
|
|
246
|
+
return fail(comp, "FV0601", `\`${n.name}\` is not available when rendering on the server`, n);
|
|
211
247
|
}
|
|
212
248
|
case "MemberExpression": {
|
|
213
249
|
if (!n.computed && n.property.name === "value" && n.object.type === "Identifier" && s.refs.has(n.object.name) && !s.locals.has(n.object.name)) {
|
|
@@ -223,15 +259,13 @@ export function expr(s, n) {
|
|
|
223
259
|
if (own)
|
|
224
260
|
return own;
|
|
225
261
|
}
|
|
226
|
-
return fail(comp, "computed member access", n);
|
|
262
|
+
return fail(comp, "FV0612", "computed member access", n);
|
|
227
263
|
}
|
|
228
264
|
const prop = n.property.name;
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
n.object.object.name === "_ctx" && n.object.property.name === "$slots");
|
|
232
|
-
if (slotsObject) {
|
|
265
|
+
const slots = slotsObject(s, n.object);
|
|
266
|
+
if (slots !== null) {
|
|
233
267
|
if (!comp.slotNames.includes(prop))
|
|
234
|
-
fail(comp, `\`$slots.${prop}\` names a slot this template does not render`, n);
|
|
268
|
+
fail(comp, "FV0907", `\`${slots}.${prop}\` names a slot this template does not render`, n);
|
|
235
269
|
return { code: `fv_slots.${snake(prop)}.is_some()`, ty: BOOL };
|
|
236
270
|
}
|
|
237
271
|
if (n.object.type === "Identifier") {
|
|
@@ -244,18 +278,20 @@ export function expr(s, n) {
|
|
|
244
278
|
if (global)
|
|
245
279
|
return global;
|
|
246
280
|
if (n.object.name === "_ctx" && prop === "$attrs") {
|
|
247
|
-
return fail(comp, "`$attrs` is bound whole, with `v-bind=\"$attrs\"`; a value read from it has no type: declare it as a prop", n);
|
|
281
|
+
return fail(comp, "FV0415", "`$attrs` is bound whole, with `v-bind=\"$attrs\"`; a value read from it has no type: declare it as a prop", n);
|
|
248
282
|
}
|
|
249
283
|
const v = s.setup.get(prop);
|
|
250
284
|
if (v)
|
|
251
285
|
return v;
|
|
286
|
+
if (prop === s.propsIdent)
|
|
287
|
+
return { code: "props", ty: { k: "struct", name: "Props" } };
|
|
252
288
|
if (s.clientOnly.has(prop)) {
|
|
253
|
-
return fail(comp, `\`${prop}\` is set up in a way the server cannot evaluate: ${s.clientOnly.get(prop)}`, n);
|
|
289
|
+
return fail(comp, "FV0613", `\`${prop}\` is set up in a way the server cannot evaluate: ${s.clientOnly.get(prop)}`, n);
|
|
254
290
|
}
|
|
255
291
|
if (n.object.name === "_ctx" && comp.props.fields.some((f) => f.js === prop)) {
|
|
256
292
|
return fieldVal(comp, "props", { k: "struct", name: "Props" }, prop, n);
|
|
257
293
|
}
|
|
258
|
-
return fail(comp, `\`${prop}\` is not available when rendering on the server`, n);
|
|
294
|
+
return fail(comp, "FV0601", `\`${prop}\` is not available when rendering on the server`, n);
|
|
259
295
|
}
|
|
260
296
|
}
|
|
261
297
|
}
|
|
@@ -272,7 +308,7 @@ export function expr(s, n) {
|
|
|
272
308
|
const code = narrowing(s, p, (inner) => (right = expr(inner, n.right)).code, () => String(n.operator === "||"));
|
|
273
309
|
if (right?.ty.k === "bool")
|
|
274
310
|
return { code, ty: BOOL };
|
|
275
|
-
return fail(comp, `\`${n.operator}\` is supported between booleans only`, n);
|
|
311
|
+
return fail(comp, "FV0614", `\`${n.operator}\` is supported between booleans only`, n);
|
|
276
312
|
}
|
|
277
313
|
}
|
|
278
314
|
const a = expr(s, n.left);
|
|
@@ -281,12 +317,14 @@ export function expr(s, n) {
|
|
|
281
317
|
const own = claim((p) => p.values?.nullish?.(s, a, b, n));
|
|
282
318
|
if (own)
|
|
283
319
|
return own;
|
|
320
|
+
if (nothing(a.ty))
|
|
321
|
+
return b;
|
|
284
322
|
if (a.ty.k !== "opt")
|
|
285
323
|
return a;
|
|
286
|
-
if (b.ty
|
|
287
|
-
return a;
|
|
324
|
+
if (nothing(b.ty))
|
|
325
|
+
return { ...a, ty: withAbsence(a.ty.of, absence(b.ty)) };
|
|
288
326
|
if (b.iter !== undefined)
|
|
289
|
-
fail(comp, "`??` falling back to a computed list", n);
|
|
327
|
+
fail(comp, "FV0615", "`??` falling back to a computed list", n);
|
|
290
328
|
if (a.held !== undefined && b.ty.k === "str")
|
|
291
329
|
return { code: `&*${atom(a.held)}.unwrap_or(${asCow(b)})`, ty: STR, ...loneOf(a, b) };
|
|
292
330
|
if (a.ty.of.k === "str" && b.ty.k === "str" && isTemporary(b)) {
|
|
@@ -294,13 +332,13 @@ export function expr(s, n) {
|
|
|
294
332
|
}
|
|
295
333
|
if (sameTy(a.ty.of, b.ty))
|
|
296
334
|
return { code: `${atom(a.code)}.unwrap_or(${bare(b.code)})`, ty: b.ty, ...loneOf(a, b) };
|
|
297
|
-
if (sameTy(a.ty, b.ty))
|
|
298
|
-
return { code: `${atom(a.code)}.or(${bare(b.code)})`, ty:
|
|
335
|
+
if (b.ty.k === "opt" && sameTy(a.ty.of, b.ty.of))
|
|
336
|
+
return { code: `${atom(a.code)}.or(${bare(b.code)})`, ty: b.ty, ...loneOf(a, b) };
|
|
299
337
|
if (a.ty.of.k === "float" && b.ty.k === "int")
|
|
300
338
|
return { code: `${atom(a.code)}.unwrap_or(${bare(asF64(b))})`, ty: FLOAT };
|
|
301
339
|
if (a.ty.of.k === "int" && b.ty.k === "float")
|
|
302
340
|
return { code: `${atom(a.code)}.map(|v| v as f64).unwrap_or(${bare(b.code)})`, ty: FLOAT };
|
|
303
|
-
return fail(comp, "`??` between different types", n);
|
|
341
|
+
return fail(comp, "FV0616", "`??` between different types", n);
|
|
304
342
|
}
|
|
305
343
|
if (n.operator === "||") {
|
|
306
344
|
if (a.ty.k === "bool" && b.ty.k === "bool")
|
|
@@ -322,11 +360,11 @@ export function expr(s, n) {
|
|
|
322
360
|
const otherwise = a.ty.k === "str" ? strArg(b.code) : bare(b.code);
|
|
323
361
|
return { code: `{ let a = ${bare(a.code)}; if ${truthy({ code: "a", ty: a.ty })} { a } else { ${otherwise} } }`, ty: a.ty, ...loneOf(a, b) };
|
|
324
362
|
}
|
|
325
|
-
return fail(comp, "`||` between these types", n);
|
|
363
|
+
return fail(comp, "FV0617", "`||` between these types", n);
|
|
326
364
|
}
|
|
327
365
|
if (n.operator === "&&" && a.ty.k === "bool" && b.ty.k === "bool")
|
|
328
366
|
return boolOf(logical(a.code, "&&", b.code));
|
|
329
|
-
return fail(comp, `\`${n.operator}\` is supported between booleans only`, n);
|
|
367
|
+
return fail(comp, "FV0614", `\`${n.operator}\` is supported between booleans only`, n);
|
|
330
368
|
}
|
|
331
369
|
case "UnaryExpression":
|
|
332
370
|
if (n.operator === "!") {
|
|
@@ -341,13 +379,32 @@ export function expr(s, n) {
|
|
|
341
379
|
if (a.ty.k === "float")
|
|
342
380
|
return { code: negate(a.code), ty: FLOAT };
|
|
343
381
|
}
|
|
344
|
-
return fail(comp, `unary \`${n.operator}\``, n);
|
|
382
|
+
return fail(comp, "FV0618", `unary \`${n.operator}\``, n);
|
|
345
383
|
case "BinaryExpression": {
|
|
346
|
-
|
|
347
|
-
|
|
384
|
+
const test = nullTest(n);
|
|
385
|
+
if (n.operator !== "===" && n.operator !== "!==" && !test)
|
|
386
|
+
fail(comp, "FV0619", `\`${n.operator}\`: \`==\` and \`!=\` compare with \`null\` or \`undefined\` only; use \`===\``, n);
|
|
348
387
|
const whole = claim((p) => p.equality?.(s, n));
|
|
349
388
|
if (whole)
|
|
350
389
|
return whole;
|
|
390
|
+
if (test) {
|
|
391
|
+
const v = expr(s, test.target);
|
|
392
|
+
const none = absence(v.ty);
|
|
393
|
+
if (v.ty.k === "opt") {
|
|
394
|
+
checkNullTest(s, test, v);
|
|
395
|
+
const is = `${atom(v.code)}.is_none()`;
|
|
396
|
+
return boolOf(test.is ? is : not(is));
|
|
397
|
+
}
|
|
398
|
+
if (none !== null) {
|
|
399
|
+
const konst = (!test.strict || none === test.literal) === test.is;
|
|
400
|
+
return { code: String(konst), ty: BOOL, konst };
|
|
401
|
+
}
|
|
402
|
+
if (!test.strict && claim((p) => p.values?.describe?.(v.ty)) === undefined) {
|
|
403
|
+
return { code: String(!test.is), ty: BOOL, konst: !test.is };
|
|
404
|
+
}
|
|
405
|
+
if (!test.strict)
|
|
406
|
+
fail(comp, "FV0620", `\`${n.operator}\` of ${describeTy(v.ty)}: compare with \`${test.is ? "===" : "!=="} undefined\``, n);
|
|
407
|
+
}
|
|
351
408
|
const a = expr(s, n.left);
|
|
352
409
|
const b = expr(s, n.right);
|
|
353
410
|
let eq;
|
|
@@ -358,11 +415,16 @@ export function expr(s, n) {
|
|
|
358
415
|
const own = claim((p) => p.values?.equals?.(a, b));
|
|
359
416
|
if (own !== undefined)
|
|
360
417
|
eq = own;
|
|
361
|
-
else if (b.ty.k === "
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
eq = `${atom(
|
|
365
|
-
|
|
418
|
+
else if ((nothing(b.ty) && a.ty.k === "opt") || (nothing(a.ty) && b.ty.k === "opt")) {
|
|
419
|
+
const [v, literal] = a.ty.k === "opt" ? [a, b] : [b, a];
|
|
420
|
+
checkNullTest(s, { target: n, literal: literal.ty.k === "null" ? "null" : "undefined", strict: true, is: n.operator === "===" }, v);
|
|
421
|
+
eq = `${atom(v.code)}.is_none()`;
|
|
422
|
+
}
|
|
423
|
+
else if (nothing(a.ty) || nothing(b.ty)) {
|
|
424
|
+
const other = nothing(a.ty) ? b.ty : a.ty;
|
|
425
|
+
const opaque = nothing(other) ? undefined : claim((p) => p.values?.describe?.(other));
|
|
426
|
+
if (opaque !== undefined)
|
|
427
|
+
fail(comp, "FV0621", `\`${n.operator}\` between ${opaque} and ${describeTy(nothing(a.ty) ? a.ty : b.ty)}`, n);
|
|
366
428
|
const same = a.ty.k === b.ty.k;
|
|
367
429
|
const konst = n.operator === "===" ? same : !same;
|
|
368
430
|
return { code: String(konst), ty: BOOL, konst };
|
|
@@ -383,7 +445,7 @@ export function expr(s, n) {
|
|
|
383
445
|
else if (b.ty.k === "opt" && sameTy(b.ty.of, a.ty) && scalar(a.ty))
|
|
384
446
|
eq = binary(`Some(${bare(a.code)})`, "==", b.code);
|
|
385
447
|
else
|
|
386
|
-
return fail(comp, "`===` between these types", n);
|
|
448
|
+
return fail(comp, "FV0622", "`===` between these types", n);
|
|
387
449
|
return boolOf(n.operator === "===" ? eq : not(eq));
|
|
388
450
|
}
|
|
389
451
|
case "ConditionalExpression": {
|
|
@@ -406,6 +468,8 @@ export function expr(s, n) {
|
|
|
406
468
|
return p.option;
|
|
407
469
|
if (present === name)
|
|
408
470
|
return `${atom(p.option)}.unwrap_or(${absent})`;
|
|
471
|
+
if (absent === "None" && present.startsWith("Some(") && enclosed(present.slice(4)))
|
|
472
|
+
return `${atom(p.option)}.map(|${name}| ${bare(present.slice(5, -1))})`;
|
|
409
473
|
if (occurrences(present, name))
|
|
410
474
|
return `if ${p.pattern(name)} { ${present} } else { ${absent} }`;
|
|
411
475
|
return choice(p.present, present, absent);
|
|
@@ -418,15 +482,18 @@ export function expr(s, n) {
|
|
|
418
482
|
const both = `(${choose(collected(a), collected(b))})`;
|
|
419
483
|
return { code: both, ty: a.ty, iter: `${both}.into_iter()`, ...lone };
|
|
420
484
|
}
|
|
421
|
-
const optStr = (v) => v.ty
|
|
485
|
+
const optStr = (v) => nothing(v.ty) || (v.ty.k === "opt" && v.ty.of.k === "str") || v.ty.k === "str";
|
|
422
486
|
if (sameTy(a.ty, b.ty) && a.ty.k !== "opt")
|
|
423
487
|
return { code: choose(a.code, b.code), ty: a.ty, ...lone };
|
|
424
488
|
if (isNumber(a.ty) && isNumber(b.ty))
|
|
425
489
|
return { code: choose(asF64(a), asF64(b)), ty: FLOAT };
|
|
426
|
-
|
|
427
|
-
|
|
490
|
+
const base = (ty) => (ty.k === "opt" ? ty.of : nothing(ty) ? null : ty);
|
|
491
|
+
const [baseA, baseB] = [base(a.ty), base(b.ty)];
|
|
492
|
+
const of = baseA ?? baseB;
|
|
493
|
+
if (of !== null && (baseA === null || baseB === null || sameTy(baseA, baseB))) {
|
|
494
|
+
const ty = withAbsence(of, joinAbsence(absence(a.ty), absence(b.ty)));
|
|
428
495
|
if (optStr(a) && optStr(b) && (isTemporary(a) || isTemporary(b) || a.held !== undefined || b.held !== undefined)) {
|
|
429
|
-
const held = (v) => v.held ?? (v.ty
|
|
496
|
+
const held = (v) => v.held ?? (nothing(v.ty) ? "None" : v.ty.k === "opt" ? `${atom(v.code)}.map(std::borrow::Cow::<str>::Borrowed)` : `Some(${asCow(v)})`);
|
|
430
497
|
const both = atom(choose(held(a), held(b)));
|
|
431
498
|
return { code: `${both}.as_deref()`, ty, held: both, ...lone };
|
|
432
499
|
}
|
|
@@ -434,9 +501,13 @@ export function expr(s, n) {
|
|
|
434
501
|
return { code: choose(a.code, b.code), ty: a.ty, ...lone };
|
|
435
502
|
return { code: choose(coerce(comp, a, ty, n), coerce(comp, b, ty, n)), ty, ...lone };
|
|
436
503
|
}
|
|
437
|
-
return fail(comp, "the two branches of `?:` differ in type", n);
|
|
504
|
+
return fail(comp, "FV0623", "the two branches of `?:` differ in type", n);
|
|
505
|
+
}
|
|
506
|
+
case "NewExpression": {
|
|
507
|
+
const made = n.callee.type === "Identifier" ? n.callee.name : n.callee.type === "MemberExpression" && !n.callee.computed ? `${n.callee.object.name ?? "…"}.${n.callee.property.name}` : "…";
|
|
508
|
+
return fail(comp, "FV0624", `\`new ${made}(…)\` builds an object the server has no twin for: compute the value in Rust and pass it as a prop`, n);
|
|
438
509
|
}
|
|
439
510
|
default:
|
|
440
|
-
return fail(comp, `\`${n.type}\` is not supported in
|
|
511
|
+
return fail(comp, "FV0625", `\`${n.type}\` is not supported in a template`, n);
|
|
441
512
|
}
|
|
442
513
|
}
|
package/dist/fixture.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type App, type Component } from "vue";
|
|
2
|
-
import type
|
|
2
|
+
import { type RouteEntry } from "./routes.ts";
|
|
3
|
+
export { routeRecords, type RouteEntry } from "./routes.ts";
|
|
3
4
|
/** A conformance fixture: the props, and `$slots`, `$route`, `$stores` and `$locale`. */
|
|
4
5
|
export interface Fixture {
|
|
5
6
|
props: Record<string, unknown>;
|
|
@@ -11,12 +12,6 @@ export interface Fixture {
|
|
|
11
12
|
}
|
|
12
13
|
/** A fixture's JSON, split into the props and the rest. */
|
|
13
14
|
export declare function readFixture(json: Record<string, unknown>): Fixture;
|
|
14
|
-
/** A route as a routes file lists it: a path, or a path and a name. */
|
|
15
|
-
export type RouteEntry = string | {
|
|
16
|
-
path: string;
|
|
17
|
-
name?: string;
|
|
18
|
-
children?: RouteEntry[];
|
|
19
|
-
};
|
|
20
15
|
/** The router options ferrovue reproduces, as the project's configuration gives them. */
|
|
21
16
|
export interface RouterOptions {
|
|
22
17
|
base?: string;
|
|
@@ -29,11 +24,9 @@ export interface RouterOptions {
|
|
|
29
24
|
locale: string;
|
|
30
25
|
fallbackLocale?: string | string[];
|
|
31
26
|
};
|
|
32
|
-
/** Render on the client with `createApp
|
|
27
|
+
/** Render on the client from scratch with `createApp`: the scope
|
|
33
28
|
* ids a fresh client render writes, which the server's must equal for scoped styles to apply. */
|
|
34
29
|
client?: boolean;
|
|
35
30
|
}
|
|
36
|
-
/** Route records for vue-router, every route — nested ones too — given the fixture's view. */
|
|
37
|
-
export declare function routeRecords(routes: RouteEntry[], View: Component): RouteRecordRaw[];
|
|
38
31
|
/** An app rendering one fixture of `component`: with a router over `routes` when there are any. */
|
|
39
32
|
export declare function fixtureApp(component: Component, fixture: Fixture, routes: RouteEntry[] | null, options?: RouterOptions): Promise<App>;
|
package/dist/fixture.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createApp, createSSRApp, createStaticVNode, defineComponent, h } from "vue";
|
|
2
|
+
import { routeRecords } from "./routes.js";
|
|
3
|
+
export { routeRecords } from "./routes.js";
|
|
2
4
|
/** A fixture's JSON, split into the props and the rest. */
|
|
3
5
|
export function readFixture(json) {
|
|
4
6
|
const { $slots, $route, $stores, $locale, ...props } = json;
|
|
@@ -45,12 +47,6 @@ async function optionalPinia(stores) {
|
|
|
45
47
|
return null;
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
|
-
/** Route records for vue-router, every route — nested ones too — given the fixture's view. */
|
|
49
|
-
export function routeRecords(routes, View) {
|
|
50
|
-
return routes.map((r) => typeof r === "string"
|
|
51
|
-
? { path: r, component: View }
|
|
52
|
-
: { path: r.path, component: View, ...(r.name ? { name: r.name } : {}), ...(r.children ? { children: routeRecords(r.children, View) } : {}) });
|
|
53
|
-
}
|
|
54
50
|
/** An app rendering one fixture of `component`: with a router over `routes` when there are any. */
|
|
55
51
|
export async function fixtureApp(component, fixture, routes, options = {}) {
|
|
56
52
|
const slots = Object.fromEntries(Object.entries(fixture.slots)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { App, Component } from "vue";
|
|
2
|
+
import { type IslandComponent, type PageOptions, type PageRecord } from "./client.ts";
|
|
3
|
+
/** A page as the server wrote it: `html` holds the container and, unless `record` is given, the
|
|
4
|
+
* record's script. */
|
|
5
|
+
export interface RecordedPage {
|
|
6
|
+
html: string;
|
|
7
|
+
record?: PageRecord;
|
|
8
|
+
}
|
|
9
|
+
/** Put a recorded page into the document's body and hydrate it with `mountPage`, throwing on a
|
|
10
|
+
* node Vue replaced, on any change hydrating made to the markup (a `style` attribute is compared by
|
|
11
|
+
* the declarations it holds), and on anything Vue warns or logs as an error until the page has
|
|
12
|
+
* settled. The markup is compared as hydrating leaves it; what changes once the page is mounted,
|
|
13
|
+
* such as a `<ClientOnly>` showing its content or an async component loading, is the app's to
|
|
14
|
+
* change. Resolves to the app, mounted and settled. */
|
|
15
|
+
export declare function hydrateRecordedPage(page: RecordedPage, layout: Component, components: Record<string, IslandComponent>, options?: PageOptions): Promise<App>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { mountPage } from "./client.js";
|
|
2
|
+
import { settled } from "./settle.js";
|
|
3
|
+
function recordScript(id, record) {
|
|
4
|
+
const json = JSON.stringify(record).replace(/[<>&\u2028\u2029]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
5
|
+
return `<script type="application/json" id="${id}">${json}</script>`;
|
|
6
|
+
}
|
|
7
|
+
function shape(node, scratch) {
|
|
8
|
+
if (node.nodeType === 3)
|
|
9
|
+
return node.textContent ?? "";
|
|
10
|
+
if (node.nodeType === 8)
|
|
11
|
+
return `<!--${node.textContent ?? ""}-->`;
|
|
12
|
+
if (node.nodeType !== 1)
|
|
13
|
+
return "";
|
|
14
|
+
const el = node;
|
|
15
|
+
const attrs = el
|
|
16
|
+
.getAttributeNames()
|
|
17
|
+
.filter((name) => name !== "style")
|
|
18
|
+
.toSorted()
|
|
19
|
+
.map((name) => ` ${name}=${JSON.stringify(el.getAttribute(name))}`);
|
|
20
|
+
scratch.setAttribute("style", el.getAttribute("style") ?? "");
|
|
21
|
+
if (scratch.style.cssText)
|
|
22
|
+
attrs.push(` style=${JSON.stringify(scratch.style.cssText)}`);
|
|
23
|
+
return `<${el.tagName}${attrs.join("")}>${[...el.childNodes].map((child) => shape(child, scratch)).join("")}</${el.tagName}>`;
|
|
24
|
+
}
|
|
25
|
+
/** Put a recorded page into the document's body and hydrate it with `mountPage`, throwing on a
|
|
26
|
+
* node Vue replaced, on any change hydrating made to the markup (a `style` attribute is compared by
|
|
27
|
+
* the declarations it holds), and on anything Vue warns or logs as an error until the page has
|
|
28
|
+
* settled. The markup is compared as hydrating leaves it; what changes once the page is mounted,
|
|
29
|
+
* such as a `<ClientOnly>` showing its content or an async component loading, is the app's to
|
|
30
|
+
* change. Resolves to the app, mounted and settled. */
|
|
31
|
+
export async function hydrateRecordedPage(page, layout, components, options = {}) {
|
|
32
|
+
const doc = options.doc ?? document;
|
|
33
|
+
const id = options.record ?? "__fv_page";
|
|
34
|
+
doc.body.innerHTML = page.html + (page.record ? recordScript(id, page.record) : "");
|
|
35
|
+
const selector = options.container ?? "#app";
|
|
36
|
+
const container = typeof selector === "string" ? doc.querySelector(selector) : selector;
|
|
37
|
+
if (!container)
|
|
38
|
+
throw new Error(`the recorded page has no ${selector}`);
|
|
39
|
+
const scratch = doc.createElement("div");
|
|
40
|
+
const before = container.innerHTML;
|
|
41
|
+
const shapeBefore = [...container.childNodes].map((child) => shape(child, scratch)).join("");
|
|
42
|
+
const first = container.firstChild;
|
|
43
|
+
const problems = [];
|
|
44
|
+
const shapeOf = () => [...container.childNodes].map((child) => shape(child, scratch)).join("");
|
|
45
|
+
let hydrated = "";
|
|
46
|
+
let shapeHydrated = "";
|
|
47
|
+
const watch = {
|
|
48
|
+
install(app) {
|
|
49
|
+
app.config.warnHandler = (message) => void problems.push(`warning: ${message}`);
|
|
50
|
+
const mount = app.mount.bind(app);
|
|
51
|
+
app.mount = (...args) => {
|
|
52
|
+
const root = mount(...args);
|
|
53
|
+
hydrated = container.innerHTML;
|
|
54
|
+
shapeHydrated = shapeOf();
|
|
55
|
+
return root;
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
const { warn, error } = console;
|
|
60
|
+
console.warn = (...args) => void problems.push(`console.warn: ${args.map(String).join(" ")}`);
|
|
61
|
+
console.error = (...args) => void problems.push(`console.error: ${args.map(String).join(" ")}`);
|
|
62
|
+
let app;
|
|
63
|
+
try {
|
|
64
|
+
app = await mountPage(layout, components, { ...options, doc, container, plugins: [...(options.plugins ?? []), watch] });
|
|
65
|
+
await settled(app);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
console.warn = warn;
|
|
69
|
+
console.error = error;
|
|
70
|
+
}
|
|
71
|
+
if (container.firstChild !== first)
|
|
72
|
+
problems.push("Vue replaced the server's first node");
|
|
73
|
+
if (shapeHydrated !== shapeBefore)
|
|
74
|
+
problems.push(`hydrating changed the markup:\n server: ${before}\n hydrated: ${hydrated}`);
|
|
75
|
+
if (problems.length) {
|
|
76
|
+
app.unmount();
|
|
77
|
+
throw new Error(`the page did not hydrate exactly:\n${problems.join("\n")}`);
|
|
78
|
+
}
|
|
79
|
+
return app;
|
|
80
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export { hydrateState, mountIslands, type IslandComponent, type Islands, type MountOptions } from "./client.ts";
|
|
1
|
+
export { ClientOnly, hydrateState, mountIslands, type IslandComponent, type Islands, type MountOptions } from "./client.ts";
|
|
2
|
+
export { createPageApp, mountPage, type PageOptions, type PagePart, type PageRecord } from "./client.ts";
|
|
2
3
|
export type { Float, TrustedHtml } from "./types.ts";
|
package/dist/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { hydrateState, mountIslands } from "./client.js";
|
|
1
|
+
export { ClientOnly, hydrateState, mountIslands } from "./client.js";
|
|
2
|
+
export { createPageApp, mountPage } from "./client.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type RouteLocationNormalized, type Router, type RouterHistory } from "vue-router";
|
|
2
|
+
import { type RouteEntry } from "./routes.ts";
|
|
3
|
+
export type { RouteEntry } from "./routes.ts";
|
|
4
|
+
/** What `linkRouter` hands navigations to, and the router options of `ferrovue.config.json`. */
|
|
5
|
+
export interface LinkRouterOptions {
|
|
6
|
+
/** The application's own navigation, called with the `href` of every navigation after the first
|
|
7
|
+
* (the base included, as the link's `href` attribute has it) and the route it resolves to. */
|
|
8
|
+
navigate: (href: string, to: RouteLocationNormalized) => void;
|
|
9
|
+
/** The history's base, as `router.base` in `ferrovue.config.json` gives it. */
|
|
10
|
+
base?: string;
|
|
11
|
+
/** As `router.linkActiveClass` in `ferrovue.config.json`. */
|
|
12
|
+
linkActiveClass?: string;
|
|
13
|
+
/** As `router.linkExactActiveClass` in `ferrovue.config.json`. */
|
|
14
|
+
linkExactActiveClass?: string;
|
|
15
|
+
/** The history the router reads its first location from. By default a memory history at the
|
|
16
|
+
* page's location: the router neither writes to the browser's history nor listens to it. */
|
|
17
|
+
history?: RouterHistory;
|
|
18
|
+
}
|
|
19
|
+
/** A router for `<RouterLink>` and `useRoute()` alone, for an application whose own navigation
|
|
20
|
+
* layer leaves the page: built from the routes file the compiler reads, every route rendering
|
|
21
|
+
* nothing. Its first navigation resolves the page the server rendered; every later one, a link's
|
|
22
|
+
* click included, is handed to `navigate` and aborted, so the router stays on that page. Build one
|
|
23
|
+
* for each page the application shows. */
|
|
24
|
+
export declare function linkRouter(routes: RouteEntry[], options: LinkRouterOptions): Router;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createMemoryHistory, createRouter, isNavigationFailure, NavigationFailureType, START_LOCATION } from "vue-router";
|
|
2
|
+
import { routeRecords } from "./routes.js";
|
|
3
|
+
const Empty = { render: () => null };
|
|
4
|
+
function pageHistory(base) {
|
|
5
|
+
const history = createMemoryHistory(base);
|
|
6
|
+
const { pathname, search, hash } = window.location;
|
|
7
|
+
const prefix = history.base;
|
|
8
|
+
const path = prefix && pathname.toLowerCase().startsWith(prefix.toLowerCase()) ? pathname.slice(prefix.length) || "/" : pathname;
|
|
9
|
+
history.replace(path + search + hash);
|
|
10
|
+
return history;
|
|
11
|
+
}
|
|
12
|
+
/** A router for `<RouterLink>` and `useRoute()` alone, for an application whose own navigation
|
|
13
|
+
* layer leaves the page: built from the routes file the compiler reads, every route rendering
|
|
14
|
+
* nothing. Its first navigation resolves the page the server rendered; every later one, a link's
|
|
15
|
+
* click included, is handed to `navigate` and aborted, so the router stays on that page. Build one
|
|
16
|
+
* for each page the application shows. */
|
|
17
|
+
export function linkRouter(routes, options) {
|
|
18
|
+
const history = options.history ?? pageHistory(options.base);
|
|
19
|
+
const router = createRouter({
|
|
20
|
+
history,
|
|
21
|
+
routes: routeRecords(routes, Empty),
|
|
22
|
+
...(options.linkActiveClass !== undefined ? { linkActiveClass: options.linkActiveClass } : {}),
|
|
23
|
+
...(options.linkExactActiveClass !== undefined ? { linkExactActiveClass: options.linkExactActiveClass } : {}),
|
|
24
|
+
});
|
|
25
|
+
router.beforeEach((to, from) => {
|
|
26
|
+
if (from === START_LOCATION)
|
|
27
|
+
return true;
|
|
28
|
+
options.navigate(history.createHref(to.fullPath), to);
|
|
29
|
+
return false;
|
|
30
|
+
});
|
|
31
|
+
router.afterEach((to, _from, failure) => {
|
|
32
|
+
if (isNavigationFailure(failure, NavigationFailureType.duplicated))
|
|
33
|
+
options.navigate(history.createHref(to.fullPath), to);
|
|
34
|
+
});
|
|
35
|
+
return router;
|
|
36
|
+
}
|