ferrovue 0.4.0 → 0.6.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 +378 -63
- package/dist/attrs.d.ts +3 -2
- package/dist/attrs.js +31 -26
- package/dist/calls.js +17 -15
- package/dist/children.d.ts +11 -4
- package/dist/children.js +110 -44
- package/dist/classes.d.ts +4 -0
- package/dist/classes.js +33 -11
- package/dist/cli.js +31 -5
- package/dist/client.d.ts +44 -6
- package/dist/client.js +182 -18
- package/dist/compiler.d.ts +1 -1
- package/dist/compiler.js +42 -10
- package/dist/component.d.ts +3 -1
- package/dist/component.js +114 -20
- package/dist/conformance.d.ts +64 -0
- package/dist/conformance.js +276 -0
- package/dist/constants.d.ts +42 -0
- package/dist/constants.js +292 -0
- package/dist/context.d.ts +40 -7
- package/dist/context.js +14 -5
- package/dist/diagnostics.d.ts +23 -0
- package/dist/diagnostics.js +30 -0
- package/dist/dynamic.d.ts +18 -0
- package/dist/dynamic.js +576 -0
- package/dist/errors.d.ts +1034 -0
- package/dist/errors.js +425 -0
- package/dist/expr.d.ts +1 -0
- package/dist/expr.js +120 -49
- package/dist/file-routes.d.ts +33 -0
- package/dist/file-routes.js +224 -0
- package/dist/fixture.d.ts +19 -10
- package/dist/fixture.js +23 -12
- package/dist/hydration.d.ts +15 -0
- package/dist/hydration.js +80 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/link-router.d.ts +24 -0
- package/dist/link-router.js +37 -0
- package/dist/lists.d.ts +4 -0
- package/dist/lists.js +32 -23
- package/dist/loops.js +18 -14
- package/dist/model.d.ts +43 -1
- package/dist/model.js +167 -7
- package/dist/narrowing.d.ts +8 -0
- package/dist/narrowing.js +40 -9
- package/dist/numbers.js +3 -2
- package/dist/page-routes.d.ts +9 -0
- package/dist/page-routes.js +10 -0
- package/dist/plugin.d.ts +13 -1
- package/dist/plugin.js +3 -0
- package/dist/plugins/client-only.d.ts +8 -0
- package/dist/plugins/client-only.js +110 -0
- package/dist/plugins/head.d.ts +15 -0
- package/dist/plugins/head.js +385 -0
- package/dist/plugins/i18n.js +14 -14
- package/dist/plugins/index.js +5 -1
- package/dist/plugins/provide.d.ts +44 -0
- package/dist/plugins/provide.js +528 -0
- package/dist/plugins/router-link.js +39 -32
- package/dist/plugins/router.d.ts +4 -1
- package/dist/plugins/router.js +65 -19
- package/dist/plugins/scoped.js +1 -1
- package/dist/plugins/shared-types.js +1 -1
- package/dist/plugins/stores.js +45 -33
- 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 +13 -0
- package/dist/routes.js +12 -0
- package/dist/rust.js +64 -32
- package/dist/script.d.ts +3 -0
- package/dist/script.js +138 -20
- package/dist/settle.d.ts +8 -0
- package/dist/settle.js +39 -0
- package/dist/slots.js +36 -22
- package/dist/ssr.d.ts +3 -0
- package/dist/ssr.js +29 -0
- package/dist/strings.d.ts +3 -0
- package/dist/strings.js +27 -13
- package/dist/styles.js +13 -13
- package/dist/template.js +102 -21
- package/dist/testing.d.ts +3 -3
- package/dist/testing.js +3 -29
- package/dist/types.d.ts +5 -0
- package/dist/typescript.d.ts +2 -1
- package/dist/typescript.js +52 -21
- package/dist/vhtml.d.ts +4 -0
- package/dist/vhtml.js +63 -0
- package/dist/vite.d.ts +8 -2
- package/dist/vite.js +75 -14
- package/package.json +23 -5
package/dist/children.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
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";
|
|
9
9
|
import { attrOf, dollarAttrs, IGNORED_PROPS, isAttrs, mergedParts } from "./attrs.js";
|
|
10
10
|
import { passedKey } from "./fallthrough.js";
|
|
11
|
-
import { claim, paramsOf, slotFieldsOf } from "./plugin.js";
|
|
11
|
+
import { claim, paramsOf, slotContextOf, slotFieldsOf } from "./plugin.js";
|
|
12
12
|
import { statements } from "./template.js";
|
|
13
13
|
import { slotContent, slotFieldBorrows, staticallyFilled } from "./slots.js";
|
|
14
|
-
|
|
14
|
+
import { slotAsVnodes } from "./dynamic.js";
|
|
15
|
+
export function renderChild(s, e, n, twin) {
|
|
15
16
|
const [target, rawProps, slots, , slotScopeId] = n.arguments;
|
|
16
17
|
let local = null;
|
|
17
18
|
if (target.type === "MemberExpression" && target.object.name === "$setup") {
|
|
@@ -19,9 +20,10 @@ export function renderChild(s, e, n) {
|
|
|
19
20
|
}
|
|
20
21
|
const isSelf = target.type === "Identifier" && target.name === s.selfAlias.name;
|
|
21
22
|
const childName = isSelf ? s.comp.name : local ? s.children.get(local) : undefined;
|
|
22
|
-
const child = childName ? s.components.get(childName) : undefined;
|
|
23
|
+
const child = twin?.comp ?? (childName ? s.components.get(childName) : undefined);
|
|
23
24
|
if (!child)
|
|
24
|
-
fail(s.comp, "a child component must be
|
|
25
|
+
fail(s.comp, "FV0501", `a child component must be imported from a \`.vue\` file among the components compiled, in ${ctx.componentsDir.replace(/\/$/, "")}`, n);
|
|
26
|
+
const later = !twin && local !== null && s.loadedLater.has(local);
|
|
25
27
|
for (const p of ctx.plugins)
|
|
26
28
|
p.child?.(s, child, n);
|
|
27
29
|
const parts = !rawProps || rawProps.type === "NullLiteral" ? [] : mergedParts(rawProps);
|
|
@@ -33,30 +35,33 @@ export function renderChild(s, e, n) {
|
|
|
33
35
|
if (parts.some(passed)) {
|
|
34
36
|
for (const name of s.comp.attrNames) {
|
|
35
37
|
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);
|
|
38
|
+
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
39
|
}
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
const ids = claim((p) => p.childIds?.(s, child, passesAttrs, !!slotScopeId, n)) ?? null;
|
|
41
43
|
if (objects.length === 1 && objects[0].type !== "ObjectExpression") {
|
|
44
|
+
if (twin)
|
|
45
|
+
fail(s.comp, "FV1501", `the props of ${child.name}, a Rust twin, are attributes or an object literal`, objects[0]);
|
|
42
46
|
const v = expr(s, objects[0]);
|
|
43
47
|
const own = child.name === s.comp.name && v.ty.k === "struct" && v.ty.name === "Props";
|
|
44
48
|
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));
|
|
49
|
+
callChild(s, e, child, v.code, slots, childAttrsArg(s, child, parts, merges, null, ids, n), later);
|
|
46
50
|
return;
|
|
47
51
|
}
|
|
48
|
-
fail(s.comp, `child props must be an object literal, or \`v-bind\` of ${child.name}'s own \`Props\``, n);
|
|
52
|
+
fail(s.comp, "FV0503", `child props must be an object literal, or \`v-bind\` of ${child.name}'s own \`Props\``, n);
|
|
49
53
|
}
|
|
50
54
|
for (const p of objects) {
|
|
51
55
|
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);
|
|
56
|
+
fail(s.comp, "FV0503", `child props must be an object literal, or \`v-bind\` of ${child.name}'s own \`Props\``, p);
|
|
53
57
|
}
|
|
54
58
|
const given = new Map();
|
|
55
|
-
const fallthrough = new
|
|
59
|
+
const fallthrough = new Map();
|
|
60
|
+
const loaded = !merges && local !== null && s.loadedLater.has(local);
|
|
56
61
|
for (const obj of objects) {
|
|
57
62
|
for (const p of obj.properties) {
|
|
58
63
|
if (p.type !== "ObjectProperty" || p.computed)
|
|
59
|
-
fail(s.comp, "child props hold plain keys", p);
|
|
64
|
+
fail(s.comp, "FV0504", "child props hold plain keys", p);
|
|
60
65
|
const key = p.key.type === "Identifier" ? p.key.name : String(p.key.value);
|
|
61
66
|
if (!passedKey(child, key))
|
|
62
67
|
continue;
|
|
@@ -66,29 +71,63 @@ export function renderChild(s, e, n) {
|
|
|
66
71
|
continue;
|
|
67
72
|
}
|
|
68
73
|
if (array(key))
|
|
69
|
-
fail(s.comp, `an attribute named \`${key}\`, which JavaScript would put before the others`, p);
|
|
74
|
+
fail(s.comp, "FV0505", `an attribute named \`${key}\`, which JavaScript would put before the others`, p);
|
|
70
75
|
const name = propsToAttrMap[key] ?? key.toLowerCase();
|
|
71
76
|
if (key !== "class" && key !== "style" && !isSSRSafeAttrName(name))
|
|
72
|
-
fail(s.comp, `unsafe attribute name \`${name}\``, p);
|
|
73
|
-
fallthrough.
|
|
77
|
+
fail(s.comp, "FV0404", `unsafe attribute name \`${name}\``, p);
|
|
78
|
+
fallthrough.set(p, loaded && key === "class" ? loadedClass(s, p.value) : p.value);
|
|
74
79
|
}
|
|
75
80
|
}
|
|
81
|
+
if (twin) {
|
|
82
|
+
const inits = child.props.fields.map((f) => twin.init(s, f, given.get(f.js), n));
|
|
83
|
+
twin.call(s, e, inits, slots, childAttrsArg(s, child, parts, merges, fallthrough, ids, n));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
76
86
|
const inits = child.props.fields.map((f) => {
|
|
77
87
|
const node = given.get(f.js);
|
|
78
88
|
if (!node) {
|
|
79
89
|
if (f.ty.k !== "opt")
|
|
80
|
-
fail(s.comp, `${child.name} requires \`${f.js}\``, n);
|
|
90
|
+
fail(s.comp, "FV0506", `${child.name} requires \`${f.js}\``, n);
|
|
91
|
+
if (f.ty.none !== undefined)
|
|
92
|
+
fail(s.comp, "FV0507", `${child.name} requires \`${f.js}\`, which is \`T | null\`: Vue would hand it \`undefined\`; pass \`null\` for none`, n);
|
|
81
93
|
return `${f.rust}: None`;
|
|
82
94
|
}
|
|
83
95
|
const v = expr(s, node);
|
|
84
96
|
return fieldInit(f.rust, ownInto(s.comp, { ...v, ty: markHome(v.ty, s.comp.name) }, markHome(f.ty, child.name), node));
|
|
85
97
|
});
|
|
86
|
-
callChild(s, e, child, `&super::${child.module}::Props { ${inits.join(", ")} }`, slots, childAttrsArg(s, child, parts, merges, fallthrough, ids));
|
|
98
|
+
callChild(s, e, child, `&super::${child.module}::Props { ${inits.join(", ")} }`, slots, childAttrsArg(s, child, parts, merges, fallthrough, ids, n), later);
|
|
87
99
|
}
|
|
88
100
|
export function array(key) {
|
|
89
101
|
return /^(0|[1-9]\d*)$/.test(key) && Number(key) < 2 ** 32 - 1;
|
|
90
102
|
}
|
|
91
|
-
|
|
103
|
+
/** A string `:class` given to a component `defineAsyncComponent` loads, in props no `mergeProps`
|
|
104
|
+
* normalised: Vue's wrapper hands on the class once as written and once trimmed, so a class with
|
|
105
|
+
* whitespace around it is written twice. */
|
|
106
|
+
const lengthOf = (n) => ({ type: "MemberExpression", object: n, property: { type: "Identifier", name: "length" }, computed: false });
|
|
107
|
+
function holdsClassLiteral(n) {
|
|
108
|
+
if (n.type === "ObjectExpression" || n.type === "ArrayExpression")
|
|
109
|
+
return true;
|
|
110
|
+
if (n.type === "ConditionalExpression")
|
|
111
|
+
return holdsClassLiteral(n.consequent) || holdsClassLiteral(n.alternate);
|
|
112
|
+
if (n.type === "LogicalExpression")
|
|
113
|
+
return holdsClassLiteral(n.left) || holdsClassLiteral(n.right);
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
function loadedClass(s, value) {
|
|
117
|
+
if (value.type === "ConditionalExpression" && (holdsClassLiteral(value.consequent) || holdsClassLiteral(value.alternate))) {
|
|
118
|
+
return { ...value, consequent: loadedClass(s, value.consequent), alternate: loadedClass(s, value.alternate) };
|
|
119
|
+
}
|
|
120
|
+
if (holdsClassLiteral(value) || expr(s, value).ty.k !== "str")
|
|
121
|
+
return value;
|
|
122
|
+
const trimmed = { type: "CallExpression", callee: { type: "MemberExpression", object: value, property: { type: "Identifier", name: "trim" }, computed: false }, arguments: [] };
|
|
123
|
+
return {
|
|
124
|
+
type: "ConditionalExpression",
|
|
125
|
+
test: { type: "BinaryExpression", operator: "===", left: lengthOf(trimmed), right: lengthOf(value) },
|
|
126
|
+
consequent: value,
|
|
127
|
+
alternate: { type: "ArrayExpression", elements: [trimmed, value] },
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function childAttrsArg(s, child, parts, merges, fallthrough, ids, n) {
|
|
92
131
|
const sources = [];
|
|
93
132
|
for (const p of parts) {
|
|
94
133
|
if (s.fallthrough !== null && ((isAttrs(p) && s.comp.inheritAttrs) || dollarAttrs(s, p))) {
|
|
@@ -97,7 +136,7 @@ function childAttrsArg(s, child, parts, merges, fallthrough, ids) {
|
|
|
97
136
|
else if (p.type === "ObjectExpression" && fallthrough !== null) {
|
|
98
137
|
const entries = p.properties.filter((q) => fallthrough.has(q)).map((q) => {
|
|
99
138
|
const key = q.key.type === "Identifier" ? q.key.name : String(q.key.value);
|
|
100
|
-
return `(${rustStr(key)}, ${attrOf(s, key, q
|
|
139
|
+
return `(${rustStr(key)}, ${attrOf(s, key, fallthrough.get(q), "vnode")})`;
|
|
101
140
|
});
|
|
102
141
|
if (entries.length)
|
|
103
142
|
sources.push(`&[${entries.join(", ")}]`);
|
|
@@ -105,7 +144,7 @@ function childAttrsArg(s, child, parts, merges, fallthrough, ids) {
|
|
|
105
144
|
}
|
|
106
145
|
if (!takesAttrs(child)) {
|
|
107
146
|
if (sources.length)
|
|
108
|
-
|
|
147
|
+
fail(s.comp, "FV0508", `${child.name} is passed attributes it does not take`, n);
|
|
109
148
|
return ids;
|
|
110
149
|
}
|
|
111
150
|
if (!sources.length)
|
|
@@ -120,34 +159,48 @@ export function fieldInit(name, value) {
|
|
|
120
159
|
export function takesSlots(c) {
|
|
121
160
|
return c.slotNames.length > 0 || slotFieldsOf(c).length > 0;
|
|
122
161
|
}
|
|
123
|
-
export function extraParams(c) {
|
|
124
|
-
return (takesSlots(c) ? ", fv_slots: Slots<'_>" : "") + paramsOf(c).map((p) => `, ${p.name}: ${p.ty}`).join("");
|
|
162
|
+
export function extraParams(c, unread = () => false) {
|
|
163
|
+
return (takesSlots(c) ? ", fv_slots: Slots<'_>" : "") + paramsOf(c).map((p) => `, ${unread(p.name) ? "_" : ""}${p.name}: ${p.ty}`).join("");
|
|
164
|
+
}
|
|
165
|
+
export function callChild(s, e, child, propsCode, slots, attrs, later = false) {
|
|
166
|
+
const m = `super::${child.module}`;
|
|
167
|
+
const scoped = child.inherits || takesAttrs(child);
|
|
168
|
+
const trailing = paramsOf(child).map((p) => `, ${p.name}`).join("") + (scoped ? `, ${attrs ?? (takesAttrs(child) ? "&fv::Attrs::NONE" : '""')}` : "");
|
|
169
|
+
const render = scoped ? "render_scoped" : "render";
|
|
170
|
+
const around = later ? ctx.plugins.flatMap((p) => p.loadedLater?.(child) ?? []) : [];
|
|
171
|
+
if (around.length) {
|
|
172
|
+
e.open("");
|
|
173
|
+
for (const line of around)
|
|
174
|
+
e.stmt(line);
|
|
175
|
+
}
|
|
176
|
+
callWith(s, e, child, m, `${m}::${render}(out, ${propsCode}`, `${m}::Slots`, trailing, slots);
|
|
177
|
+
if (around.length)
|
|
178
|
+
e.close();
|
|
125
179
|
}
|
|
126
|
-
export function
|
|
180
|
+
export function callWith(s, e, child, m, head, slotsTy, trailing, slots, vnode = false) {
|
|
127
181
|
const given = new Map();
|
|
128
182
|
if (slots && slots.type !== "NullLiteral") {
|
|
129
183
|
if (slots.type !== "ObjectExpression")
|
|
130
|
-
fail(s.comp, "slots must be an object literal", slots);
|
|
184
|
+
fail(s.comp, "FV0901", "slots must be an object literal", slots);
|
|
131
185
|
for (const p of slots.properties) {
|
|
132
186
|
const key = p.key?.type === "Identifier" ? p.key.name : p.key?.value;
|
|
133
187
|
if (key === "_")
|
|
134
188
|
continue;
|
|
135
189
|
if (p.type !== "ObjectProperty" || p.computed)
|
|
136
|
-
fail(s.comp, "slots hold plain keys", p);
|
|
190
|
+
fail(s.comp, "FV0902", "slots hold plain keys", p);
|
|
137
191
|
if (!child.slotNames.includes(key))
|
|
138
|
-
fail(s.comp, `${child.name} has no slot \`${key}\``, p);
|
|
192
|
+
fail(s.comp, "FV0903", `${child.name} has no slot \`${key}\``, p);
|
|
139
193
|
given.set(key, p.value);
|
|
140
194
|
}
|
|
141
195
|
}
|
|
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
196
|
if (!takesSlots(child)) {
|
|
147
|
-
e.stmt(`${
|
|
197
|
+
e.stmt(`${head}${trailing});`);
|
|
148
198
|
return;
|
|
149
199
|
}
|
|
150
|
-
e.open(`${
|
|
200
|
+
e.open(`${head}, ${slotsTy}`);
|
|
201
|
+
const context = slotContextOf(child);
|
|
202
|
+
const contextParams = context.map((p) => `, ${p.name}: ${p.ty}`).join("");
|
|
203
|
+
const opaque = vnode ? child.name : s.opaque;
|
|
151
204
|
for (const name of child.slotNames) {
|
|
152
205
|
const field = snake(name);
|
|
153
206
|
const value = given.get(name);
|
|
@@ -156,6 +209,7 @@ export function callChild(s, e, child, propsCode, slots, attrs) {
|
|
|
156
209
|
continue;
|
|
157
210
|
}
|
|
158
211
|
const { body, param } = slotContent(s, value);
|
|
212
|
+
const asVnodes = vnode || s.vnode || slotAsVnodes(child, name);
|
|
159
213
|
const shape = child.slotShapes.get(name);
|
|
160
214
|
const takesNone = param?.type === "Identifier" && param.name === "_";
|
|
161
215
|
const sid = child.passesSlotIds ? `fv_sid${++ctx.narrowCount}` : null;
|
|
@@ -182,7 +236,7 @@ export function callChild(s, e, child, propsCode, slots, attrs) {
|
|
|
182
236
|
if (param?.type === "ObjectPattern") {
|
|
183
237
|
for (const p of param.properties) {
|
|
184
238
|
if (p.type !== "ObjectProperty" || p.computed || p.value.type !== "Identifier") {
|
|
185
|
-
fail(s.comp, "slot props are destructured into plain names, without defaults", p);
|
|
239
|
+
fail(s.comp, "FV0904", "slot props are destructured into plain names, without defaults", p);
|
|
186
240
|
}
|
|
187
241
|
locals.set(p.value.name, fieldVal(s.comp, sp, ty, p.key.name ?? p.key.value, p));
|
|
188
242
|
}
|
|
@@ -191,20 +245,31 @@ export function callChild(s, e, child, propsCode, slots, attrs) {
|
|
|
191
245
|
locals.set(param.name, { code: sp, ty });
|
|
192
246
|
}
|
|
193
247
|
else if (!takesNone)
|
|
194
|
-
fail(s.comp, "slot props are a name or an object pattern", param);
|
|
248
|
+
fail(s.comp, "FV0905", "slot props are a name or an object pattern", param);
|
|
195
249
|
const life = shape.fields.some((f) => slotFieldBorrows(f.ty)) ? "<'_>" : "";
|
|
196
|
-
e.open(`${field}: Some(&|out: &mut String, ${sp}: &${m}::${shape.name}${life}${sidParam}| -> bool`);
|
|
250
|
+
e.open(`${field}: Some(&|out: &mut String, ${sp}: &${m}::${shape.name}${life}${sidParam}${contextParams}| -> bool`);
|
|
197
251
|
const opened = e.lines.length - 1;
|
|
198
|
-
content({ ...s, locals, sid });
|
|
252
|
+
content({ ...s, locals, sid, vnode: asVnodes, opaque });
|
|
199
253
|
unread(opened, sp);
|
|
200
254
|
unread(opened, sid);
|
|
255
|
+
for (const p of context)
|
|
256
|
+
unread(opened, p.name);
|
|
201
257
|
e.close("),");
|
|
202
258
|
continue;
|
|
203
259
|
}
|
|
204
260
|
if (!takesNone)
|
|
205
|
-
fail(s.comp, `\`<slot${name === "default" ? "" : ` name="${name}"`}>\` in ${child.name} passes no props`, param);
|
|
206
|
-
const inner = { ...s, sid };
|
|
207
|
-
if (
|
|
261
|
+
fail(s.comp, "FV0906", `\`<slot${name === "default" ? "" : ` name="${name}"`}>\` in ${child.name} passes no props`, param);
|
|
262
|
+
const inner = { ...s, sid, vnode: asVnodes, opaque };
|
|
263
|
+
if (context.length) {
|
|
264
|
+
e.open(`${field}: Some(&|out: &mut String${sidParam}${contextParams}| -> bool`);
|
|
265
|
+
const opened = e.lines.length - 1;
|
|
266
|
+
content(inner);
|
|
267
|
+
unread(opened, sid);
|
|
268
|
+
for (const p of context)
|
|
269
|
+
unread(opened, p.name);
|
|
270
|
+
e.close("),");
|
|
271
|
+
}
|
|
272
|
+
else if (sid !== null) {
|
|
208
273
|
e.open(`${field}: Some(fv::Slot::slotted(&|out: &mut String${sidParam}| -> bool`);
|
|
209
274
|
const opened = e.lines.length - 1;
|
|
210
275
|
content(inner);
|
|
@@ -227,14 +292,15 @@ export function callChild(s, e, child, propsCode, slots, attrs) {
|
|
|
227
292
|
e.close(`${trailing});`);
|
|
228
293
|
}
|
|
229
294
|
export function ownInto(comp, v, want, node) {
|
|
295
|
+
holdsNothing(comp, v, want, node, "the prop");
|
|
296
|
+
if (want.k === "opt" && nothing(v.ty))
|
|
297
|
+
return "None";
|
|
230
298
|
if (want.k === "str") {
|
|
231
299
|
if (v.ty.k !== "str")
|
|
232
|
-
fail(comp, "a string prop needs a string", node);
|
|
300
|
+
fail(comp, "FV0509", "a string prop needs a string", node);
|
|
233
301
|
return `std::borrow::Cow::Borrowed(${bare(v.code)})`;
|
|
234
302
|
}
|
|
235
303
|
if (want.k === "opt" && want.of.k === "str") {
|
|
236
|
-
if (v.ty.k === "undef")
|
|
237
|
-
return "None";
|
|
238
304
|
if (v.ty.k === "str")
|
|
239
305
|
return `Some(std::borrow::Cow::Borrowed(${bare(v.code)}))`;
|
|
240
306
|
if (v.ty.k === "opt" && v.ty.of.k === "str")
|
|
@@ -257,7 +323,7 @@ export function ownInto(comp, v, want, node) {
|
|
|
257
323
|
if (want.k === "opt" && objecty(want.of) && sameTy(v.ty, want.of))
|
|
258
324
|
return `Some(${atom(v.code)}.to_owned())`;
|
|
259
325
|
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);
|
|
326
|
+
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
327
|
}
|
|
262
328
|
if (want.k === "list" && v.ty.k === "list" && v.ty.of.k === "undef")
|
|
263
329
|
return "Vec::new()";
|
|
@@ -269,5 +335,5 @@ export function ownInto(comp, v, want, node) {
|
|
|
269
335
|
if (want.of.k === "int" || want.of.k === "float" || want.of.k === "bool")
|
|
270
336
|
return `${atom(v.code)}.to_vec()`;
|
|
271
337
|
}
|
|
272
|
-
return fail(comp, `a ${JSON.stringify(v.ty)} into a ${JSON.stringify(want)} prop`, node);
|
|
338
|
+
return fail(comp, "FV0511", `a ${JSON.stringify(v.ty)} into a ${JSON.stringify(want)} prop`, node);
|
|
273
339
|
}
|
package/dist/classes.d.ts
CHANGED
|
@@ -7,6 +7,10 @@ export type ClassItem = {
|
|
|
7
7
|
};
|
|
8
8
|
export declare function classItems(s: Scope, n: N): ClassItem[];
|
|
9
9
|
export declare function renderClass(s: Scope, e: Emitter, n: N, after?: boolean): void;
|
|
10
|
+
/** The class list of an element in content Vue renders from virtual nodes, in the order Vue's
|
|
11
|
+
* client compiler merges a static `class` written before `:class`: the static names first, at the
|
|
12
|
+
* place of the static `class`. */
|
|
13
|
+
export declare function vnodeClass(s: Scope, n: N): N;
|
|
10
14
|
export declare function classPresent(s: Scope, n: N): string;
|
|
11
15
|
export declare function maybeEqual(s: Scope, n: N): boolean;
|
|
12
16
|
export declare function literalClass(n: N): string | null;
|
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 { directiveOwner, 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")
|
|
@@ -102,6 +102,28 @@ export function renderClass(s, e, n, after = false) {
|
|
|
102
102
|
e.stmt(`fv::class_into(out, ${wrote}, &[${rest.join(", ")}]);`);
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
+
const UNWRITTEN = new Set(["on", "if", "else-if", "else", "for", "slot", "once", "memo", "cloak", "pre"]);
|
|
106
|
+
/** The class list of an element in content Vue renders from virtual nodes, in the order Vue's
|
|
107
|
+
* client compiler merges a static `class` written before `:class`: the static names first, at the
|
|
108
|
+
* place of the static `class`. */
|
|
109
|
+
export function vnodeClass(s, n) {
|
|
110
|
+
const last = n.type === "ArrayExpression" ? n.elements.at(-1) : null;
|
|
111
|
+
if (n.fvMerged || last?.type !== "StringLiteral" || n.elements.length < 2)
|
|
112
|
+
return n;
|
|
113
|
+
const el = directiveOwner(s.comp, n.elements[0]);
|
|
114
|
+
if (!el)
|
|
115
|
+
return n;
|
|
116
|
+
const own = el.props.findIndex((p) => p.type === 6 && p.name === "class");
|
|
117
|
+
const bound = el.props.findIndex((p) => p.type === 7 && p.name === "bind" && p.arg?.content === "class");
|
|
118
|
+
if (own < 0 || bound < own)
|
|
119
|
+
return n;
|
|
120
|
+
const between = el.props.slice(own + 1, bound).find((p) => p.type === 6 || !UNWRITTEN.has(p.name));
|
|
121
|
+
if (between) {
|
|
122
|
+
const at = { type: "VueTemplate", loc: { start: { line: between.loc.start.line, column: between.loc.start.column - 1 } }, __fv: "source" };
|
|
123
|
+
fail(s.comp, "FV1013", "in content Vue renders from virtual nodes, a `class` written before `:class` is written where the `class` stands, and Vue's server compiler moves it to where `:class` stands; write the two next to each other, or `:class` first", at);
|
|
124
|
+
}
|
|
125
|
+
return { ...n, elements: [last, ...n.elements.slice(0, -1)] };
|
|
126
|
+
}
|
|
105
127
|
export function classPresent(s, n) {
|
|
106
128
|
if (n.fvMerged)
|
|
107
129
|
return n.elements.reduce((acc, el) => logical(acc, "||", classPresent(s, el)), "false");
|
|
@@ -123,7 +145,7 @@ export function classPresent(s, n) {
|
|
|
123
145
|
break;
|
|
124
146
|
}
|
|
125
147
|
const v = expr(s, n);
|
|
126
|
-
if (v.ty
|
|
148
|
+
if (nothing(v.ty))
|
|
127
149
|
return "false";
|
|
128
150
|
if (v.ty.k === "opt")
|
|
129
151
|
return `${atom(v.code)}.is_some()`;
|
|
@@ -170,7 +192,7 @@ export function classAttr(s, n, side) {
|
|
|
170
192
|
return `if ${condition(cond(s, n.test))} { ${classAttr(s, n.consequent, side)} } else { ${classAttr(s, n.alternate, side)} }`;
|
|
171
193
|
}
|
|
172
194
|
const v = expr(s, n);
|
|
173
|
-
if (v.ty.k === "str" || v.ty
|
|
195
|
+
if (v.ty.k === "str" || nothing(v.ty) || (v.ty.k === "opt" && v.ty.of.k === "str"))
|
|
174
196
|
return valueAttr(s, v, n);
|
|
175
|
-
return fail(s.comp, "a class is a string, an array, or an object of conditions", n);
|
|
197
|
+
return fail(s.comp, "FV1004", "a class is a string, an array, or an object of conditions", n);
|
|
176
198
|
}
|
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 {
|
|
@@ -15,10 +15,11 @@ export interface MountOptions {
|
|
|
15
15
|
/** Where to look for islands: the whole document by default. */
|
|
16
16
|
root?: ParentNode;
|
|
17
17
|
/** Told about an island left as the server rendered it, because its component is not among
|
|
18
|
-
* those given or its props are malformed
|
|
18
|
+
* those given, did not load or its props are malformed, and about one hydrated at once because
|
|
19
|
+
* its `data-hydrate` is not one this version knows. Warns on the console by default. */
|
|
19
20
|
onError?: (element: Element, problem: string) => void;
|
|
20
21
|
}
|
|
21
|
-
/** A component, or a function that loads one
|
|
22
|
+
/** A component, or a function that loads one (`() => import("./Counter.vue")`), so that the code
|
|
22
23
|
* of an island not on the page is never fetched. A function is taken for a loader as vue-router
|
|
23
24
|
* takes one for a lazy route: unless it has `props` or `displayName`, which mark a functional
|
|
24
25
|
* component, or `__vccOpts`, a class component. `ferrovue/islands` maps the name of every island
|
|
@@ -28,13 +29,50 @@ export type IslandComponent = Component | (() => Promise<Component | {
|
|
|
28
29
|
}>);
|
|
29
30
|
/** The islands mounted. */
|
|
30
31
|
export interface Islands {
|
|
31
|
-
/** One app per island
|
|
32
|
+
/** One app per island hydrated so far: those hydrated at once in document order, then each
|
|
33
|
+
* island with a `data-hydrate` as it hydrates. */
|
|
32
34
|
apps: App[];
|
|
33
|
-
/** Unmount every island, as a page leaving would. */
|
|
35
|
+
/** Unmount every island and stop waiting to hydrate the others, as a page leaving would. */
|
|
34
36
|
unmount(): void;
|
|
35
37
|
}
|
|
36
38
|
/** Hydrate every island on the page: each `<div data-island="Name" data-props="…">` that
|
|
37
39
|
* `Html::island` wrote becomes an app of the component of that name, given the props the server
|
|
38
40
|
* rendered it with, mounted where it is. A loader is called only for an island the page holds, all
|
|
39
|
-
* of them at once, and the islands mount in document order once every one has loaded.
|
|
41
|
+
* of them at once, and the islands mount in document order once every one has loaded. An island
|
|
42
|
+
* with a `data-hydrate` (`Html::hydrate`) waits instead: once it is visible (or within a root margin
|
|
43
|
+
* of the viewport), the browser is idle,
|
|
44
|
+
* it is interacted with or a media query matches, its component is loaded and it hydrates. */
|
|
40
45
|
export declare function mountIslands(components: Record<string, IslandComponent>, options?: MountOptions): Promise<Islands>;
|
|
46
|
+
/** Content only the browser renders. The server writes the `#fallback` slot, or nothing; the
|
|
47
|
+
* browser hydrates that fallback, then swaps in the default slot once mounted. ferrovue does not
|
|
48
|
+
* compile the default slot, so anything may go there: a component library's components, code that
|
|
49
|
+
* reads `window`. The swap happens only where Vue runs on the page, as in an island. */
|
|
50
|
+
export declare const ClientOnly: Component;
|
|
51
|
+
/** One component the server rendered into a slot of a page's layout, as `ferrovue::Part` records
|
|
52
|
+
* it: its name (`c`) and the props it was rendered from (`p`). */
|
|
53
|
+
export interface PagePart {
|
|
54
|
+
c: string;
|
|
55
|
+
p: Record<string, unknown>;
|
|
56
|
+
}
|
|
57
|
+
/** What `ferrovue::PageRecord` writes: the layout's props, and each slot's parts in order. */
|
|
58
|
+
export interface PageRecord {
|
|
59
|
+
props: Record<string, unknown>;
|
|
60
|
+
slots: Record<string, PagePart[]>;
|
|
61
|
+
}
|
|
62
|
+
export interface PageOptions extends Pick<MountOptions, "pinia" | "router" | "plugins"> {
|
|
63
|
+
/** The element holding the layout's root, or a selector for it: `#app` by default. */
|
|
64
|
+
container?: Element | string;
|
|
65
|
+
/** The `id` of the record's script, as given to `PageRecord::script_into`: `__fv_page` by default. */
|
|
66
|
+
record?: string;
|
|
67
|
+
/** The document to read the page from. */
|
|
68
|
+
doc?: Document;
|
|
69
|
+
}
|
|
70
|
+
/** The app of a page: its root renders `layout` with the record's props, and each slot as the
|
|
71
|
+
* components the record names, in order, as a plain array, which hydrates the markup the server
|
|
72
|
+
* wrote with no fragment of its own. `components` must hold every component the record names. */
|
|
73
|
+
export declare function createPageApp(layout: Component, record: PageRecord, components: Record<string, Component>, options?: PageOptions): App;
|
|
74
|
+
/** Hydrate a page `ferrovue::Page` rendered: read its record, load the components it names (and
|
|
75
|
+
* no others), wait for the router, and mount the layout on the container. Rejects, leaving the page
|
|
76
|
+
* as the server rendered it, when the container or the record is missing or a component is not
|
|
77
|
+
* given or does not load. */
|
|
78
|
+
export declare function mountPage(layout: IslandComponent, components: Record<string, IslandComponent>, options?: PageOptions): Promise<App>;
|