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/calls.js
CHANGED
|
@@ -20,7 +20,7 @@ export function call(s, n) {
|
|
|
20
20
|
meet(comp, a, b, "`v-model`", n, "equal");
|
|
21
21
|
return boolOf(stringsEqual(a, b));
|
|
22
22
|
}
|
|
23
|
-
return fail(comp, "`v-model` comparison between these types", n);
|
|
23
|
+
return fail(comp, "FV0413", "`v-model` comparison between these types", n);
|
|
24
24
|
}
|
|
25
25
|
case "_ssrIncludeBooleanAttr": {
|
|
26
26
|
const a = expr(s, args[0]);
|
|
@@ -33,7 +33,7 @@ export function call(s, n) {
|
|
|
33
33
|
return boolOf(truthy(a));
|
|
34
34
|
}
|
|
35
35
|
case "_ssrLooseContain":
|
|
36
|
-
return fail(comp, "`v-model` over an array", n);
|
|
36
|
+
return fail(comp, "FV0414", "`v-model` over an array", n);
|
|
37
37
|
}
|
|
38
38
|
const own = claim((p) => p.call?.(s, n));
|
|
39
39
|
if (own)
|
|
@@ -58,21 +58,21 @@ export function call(s, n) {
|
|
|
58
58
|
return a;
|
|
59
59
|
if (a.ty.k === "bool")
|
|
60
60
|
return { code: `i64::from(${bare(a.code)})`, ty: INT };
|
|
61
|
-
return fail(comp, "`Number()` takes a string, a number or a boolean that is present", n);
|
|
61
|
+
return fail(comp, "FV0701", "`Number()` takes a string, a number or a boolean that is present", n);
|
|
62
62
|
}
|
|
63
63
|
if ((callee.name === "parseInt" && (args.length === 1 || args.length === 2)) || (callee.name === "parseFloat" && args.length === 1)) {
|
|
64
64
|
const a = expr(s, args[0]);
|
|
65
65
|
if (a.ty.k !== "str")
|
|
66
|
-
fail(comp, `\`${callee.name}()\` takes a string`, args[0]);
|
|
66
|
+
fail(comp, "FV0702", `\`${callee.name}()\` takes a string`, args[0]);
|
|
67
67
|
if (callee.name === "parseFloat")
|
|
68
68
|
return { code: `fv::js_parse_float(${strArg(a.code)})`, ty: FLOAT };
|
|
69
69
|
const radix = args[1];
|
|
70
70
|
if (radix && !(radix.type === "NumericLiteral" && (radix.value === 10 || radix.value === 16))) {
|
|
71
|
-
fail(comp, "`parseInt()` takes a radix of 10 or 16, written as a literal", radix);
|
|
71
|
+
fail(comp, "FV0703", "`parseInt()` takes a radix of 10 or 16, written as a literal", radix);
|
|
72
72
|
}
|
|
73
73
|
return { code: `fv::js_parse_int(${strArg(a.code)}, ${radix ? radix.value : 0})`, ty: FLOAT };
|
|
74
74
|
}
|
|
75
|
-
return fail(comp, `\`${callee.name}()\` is not available when rendering on the server`, n);
|
|
75
|
+
return fail(comp, "FV0601", `\`${callee.name}()\` is not available when rendering on the server`, n);
|
|
76
76
|
}
|
|
77
77
|
if (callee.type === "MemberExpression" && !callee.computed) {
|
|
78
78
|
const method = callee.property.name;
|
|
@@ -119,7 +119,7 @@ export function call(s, n) {
|
|
|
119
119
|
return { code: `${receiver(asF64(x))}.${method}()`, ty: FLOAT };
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
-
return fail(comp, `\`Math.${method}()\` is supported on numbers as \`max\`, \`min\`, \`abs\`, \`round\`, \`floor\`, \`ceil\` and \`trunc\``, n);
|
|
122
|
+
return fail(comp, "FV0704", `\`Math.${method}()\` is supported on numbers as \`max\`, \`min\`, \`abs\`, \`round\`, \`floor\`, \`ceil\` and \`trunc\``, n);
|
|
123
123
|
}
|
|
124
124
|
const target = expr(s, callee.object);
|
|
125
125
|
if (target.ty.k === "str") {
|
|
@@ -145,7 +145,7 @@ export function call(s, n) {
|
|
|
145
145
|
if (isNumber(target.ty) && method === "toFixed" && args.length <= 1) {
|
|
146
146
|
const d = args.length ? args[0] : { type: "NumericLiteral", value: 0 };
|
|
147
147
|
if (d.type !== "NumericLiteral" || !Number.isInteger(d.value) || d.value < 0 || d.value > 100) {
|
|
148
|
-
fail(comp, "`.toFixed()` takes a literal number of digits, from 0 to 100", n);
|
|
148
|
+
fail(comp, "FV0705", "`.toFixed()` takes a literal number of digits, from 0 to 100", n);
|
|
149
149
|
}
|
|
150
150
|
return { code: `&*fv::js_to_fixed(${bare(asF64(target))}, ${d.value})`, ty: STR };
|
|
151
151
|
}
|
|
@@ -162,8 +162,10 @@ export function call(s, n) {
|
|
|
162
162
|
const of = target.ty.of;
|
|
163
163
|
if (method === "includes" && args.length === 1) {
|
|
164
164
|
const v = expr(s, args[0]);
|
|
165
|
+
if (of.k === "int" && v.ty.k === "float")
|
|
166
|
+
return { code: `${atom(target.code)}.iter().any(|v| ${binary("*v as f64", "==", v.code)})`, ty: BOOL };
|
|
165
167
|
if (!sameTy(v.ty, of))
|
|
166
|
-
fail(comp, "`.includes()` looks for a value of the list's own type", args[0]);
|
|
168
|
+
fail(comp, "FV0801", "`.includes()` looks for a value of the list's own type", args[0]);
|
|
167
169
|
if (of.k === "int")
|
|
168
170
|
return { code: `${atom(target.code)}.contains(&${operand(v.code, UNARY)})`, ty: BOOL };
|
|
169
171
|
const test = v.code === '""' ? "v.is_empty()" : binary("&**v", "==", v.code);
|
|
@@ -172,14 +174,14 @@ export function call(s, n) {
|
|
|
172
174
|
if (method === "join" && args.length <= 1) {
|
|
173
175
|
const sep = args.length ? expr(s, args[0]) : { code: '","', ty: STR };
|
|
174
176
|
if (sep.ty.k !== "str")
|
|
175
|
-
fail(comp, "`.join()` takes a string", args[0]);
|
|
177
|
+
fail(comp, "FV0802", "`.join()` takes a string", args[0]);
|
|
176
178
|
const each = of.k === "str" ? `${atom(target.code)}.iter().map(|v| &**v)` : `${atom(target.code)}.iter().map(|v| fv::Js(*v).to_string())`;
|
|
177
179
|
return { code: `&*${each}.collect::<Vec<_>>().join(${strArg(sep.code)})`, ty: STR, ...loneOf(sep) };
|
|
178
180
|
}
|
|
179
181
|
}
|
|
180
|
-
return fail(comp, `\`.${method}()\` is not supported`, n);
|
|
182
|
+
return fail(comp, "FV0602", `\`.${method}()\` is not supported`, n);
|
|
181
183
|
}
|
|
182
|
-
return fail(comp, "this call is not supported", n);
|
|
184
|
+
return fail(comp, "FV0603", "this call is not supported", n);
|
|
183
185
|
}
|
|
184
186
|
export function isObjectCall(n, method) {
|
|
185
187
|
return (n?.type === "CallExpression" && n.callee.type === "MemberExpression" && !n.callee.computed &&
|
|
@@ -187,7 +189,7 @@ export function isObjectCall(n, method) {
|
|
|
187
189
|
}
|
|
188
190
|
function json(s, v, n) {
|
|
189
191
|
if (v.lone)
|
|
190
|
-
fail(s.comp, lonely("`JSON.stringify()`"), n);
|
|
192
|
+
fail(s.comp, "FV0706", lonely("`JSON.stringify()`"), n);
|
|
191
193
|
const one = (code, ty) => {
|
|
192
194
|
switch (ty.k) {
|
|
193
195
|
case "str":
|
|
@@ -199,7 +201,7 @@ function json(s, v, n) {
|
|
|
199
201
|
case "bool":
|
|
200
202
|
return `(if ${condition(code)} { "true" } else { "false" }).to_owned()`;
|
|
201
203
|
default:
|
|
202
|
-
return fail(s.comp, "`JSON.stringify()` of a string, a number, a boolean or a list of those, present", n);
|
|
204
|
+
return fail(s.comp, "FV0707", "`JSON.stringify()` of a string, a number, a boolean or a list of those, present", n);
|
|
203
205
|
}
|
|
204
206
|
};
|
|
205
207
|
if (v.ty.k === "list") {
|
|
@@ -212,7 +214,7 @@ function json(s, v, n) {
|
|
|
212
214
|
export function helperCall(s, name, args, n) {
|
|
213
215
|
const h = ctx.helpers[name];
|
|
214
216
|
if (args.length !== h.params.length)
|
|
215
|
-
fail(s.comp, `\`${name}\` takes ${h.params.length} argument(s)`, n);
|
|
217
|
+
fail(s.comp, "FV1101", `\`${name}\` takes ${h.params.length} argument(s)`, n);
|
|
216
218
|
const code = args.map((a, i) => bare(coerce(s.comp, expr(s, a), h.params[i], a))).join(", ");
|
|
217
219
|
s.helperBytes.n += h.maxLen;
|
|
218
220
|
return { code: `${h.rust}(${code})`, ty: h.ret };
|
package/dist/children.d.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import { type Component, type N, type Scope, type Ty, type Val } from "./model.ts";
|
|
1
|
+
import { type Component, type Field, type N, type Scope, type Ty, type Val } from "./model.ts";
|
|
2
2
|
import { Emitter } from "./emitter.ts";
|
|
3
|
-
|
|
3
|
+
/** A component the project renders with a Rust function of its own, in place of a compiled one. */
|
|
4
|
+
export interface TwinCall {
|
|
5
|
+
comp: Component;
|
|
6
|
+
init(s: Scope, f: Field, node: N | undefined, n: N): string;
|
|
7
|
+
call(s: Scope, e: Emitter, inits: string[], slots: N, attrs: string): void;
|
|
8
|
+
}
|
|
9
|
+
export declare function renderChild(s: Scope, e: Emitter, n: N, twin?: TwinCall): void;
|
|
4
10
|
export declare function array(key: string): boolean;
|
|
5
11
|
export declare function fieldInit(name: string, value: string): string;
|
|
6
12
|
export declare function takesSlots(c: Component): boolean;
|
|
7
13
|
export declare function extraParams(c: Component): string;
|
|
8
14
|
export declare function callChild(s: Scope, e: Emitter, child: Component, propsCode: string, slots: N, attrs: string | null): void;
|
|
15
|
+
export declare function callWith(s: Scope, e: Emitter, child: Component, m: string, head: string, slotsTy: string, trailing: string, slots: N, vnode?: boolean): void;
|
|
9
16
|
export declare function ownInto(comp: Component, v: Val, want: Ty, node: N): string;
|
package/dist/children.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { isSSRSafeAttrName, propsToAttrMap } from "@vue/shared";
|
|
2
|
-
import { camelize, declares, fail,
|
|
2
|
+
import { camelize, declares, fail, nothing, rustStr, sameTy, snake, takesAttrs } from "./model.js";
|
|
3
3
|
import { ctx } from "./context.js";
|
|
4
4
|
import { markHome } from "./typescript.js";
|
|
5
|
-
import { expr, fieldVal } from "./expr.js";
|
|
5
|
+
import { expr, fieldVal, holdsNothing } from "./expr.js";
|
|
6
6
|
import { asF64 } from "./numbers.js";
|
|
7
7
|
import { atom, bare } from "./parens.js";
|
|
8
8
|
import { Emitter } from "./emitter.js";
|
|
@@ -11,7 +11,7 @@ import { passedKey } from "./fallthrough.js";
|
|
|
11
11
|
import { claim, paramsOf, slotFieldsOf } from "./plugin.js";
|
|
12
12
|
import { statements } from "./template.js";
|
|
13
13
|
import { slotContent, slotFieldBorrows, staticallyFilled } from "./slots.js";
|
|
14
|
-
export function renderChild(s, e, n) {
|
|
14
|
+
export function renderChild(s, e, n, twin) {
|
|
15
15
|
const [target, rawProps, slots, , slotScopeId] = n.arguments;
|
|
16
16
|
let local = null;
|
|
17
17
|
if (target.type === "MemberExpression" && target.object.name === "$setup") {
|
|
@@ -19,9 +19,9 @@ export function renderChild(s, e, n) {
|
|
|
19
19
|
}
|
|
20
20
|
const isSelf = target.type === "Identifier" && target.name === s.selfAlias.name;
|
|
21
21
|
const childName = isSelf ? s.comp.name : local ? s.children.get(local) : undefined;
|
|
22
|
-
const child = childName ? s.components.get(childName) : undefined;
|
|
22
|
+
const child = twin?.comp ?? (childName ? s.components.get(childName) : undefined);
|
|
23
23
|
if (!child)
|
|
24
|
-
fail(s.comp, "a child component must be
|
|
24
|
+
fail(s.comp, "FV0501", `a child component must be imported from a \`.vue\` file among the components compiled, in ${ctx.componentsDir.replace(/\/$/, "")}`, n);
|
|
25
25
|
for (const p of ctx.plugins)
|
|
26
26
|
p.child?.(s, child, n);
|
|
27
27
|
const parts = !rawProps || rawProps.type === "NullLiteral" ? [] : mergedParts(rawProps);
|
|
@@ -33,30 +33,32 @@ export function renderChild(s, e, n) {
|
|
|
33
33
|
if (parts.some(passed)) {
|
|
34
34
|
for (const name of s.comp.attrNames) {
|
|
35
35
|
if (declares(child, name)) {
|
|
36
|
-
fail(s.comp, `\`${name}\`, an attribute ${s.comp.name} may be passed, would reach ${child.name} as its prop \`${camelize(name)}\`, which an attribute has no type for`, n);
|
|
36
|
+
fail(s.comp, "FV0502", `\`${name}\`, an attribute ${s.comp.name} may be passed, would reach ${child.name} as its prop \`${camelize(name)}\`, which an attribute has no type for`, n);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
const ids = claim((p) => p.childIds?.(s, child, passesAttrs, !!slotScopeId, n)) ?? null;
|
|
41
41
|
if (objects.length === 1 && objects[0].type !== "ObjectExpression") {
|
|
42
|
+
if (twin)
|
|
43
|
+
fail(s.comp, "FV1501", `the props of ${child.name}, a Rust twin, are attributes or an object literal`, objects[0]);
|
|
42
44
|
const v = expr(s, objects[0]);
|
|
43
45
|
const own = child.name === s.comp.name && v.ty.k === "struct" && v.ty.name === "Props";
|
|
44
46
|
if (own || (v.ty.k === "child" && v.ty.name === child.name)) {
|
|
45
|
-
callChild(s, e, child, v.code, slots, childAttrsArg(s, child, parts, merges, null, ids));
|
|
47
|
+
callChild(s, e, child, v.code, slots, childAttrsArg(s, child, parts, merges, null, ids, n));
|
|
46
48
|
return;
|
|
47
49
|
}
|
|
48
|
-
fail(s.comp, `child props must be an object literal, or \`v-bind\` of ${child.name}'s own \`Props\``, n);
|
|
50
|
+
fail(s.comp, "FV0503", `child props must be an object literal, or \`v-bind\` of ${child.name}'s own \`Props\``, n);
|
|
49
51
|
}
|
|
50
52
|
for (const p of objects) {
|
|
51
53
|
if (p.type !== "ObjectExpression")
|
|
52
|
-
fail(s.comp, `child props must be an object literal, or \`v-bind\` of ${child.name}'s own \`Props\``, p);
|
|
54
|
+
fail(s.comp, "FV0503", `child props must be an object literal, or \`v-bind\` of ${child.name}'s own \`Props\``, p);
|
|
53
55
|
}
|
|
54
56
|
const given = new Map();
|
|
55
57
|
const fallthrough = new Set();
|
|
56
58
|
for (const obj of objects) {
|
|
57
59
|
for (const p of obj.properties) {
|
|
58
60
|
if (p.type !== "ObjectProperty" || p.computed)
|
|
59
|
-
fail(s.comp, "child props hold plain keys", p);
|
|
61
|
+
fail(s.comp, "FV0504", "child props hold plain keys", p);
|
|
60
62
|
const key = p.key.type === "Identifier" ? p.key.name : String(p.key.value);
|
|
61
63
|
if (!passedKey(child, key))
|
|
62
64
|
continue;
|
|
@@ -66,29 +68,36 @@ export function renderChild(s, e, n) {
|
|
|
66
68
|
continue;
|
|
67
69
|
}
|
|
68
70
|
if (array(key))
|
|
69
|
-
fail(s.comp, `an attribute named \`${key}\`, which JavaScript would put before the others`, p);
|
|
71
|
+
fail(s.comp, "FV0505", `an attribute named \`${key}\`, which JavaScript would put before the others`, p);
|
|
70
72
|
const name = propsToAttrMap[key] ?? key.toLowerCase();
|
|
71
73
|
if (key !== "class" && key !== "style" && !isSSRSafeAttrName(name))
|
|
72
|
-
fail(s.comp, `unsafe attribute name \`${name}\``, p);
|
|
74
|
+
fail(s.comp, "FV0404", `unsafe attribute name \`${name}\``, p);
|
|
73
75
|
fallthrough.add(p);
|
|
74
76
|
}
|
|
75
77
|
}
|
|
78
|
+
if (twin) {
|
|
79
|
+
const inits = child.props.fields.map((f) => twin.init(s, f, given.get(f.js), n));
|
|
80
|
+
twin.call(s, e, inits, slots, childAttrsArg(s, child, parts, merges, fallthrough, ids, n));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
76
83
|
const inits = child.props.fields.map((f) => {
|
|
77
84
|
const node = given.get(f.js);
|
|
78
85
|
if (!node) {
|
|
79
86
|
if (f.ty.k !== "opt")
|
|
80
|
-
fail(s.comp, `${child.name} requires \`${f.js}\``, n);
|
|
87
|
+
fail(s.comp, "FV0506", `${child.name} requires \`${f.js}\``, n);
|
|
88
|
+
if (f.ty.none !== undefined)
|
|
89
|
+
fail(s.comp, "FV0507", `${child.name} requires \`${f.js}\`, which is \`T | null\`: Vue would hand it \`undefined\`; pass \`null\` for none`, n);
|
|
81
90
|
return `${f.rust}: None`;
|
|
82
91
|
}
|
|
83
92
|
const v = expr(s, node);
|
|
84
93
|
return fieldInit(f.rust, ownInto(s.comp, { ...v, ty: markHome(v.ty, s.comp.name) }, markHome(f.ty, child.name), node));
|
|
85
94
|
});
|
|
86
|
-
callChild(s, e, child, `&super::${child.module}::Props { ${inits.join(", ")} }`, slots, childAttrsArg(s, child, parts, merges, fallthrough, ids));
|
|
95
|
+
callChild(s, e, child, `&super::${child.module}::Props { ${inits.join(", ")} }`, slots, childAttrsArg(s, child, parts, merges, fallthrough, ids, n));
|
|
87
96
|
}
|
|
88
97
|
export function array(key) {
|
|
89
98
|
return /^(0|[1-9]\d*)$/.test(key) && Number(key) < 2 ** 32 - 1;
|
|
90
99
|
}
|
|
91
|
-
function childAttrsArg(s, child, parts, merges, fallthrough, ids) {
|
|
100
|
+
function childAttrsArg(s, child, parts, merges, fallthrough, ids, n) {
|
|
92
101
|
const sources = [];
|
|
93
102
|
for (const p of parts) {
|
|
94
103
|
if (s.fallthrough !== null && ((isAttrs(p) && s.comp.inheritAttrs) || dollarAttrs(s, p))) {
|
|
@@ -105,7 +114,7 @@ function childAttrsArg(s, child, parts, merges, fallthrough, ids) {
|
|
|
105
114
|
}
|
|
106
115
|
if (!takesAttrs(child)) {
|
|
107
116
|
if (sources.length)
|
|
108
|
-
|
|
117
|
+
fail(s.comp, "FV0508", `${child.name} is passed attributes it does not take`, n);
|
|
109
118
|
return ids;
|
|
110
119
|
}
|
|
111
120
|
if (!sources.length)
|
|
@@ -124,30 +133,33 @@ export function extraParams(c) {
|
|
|
124
133
|
return (takesSlots(c) ? ", fv_slots: Slots<'_>" : "") + paramsOf(c).map((p) => `, ${p.name}: ${p.ty}`).join("");
|
|
125
134
|
}
|
|
126
135
|
export function callChild(s, e, child, propsCode, slots, attrs) {
|
|
136
|
+
const m = `super::${child.module}`;
|
|
137
|
+
const scoped = child.inherits || takesAttrs(child);
|
|
138
|
+
const trailing = paramsOf(child).map((p) => `, ${p.name}`).join("") + (scoped ? `, ${attrs ?? (takesAttrs(child) ? "&fv::Attrs::NONE" : '""')}` : "");
|
|
139
|
+
const render = scoped ? "render_scoped" : "render";
|
|
140
|
+
callWith(s, e, child, m, `${m}::${render}(out, ${propsCode}`, `${m}::Slots`, trailing, slots);
|
|
141
|
+
}
|
|
142
|
+
export function callWith(s, e, child, m, head, slotsTy, trailing, slots, vnode = false) {
|
|
127
143
|
const given = new Map();
|
|
128
144
|
if (slots && slots.type !== "NullLiteral") {
|
|
129
145
|
if (slots.type !== "ObjectExpression")
|
|
130
|
-
fail(s.comp, "slots must be an object literal", slots);
|
|
146
|
+
fail(s.comp, "FV0901", "slots must be an object literal", slots);
|
|
131
147
|
for (const p of slots.properties) {
|
|
132
148
|
const key = p.key?.type === "Identifier" ? p.key.name : p.key?.value;
|
|
133
149
|
if (key === "_")
|
|
134
150
|
continue;
|
|
135
151
|
if (p.type !== "ObjectProperty" || p.computed)
|
|
136
|
-
fail(s.comp, "slots hold plain keys", p);
|
|
152
|
+
fail(s.comp, "FV0902", "slots hold plain keys", p);
|
|
137
153
|
if (!child.slotNames.includes(key))
|
|
138
|
-
fail(s.comp, `${child.name} has no slot \`${key}\``, p);
|
|
154
|
+
fail(s.comp, "FV0903", `${child.name} has no slot \`${key}\``, p);
|
|
139
155
|
given.set(key, p.value);
|
|
140
156
|
}
|
|
141
157
|
}
|
|
142
|
-
const m = `super::${child.module}`;
|
|
143
|
-
const scoped = child.inherits || takesAttrs(child);
|
|
144
|
-
const trailing = paramsOf(child).map((p) => `, ${p.name}`).join("") + (scoped ? `, ${attrs ?? (takesAttrs(child) ? "&fv::Attrs::NONE" : '""')}` : "");
|
|
145
|
-
const render = scoped ? "render_scoped" : "render";
|
|
146
158
|
if (!takesSlots(child)) {
|
|
147
|
-
e.stmt(`${
|
|
159
|
+
e.stmt(`${head}${trailing});`);
|
|
148
160
|
return;
|
|
149
161
|
}
|
|
150
|
-
e.open(`${
|
|
162
|
+
e.open(`${head}, ${slotsTy}`);
|
|
151
163
|
for (const name of child.slotNames) {
|
|
152
164
|
const field = snake(name);
|
|
153
165
|
const value = given.get(name);
|
|
@@ -182,7 +194,7 @@ export function callChild(s, e, child, propsCode, slots, attrs) {
|
|
|
182
194
|
if (param?.type === "ObjectPattern") {
|
|
183
195
|
for (const p of param.properties) {
|
|
184
196
|
if (p.type !== "ObjectProperty" || p.computed || p.value.type !== "Identifier") {
|
|
185
|
-
fail(s.comp, "slot props are destructured into plain names, without defaults", p);
|
|
197
|
+
fail(s.comp, "FV0904", "slot props are destructured into plain names, without defaults", p);
|
|
186
198
|
}
|
|
187
199
|
locals.set(p.value.name, fieldVal(s.comp, sp, ty, p.key.name ?? p.key.value, p));
|
|
188
200
|
}
|
|
@@ -191,19 +203,19 @@ export function callChild(s, e, child, propsCode, slots, attrs) {
|
|
|
191
203
|
locals.set(param.name, { code: sp, ty });
|
|
192
204
|
}
|
|
193
205
|
else if (!takesNone)
|
|
194
|
-
fail(s.comp, "slot props are a name or an object pattern", param);
|
|
206
|
+
fail(s.comp, "FV0905", "slot props are a name or an object pattern", param);
|
|
195
207
|
const life = shape.fields.some((f) => slotFieldBorrows(f.ty)) ? "<'_>" : "";
|
|
196
208
|
e.open(`${field}: Some(&|out: &mut String, ${sp}: &${m}::${shape.name}${life}${sidParam}| -> bool`);
|
|
197
209
|
const opened = e.lines.length - 1;
|
|
198
|
-
content({ ...s, locals, sid });
|
|
210
|
+
content({ ...s, locals, sid, vnode });
|
|
199
211
|
unread(opened, sp);
|
|
200
212
|
unread(opened, sid);
|
|
201
213
|
e.close("),");
|
|
202
214
|
continue;
|
|
203
215
|
}
|
|
204
216
|
if (!takesNone)
|
|
205
|
-
fail(s.comp, `\`<slot${name === "default" ? "" : ` name="${name}"`}>\` in ${child.name} passes no props`, param);
|
|
206
|
-
const inner = { ...s, sid };
|
|
217
|
+
fail(s.comp, "FV0906", `\`<slot${name === "default" ? "" : ` name="${name}"`}>\` in ${child.name} passes no props`, param);
|
|
218
|
+
const inner = { ...s, sid, vnode };
|
|
207
219
|
if (sid !== null) {
|
|
208
220
|
e.open(`${field}: Some(fv::Slot::slotted(&|out: &mut String${sidParam}| -> bool`);
|
|
209
221
|
const opened = e.lines.length - 1;
|
|
@@ -227,14 +239,15 @@ export function callChild(s, e, child, propsCode, slots, attrs) {
|
|
|
227
239
|
e.close(`${trailing});`);
|
|
228
240
|
}
|
|
229
241
|
export function ownInto(comp, v, want, node) {
|
|
242
|
+
holdsNothing(comp, v, want, node, "the prop");
|
|
243
|
+
if (want.k === "opt" && nothing(v.ty))
|
|
244
|
+
return "None";
|
|
230
245
|
if (want.k === "str") {
|
|
231
246
|
if (v.ty.k !== "str")
|
|
232
|
-
fail(comp, "a string prop needs a string", node);
|
|
247
|
+
fail(comp, "FV0509", "a string prop needs a string", node);
|
|
233
248
|
return `std::borrow::Cow::Borrowed(${bare(v.code)})`;
|
|
234
249
|
}
|
|
235
250
|
if (want.k === "opt" && want.of.k === "str") {
|
|
236
|
-
if (v.ty.k === "undef")
|
|
237
|
-
return "None";
|
|
238
251
|
if (v.ty.k === "str")
|
|
239
252
|
return `Some(std::borrow::Cow::Borrowed(${bare(v.code)}))`;
|
|
240
253
|
if (v.ty.k === "opt" && v.ty.of.k === "str")
|
|
@@ -257,7 +270,7 @@ export function ownInto(comp, v, want, node) {
|
|
|
257
270
|
if (want.k === "opt" && objecty(want.of) && sameTy(v.ty, want.of))
|
|
258
271
|
return `Some(${atom(v.code)}.to_owned())`;
|
|
259
272
|
if (objecty(want) && v.ty.k === want.k && JSON.stringify(v.ty).includes('"struct"')) {
|
|
260
|
-
fail(comp, `a ${JSON.stringify(v.ty)} where the child takes a ${JSON.stringify(want)}: two components share a type only when both import it from one \`.ts\` file`, node);
|
|
273
|
+
fail(comp, "FV0510", `a ${JSON.stringify(v.ty)} where the child takes a ${JSON.stringify(want)}: two components share a type only when both import it from one \`.ts\` file`, node);
|
|
261
274
|
}
|
|
262
275
|
if (want.k === "list" && v.ty.k === "list" && v.ty.of.k === "undef")
|
|
263
276
|
return "Vec::new()";
|
|
@@ -269,5 +282,5 @@ export function ownInto(comp, v, want, node) {
|
|
|
269
282
|
if (want.of.k === "int" || want.of.k === "float" || want.of.k === "bool")
|
|
270
283
|
return `${atom(v.code)}.to_vec()`;
|
|
271
284
|
}
|
|
272
|
-
return fail(comp, `a ${JSON.stringify(v.ty)} into a ${JSON.stringify(want)} prop`, node);
|
|
285
|
+
return fail(comp, "FV0511", `a ${JSON.stringify(v.ty)} into a ${JSON.stringify(want)} prop`, node);
|
|
273
286
|
}
|
package/dist/classes.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { escapeHtml } from "@vue/shared";
|
|
2
|
-
import { fail, rustStr, STR } from "./model.js";
|
|
2
|
+
import { fail, nothing, rustStr, STR } from "./model.js";
|
|
3
3
|
import { expr } from "./expr.js";
|
|
4
4
|
import { cond, known, truthy } from "./narrowing.js";
|
|
5
5
|
import { meet } from "./strings.js";
|
|
@@ -25,10 +25,10 @@ export function classItems(s, n) {
|
|
|
25
25
|
const keys = [];
|
|
26
26
|
const entries = n.properties.map((p) => {
|
|
27
27
|
if (p.type !== "ObjectProperty")
|
|
28
|
-
fail(s.comp, "a class object holds `name: condition` pairs", p);
|
|
28
|
+
fail(s.comp, "FV1001", "a class object holds `name: condition` pairs", p);
|
|
29
29
|
const key = p.computed ? expr(s, p.key) : { code: rustStr(p.key.type === "Identifier" ? p.key.name : String(p.key.value)), ty: STR };
|
|
30
30
|
if (key.ty.k !== "str")
|
|
31
|
-
fail(s.comp, "a computed class name is a string", p.key);
|
|
31
|
+
fail(s.comp, "FV1002", "a computed class name is a string", p.key);
|
|
32
32
|
for (const other of keys)
|
|
33
33
|
meet(s.comp, other, key, "a class object's names", p.key, "equal");
|
|
34
34
|
keys.push(key);
|
|
@@ -38,18 +38,18 @@ export function classItems(s, n) {
|
|
|
38
38
|
}
|
|
39
39
|
return n.properties.flatMap((p) => {
|
|
40
40
|
if (p.type !== "ObjectProperty")
|
|
41
|
-
fail(s.comp, "a class object holds `name: condition` pairs", p);
|
|
41
|
+
fail(s.comp, "FV1001", "a class object holds `name: condition` pairs", p);
|
|
42
42
|
let name;
|
|
43
43
|
if (!p.computed) {
|
|
44
44
|
const key = p.key.type === "Identifier" ? p.key.name : String(p.key.value);
|
|
45
45
|
if (key !== key.trim() || !key)
|
|
46
|
-
fail(s.comp, `class name \`${key}\` has spaces around it`, p);
|
|
46
|
+
fail(s.comp, "FV1003", `class name \`${key}\` has spaces around it`, p);
|
|
47
47
|
name = { lit: key };
|
|
48
48
|
}
|
|
49
49
|
else {
|
|
50
50
|
const k = expr(s, p.key);
|
|
51
51
|
if (k.ty.k !== "str")
|
|
52
|
-
fail(s.comp, "a computed class name is a string", p.key);
|
|
52
|
+
fail(s.comp, "FV1002", "a computed class name is a string", p.key);
|
|
53
53
|
name = { code: `fv::js_trim(${strArg(k.code)})` };
|
|
54
54
|
}
|
|
55
55
|
const v = expr(s, p.value);
|
|
@@ -77,9 +77,9 @@ export function classItems(s, n) {
|
|
|
77
77
|
return [{ code: v.code }];
|
|
78
78
|
if (v.ty.k === "opt" && v.ty.of.k === "str")
|
|
79
79
|
return [{ code: `${atom(v.code)}.unwrap_or("")` }];
|
|
80
|
-
if (v.ty
|
|
80
|
+
if (nothing(v.ty))
|
|
81
81
|
return [];
|
|
82
|
-
return fail(s.comp, "a class is a string, an array, or an object of conditions", n);
|
|
82
|
+
return fail(s.comp, "FV1004", "a class is a string, an array, or an object of conditions", n);
|
|
83
83
|
}
|
|
84
84
|
function conditional(test, items) {
|
|
85
85
|
if (test === "true" || test === "false")
|
|
@@ -123,7 +123,7 @@ export function classPresent(s, n) {
|
|
|
123
123
|
break;
|
|
124
124
|
}
|
|
125
125
|
const v = expr(s, n);
|
|
126
|
-
if (v.ty
|
|
126
|
+
if (nothing(v.ty))
|
|
127
127
|
return "false";
|
|
128
128
|
if (v.ty.k === "opt")
|
|
129
129
|
return `${atom(v.code)}.is_some()`;
|
|
@@ -170,7 +170,7 @@ export function classAttr(s, n, side) {
|
|
|
170
170
|
return `if ${condition(cond(s, n.test))} { ${classAttr(s, n.consequent, side)} } else { ${classAttr(s, n.alternate, side)} }`;
|
|
171
171
|
}
|
|
172
172
|
const v = expr(s, n);
|
|
173
|
-
if (v.ty.k === "str" || v.ty
|
|
173
|
+
if (v.ty.k === "str" || nothing(v.ty) || (v.ty.k === "opt" && v.ty.of.k === "str"))
|
|
174
174
|
return valueAttr(s, v, n);
|
|
175
|
-
return fail(s.comp, "a class is a string, an array, or an object of conditions", n);
|
|
175
|
+
return fail(s.comp, "FV1004", "a class is a string, an array, or an object of conditions", n);
|
|
176
176
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, watch, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
|
-
import { CONFIG_FILE,
|
|
4
|
+
import { CONFIG_FILE, generate, loadConfig, VERSION, write } from "./compiler.js";
|
|
5
5
|
import { unifiedDiff } from "./diff.js";
|
|
6
|
+
import { diagnose, formatRefusal, isRefusal } from "./diagnostics.js";
|
|
6
7
|
const root = process.cwd();
|
|
7
8
|
const HELP = `Usage: ferrovue [command] [options]
|
|
8
9
|
|
|
@@ -14,16 +15,23 @@ Commands:
|
|
|
14
15
|
Options:
|
|
15
16
|
--check check that generated files match committed files without writing
|
|
16
17
|
-d, --diff show unified diff of changes when checking (use with --check)
|
|
18
|
+
--format <format> how to write errors and results: human (default) or json
|
|
17
19
|
--watch watch components, stores, routes and config for changes and regenerate
|
|
18
20
|
-c, --config <path> path to configuration file (default: ferrovue.config.json)
|
|
19
21
|
-v, --version print the version and exit
|
|
20
22
|
-h, --help print this help and exit
|
|
21
23
|
`;
|
|
24
|
+
let json = false;
|
|
25
|
+
function emit(result) {
|
|
26
|
+
console.log(JSON.stringify({ diagnostics: [], ...result }));
|
|
27
|
+
}
|
|
22
28
|
function report(e) {
|
|
23
|
-
if (e
|
|
24
|
-
console.error(`error: ${e.message}`);
|
|
25
|
-
else
|
|
29
|
+
if (!isRefusal(e))
|
|
26
30
|
throw e;
|
|
31
|
+
if (json)
|
|
32
|
+
emit({ diagnostics: [diagnose(e)] });
|
|
33
|
+
else
|
|
34
|
+
console.error(formatRefusal(e));
|
|
27
35
|
}
|
|
28
36
|
function initProject(targetRoot, configPath) {
|
|
29
37
|
const configFile = configPath ? resolve(targetRoot, configPath) : join(targetRoot, CONFIG_FILE);
|
|
@@ -77,6 +85,10 @@ function check(config, showDiff = false) {
|
|
|
77
85
|
}
|
|
78
86
|
});
|
|
79
87
|
const extra = have.filter((name) => !want.has(name));
|
|
88
|
+
if (json) {
|
|
89
|
+
emit({ out: config.out, stale: stale.map((name) => `${config.out}/${name}`), notGenerated: extra.map((name) => `${config.out}/${name}`) });
|
|
90
|
+
return stale.length || extra.length ? 1 : 0;
|
|
91
|
+
}
|
|
80
92
|
if (stale.length || extra.length) {
|
|
81
93
|
const committed = (name) => {
|
|
82
94
|
try {
|
|
@@ -104,6 +116,8 @@ function check(config, showDiff = false) {
|
|
|
104
116
|
}
|
|
105
117
|
function once(config) {
|
|
106
118
|
const { files, changed, removed } = write(root, config);
|
|
119
|
+
if (json)
|
|
120
|
+
return emit({ out: config.out, files, changed, removed });
|
|
107
121
|
const what = changed.length || removed.length ? `${changed.length} changed, ${removed.length} removed` : "nothing changed";
|
|
108
122
|
console.log(`${config.out}: ${files.length} files, ${what}`);
|
|
109
123
|
}
|
|
@@ -137,7 +151,7 @@ function watchProject(configPath) {
|
|
|
137
151
|
clearTimeout(timer);
|
|
138
152
|
timer = setTimeout(run, 50);
|
|
139
153
|
});
|
|
140
|
-
console.log("watching for changes (Ctrl-C to stop)");
|
|
154
|
+
(json ? console.error : console.log)("watching for changes (Ctrl-C to stop)");
|
|
141
155
|
}
|
|
142
156
|
try {
|
|
143
157
|
const rawArgs = process.argv.slice(2);
|
|
@@ -155,6 +169,14 @@ try {
|
|
|
155
169
|
else if (arg.startsWith("--config=")) {
|
|
156
170
|
configPath = arg.slice("--config=".length);
|
|
157
171
|
}
|
|
172
|
+
else if (arg === "--format" || arg.startsWith("--format=")) {
|
|
173
|
+
const format = arg === "--format" ? rawArgs[++i] : arg.slice("--format=".length);
|
|
174
|
+
if (format !== "human" && format !== "json") {
|
|
175
|
+
console.error(`error: '--format' is 'human' or 'json'`);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
json = format === "json";
|
|
179
|
+
}
|
|
158
180
|
else {
|
|
159
181
|
args.push(arg);
|
|
160
182
|
}
|
|
@@ -169,6 +191,10 @@ try {
|
|
|
169
191
|
console.error("error: '--diff' requires '--check'");
|
|
170
192
|
process.exit(1);
|
|
171
193
|
}
|
|
194
|
+
if ((args.includes("--diff") || args.includes("-d")) && json) {
|
|
195
|
+
console.error("error: '--diff' writes text, and does not combine with '--format json'");
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
172
198
|
if (args.includes("--version") || args.includes("-v")) {
|
|
173
199
|
console.log(VERSION);
|
|
174
200
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type App, type Component, type Plugin } from "vue";
|
|
2
2
|
import type { Pinia } from "pinia";
|
|
3
3
|
import type { Router } from "vue-router";
|
|
4
|
-
/** Give Pinia the state the server rendered with
|
|
4
|
+
/** Give Pinia the state the server rendered with (what `ferrovue::state_script_into` wrote) before
|
|
5
5
|
* the app mounts, so every store starts from it and the hydrated markup agrees with the server's. */
|
|
6
6
|
export declare function hydrateState(pinia: Pinia, id?: string, doc?: Document): void;
|
|
7
7
|
export interface MountOptions {
|
|
@@ -18,7 +18,7 @@ export interface MountOptions {
|
|
|
18
18
|
* those given or its props are malformed. Warns on the console by default. */
|
|
19
19
|
onError?: (element: Element, problem: string) => void;
|
|
20
20
|
}
|
|
21
|
-
/** A component, or a function that loads one
|
|
21
|
+
/** A component, or a function that loads one (`() => import("./Counter.vue")`), so that the code
|
|
22
22
|
* of an island not on the page is never fetched. A function is taken for a loader as vue-router
|
|
23
23
|
* takes one for a lazy route: unless it has `props` or `displayName`, which mark a functional
|
|
24
24
|
* component, or `__vccOpts`, a class component. `ferrovue/islands` maps the name of every island
|
|
@@ -38,3 +38,36 @@ export interface Islands {
|
|
|
38
38
|
* rendered it with, mounted where it is. A loader is called only for an island the page holds, all
|
|
39
39
|
* of them at once, and the islands mount in document order once every one has loaded. */
|
|
40
40
|
export declare function mountIslands(components: Record<string, IslandComponent>, options?: MountOptions): Promise<Islands>;
|
|
41
|
+
/** Content only the browser renders. The server writes the `#fallback` slot, or nothing; the
|
|
42
|
+
* browser hydrates that fallback, then swaps in the default slot once mounted. ferrovue does not
|
|
43
|
+
* compile the default slot, so anything may go there: a component library's components, code that
|
|
44
|
+
* reads `window`. The swap happens only where Vue runs on the page, as in an island. */
|
|
45
|
+
export declare const ClientOnly: Component;
|
|
46
|
+
/** One component the server rendered into a slot of a page's layout, as `ferrovue::Part` records
|
|
47
|
+
* it: its name (`c`) and the props it was rendered from (`p`). */
|
|
48
|
+
export interface PagePart {
|
|
49
|
+
c: string;
|
|
50
|
+
p: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
/** What `ferrovue::PageRecord` writes: the layout's props, and each slot's parts in order. */
|
|
53
|
+
export interface PageRecord {
|
|
54
|
+
props: Record<string, unknown>;
|
|
55
|
+
slots: Record<string, PagePart[]>;
|
|
56
|
+
}
|
|
57
|
+
export interface PageOptions extends Pick<MountOptions, "pinia" | "router" | "plugins"> {
|
|
58
|
+
/** The element holding the layout's root, or a selector for it: `#app` by default. */
|
|
59
|
+
container?: Element | string;
|
|
60
|
+
/** The `id` of the record's script, as given to `PageRecord::script_into`: `__fv_page` by default. */
|
|
61
|
+
record?: string;
|
|
62
|
+
/** The document to read the page from. */
|
|
63
|
+
doc?: Document;
|
|
64
|
+
}
|
|
65
|
+
/** The app of a page: its root renders `layout` with the record's props, and each slot as the
|
|
66
|
+
* components the record names, in order, as a plain array, which hydrates the markup the server
|
|
67
|
+
* wrote with no fragment of its own. `components` must hold every component the record names. */
|
|
68
|
+
export declare function createPageApp(layout: Component, record: PageRecord, components: Record<string, Component>, options?: PageOptions): App;
|
|
69
|
+
/** Hydrate a page `ferrovue::Page` rendered: read its record, load the components it names (and
|
|
70
|
+
* no others), wait for the router, and mount the layout on the container. Rejects, leaving the page
|
|
71
|
+
* as the server rendered it, when the container or the record is missing or a component is not
|
|
72
|
+
* given or does not load. */
|
|
73
|
+
export declare function mountPage(layout: IslandComponent, components: Record<string, IslandComponent>, options?: PageOptions): Promise<App>;
|