ferrovue 0.9.0 → 0.10.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 +36 -22
- package/dist/args.js +1 -1
- package/dist/attrs.d.ts +2 -0
- package/dist/attrs.js +12 -4
- package/dist/blocks.d.ts +32 -0
- package/dist/blocks.js +476 -0
- package/dist/calls.js +12 -3
- package/dist/children.js +10 -3
- package/dist/cli.js +2 -6
- package/dist/client-render.d.ts +4 -3
- package/dist/client-render.js +17 -4
- package/dist/client.d.ts +12 -0
- package/dist/client.js +28 -24
- package/dist/compiler.d.ts +14 -4
- package/dist/compiler.js +184 -35
- package/dist/component.js +9 -3
- package/dist/conformance.d.ts +1 -1
- package/dist/constants.d.ts +26 -3
- package/dist/constants.js +176 -20
- package/dist/context.d.ts +15 -1
- package/dist/context.js +8 -2
- package/dist/diagnostics.d.ts +11 -1
- package/dist/diagnostics.js +33 -4
- package/dist/dynamic.js +7 -2
- package/dist/editor.d.ts +2 -2
- package/dist/editor.js +14 -9
- package/dist/emitter.d.ts +1 -0
- package/dist/emitter.js +12 -0
- package/dist/errors.d.ts +422 -7
- package/dist/errors.js +1399 -335
- package/dist/expr.d.ts +11 -0
- package/dist/expr.js +140 -33
- package/dist/fixture.d.ts +4 -1
- package/dist/fixture.js +10 -3
- package/dist/functions.d.ts +14 -0
- package/dist/functions.js +172 -0
- package/dist/hydration.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/inputs.d.ts +2 -2
- package/dist/inputs.js +1 -1
- package/dist/lists.d.ts +5 -1
- package/dist/lists.js +173 -29
- package/dist/loads.d.ts +27 -0
- package/dist/loads.js +81 -0
- package/dist/loops.js +10 -3
- package/dist/model.d.ts +50 -3
- package/dist/model.js +162 -6
- package/dist/narrowing.d.ts +8 -0
- package/dist/narrowing.js +28 -4
- package/dist/page-json.d.ts +1 -0
- package/dist/page-json.js +14 -0
- package/dist/plugin.d.ts +2 -1
- package/dist/plugins/client-only.js +1 -1
- package/dist/plugins/head.js +2 -1
- package/dist/plugins/index.js +2 -1
- package/dist/plugins/loaders.d.ts +35 -0
- package/dist/plugins/loaders.js +349 -0
- package/dist/plugins/provide.js +5 -4
- package/dist/plugins/router-link.js +2 -2
- package/dist/plugins/stores.js +7 -7
- package/dist/plugins/twins.js +1 -1
- package/dist/resolve.d.ts +32 -0
- package/dist/resolve.js +246 -0
- package/dist/rust.d.ts +1 -1
- package/dist/rust.js +22 -4
- package/dist/schema.d.ts +1 -1
- package/dist/script.d.ts +1 -1
- package/dist/script.js +64 -54
- package/dist/settle.js +8 -0
- package/dist/template.js +162 -137
- package/dist/typescript.d.ts +0 -1
- package/dist/typescript.js +14 -22
- package/dist/vite.js +3 -3
- package/dist/volar.js +1 -1
- package/package.json +1 -1
package/dist/blocks.js
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import { fail, GenError, nothing } from "./model.js";
|
|
2
|
+
import { ctx } from "./context.js";
|
|
3
|
+
import { expr, typedBy } from "./expr.js";
|
|
4
|
+
import { collected, heldList } from "./lists.js";
|
|
5
|
+
import { atom, bare, occurrences, operand, UNARY } from "./parens.js";
|
|
6
|
+
import { presence } from "./narrowing.js";
|
|
7
|
+
import { asCow, heldStr, isTemporary } from "./strings.js";
|
|
8
|
+
import { intFromF64 } from "./numbers.js";
|
|
9
|
+
const COW = "std::borrow::Cow::<str>";
|
|
10
|
+
/** Runtime calls that panic where JavaScript throws, or user code that may. */
|
|
11
|
+
export const PANICS = /\bfv::js_(?:repeat|pad_start|pad_end|date_parse|date_utc|to_iso_string)\(|\bfv_i18n\.t\(/;
|
|
12
|
+
export function panicky(code) {
|
|
13
|
+
return PANICS.test(code) || Object.values(ctx.helpers).some((h) => code.includes(`${h.rust}(`));
|
|
14
|
+
}
|
|
15
|
+
/** How a value is held in a Rust `let`: what the `let` is given, how the name reads back, and
|
|
16
|
+
* whether the `let` owns what a read borrows (a `String`, a `Cow`, a `Vec`). */
|
|
17
|
+
export function bindingOf(v) {
|
|
18
|
+
const lone = v.lone ? { lone: true } : {};
|
|
19
|
+
if (v.slice)
|
|
20
|
+
return { init: bare(v.code), read: (at) => ({ code: at, ty: v.ty, slice: true, ...lone }), owned: false };
|
|
21
|
+
if (v.ty.k === "list" && v.iter !== undefined)
|
|
22
|
+
return { init: collected(v), read: (at) => heldList(at, v.ty.k === "list" ? v.ty.of : v.ty, v.lone), owned: true };
|
|
23
|
+
if (v.held !== undefined)
|
|
24
|
+
return { init: v.held, read: (at) => ({ code: `${at}.as_deref()`, ty: v.ty, ...lone }), owned: true };
|
|
25
|
+
if (v.ty.k === "int" && v.f64 !== undefined && (v.code.endsWith(" as i64") || Object.is(v.num, -0))) {
|
|
26
|
+
return {
|
|
27
|
+
init: bare(v.f64),
|
|
28
|
+
read: (at) => ({ ...intFromF64(at), ...lone, ...(v.num !== undefined ? { num: v.num } : {}), ...(v.konst !== undefined ? { konst: v.konst } : {}) }),
|
|
29
|
+
owned: false,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (isTemporary(v))
|
|
33
|
+
return { init: heldStr(v), read: (at) => ({ code: `&*${at}`, ty: v.ty, ...lone }), owned: true };
|
|
34
|
+
const place = v.ty.k === "list" || v.ty.k === "record" || v.ty.k === "struct" || v.ty.k === "child";
|
|
35
|
+
return {
|
|
36
|
+
init: place ? `&${operand(v.code, UNARY)}` : bare(v.code),
|
|
37
|
+
read: (at) => ({ code: at, ty: v.ty, ...lone, ...(v.konst !== undefined ? { konst: v.konst } : {}) }),
|
|
38
|
+
owned: false,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const SKIP = new Set(["loc", "start", "end", "extra", "range", "typeAnnotation", "typeParameters", "returnType", "leadingComments", "trailingComments", "innerComments", "__fv", "label"]);
|
|
42
|
+
let renames = 0;
|
|
43
|
+
function spot(from) {
|
|
44
|
+
return { loc: from?.loc, start: from?.start, end: from?.end, __fv: from?.__fv };
|
|
45
|
+
}
|
|
46
|
+
function declared(st) {
|
|
47
|
+
if (st?.type === "VariableDeclaration")
|
|
48
|
+
return st.declarations.flatMap((d) => patternNames(d.id));
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
function patternNames(p) {
|
|
52
|
+
switch (p?.type) {
|
|
53
|
+
case "Identifier":
|
|
54
|
+
return [p.name];
|
|
55
|
+
case "ObjectPattern":
|
|
56
|
+
return p.properties.flatMap((q) => patternNames(q.type === "RestElement" ? q.argument : q.value));
|
|
57
|
+
case "ArrayPattern":
|
|
58
|
+
return p.elements.flatMap((q) => (q ? patternNames(q) : []));
|
|
59
|
+
case "AssignmentPattern":
|
|
60
|
+
return patternNames(p.left);
|
|
61
|
+
case "RestElement":
|
|
62
|
+
return patternNames(p.argument);
|
|
63
|
+
default:
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
class Renamer {
|
|
68
|
+
comp;
|
|
69
|
+
constructor(comp) {
|
|
70
|
+
this.comp = comp;
|
|
71
|
+
}
|
|
72
|
+
statements(sts, outer) {
|
|
73
|
+
const env = new Map(outer);
|
|
74
|
+
for (const st of sts)
|
|
75
|
+
for (const name of declared(st))
|
|
76
|
+
env.set(name, null);
|
|
77
|
+
return sts.map((st) => {
|
|
78
|
+
if (st.type !== "VariableDeclaration")
|
|
79
|
+
return this.node(st, env);
|
|
80
|
+
const declarations = st.declarations.map((d) => {
|
|
81
|
+
const init = this.node(d.init, env);
|
|
82
|
+
return { ...d, init, id: this.pattern(d.id, env) };
|
|
83
|
+
});
|
|
84
|
+
return { ...st, declarations };
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
pattern(p, env) {
|
|
88
|
+
switch (p?.type) {
|
|
89
|
+
case "Identifier": {
|
|
90
|
+
const to = `${p.name}#${++renames}`;
|
|
91
|
+
env.set(p.name, to);
|
|
92
|
+
return { ...p, name: to, fvName: p.name };
|
|
93
|
+
}
|
|
94
|
+
case "ObjectPattern":
|
|
95
|
+
return {
|
|
96
|
+
...p,
|
|
97
|
+
properties: p.properties.map((q) => q.type === "RestElement" ? { ...q, argument: this.pattern(q.argument, env) } : { ...q, key: q.computed ? this.node(q.key, env) : q.key, value: this.pattern(q.value, env), shorthand: false }),
|
|
98
|
+
};
|
|
99
|
+
case "ArrayPattern":
|
|
100
|
+
return { ...p, elements: p.elements.map((q) => (q ? this.pattern(q, env) : q)) };
|
|
101
|
+
case "AssignmentPattern":
|
|
102
|
+
return { ...p, right: this.node(p.right, env), left: this.pattern(p.left, env) };
|
|
103
|
+
case "RestElement":
|
|
104
|
+
return { ...p, argument: this.pattern(p.argument, env) };
|
|
105
|
+
default:
|
|
106
|
+
return p;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
node(n, env) {
|
|
110
|
+
if (!n || typeof n !== "object")
|
|
111
|
+
return n;
|
|
112
|
+
if (Array.isArray(n))
|
|
113
|
+
return n.map((x) => this.node(x, env));
|
|
114
|
+
switch (n.type) {
|
|
115
|
+
case "Identifier": {
|
|
116
|
+
const to = env.get(n.name);
|
|
117
|
+
if (to === null)
|
|
118
|
+
fail(this.comp, "FV1901", `\`${n.name}\` is read before its declaration, where JavaScript throws a \`ReferenceError\``, n);
|
|
119
|
+
return to === undefined ? n : { ...n, name: to, fvName: n.name };
|
|
120
|
+
}
|
|
121
|
+
case "MemberExpression":
|
|
122
|
+
case "OptionalMemberExpression":
|
|
123
|
+
return { ...n, object: this.node(n.object, env), property: n.computed ? this.node(n.property, env) : n.property };
|
|
124
|
+
case "ObjectProperty":
|
|
125
|
+
return { ...n, key: n.computed ? this.node(n.key, env) : n.key, value: this.node(n.value, env), shorthand: false };
|
|
126
|
+
case "ArrowFunctionExpression":
|
|
127
|
+
case "FunctionExpression":
|
|
128
|
+
case "FunctionDeclaration":
|
|
129
|
+
case "ObjectMethod": {
|
|
130
|
+
const inner = new Map(env);
|
|
131
|
+
for (const p of n.params)
|
|
132
|
+
for (const name of patternNames(p))
|
|
133
|
+
inner.delete(name);
|
|
134
|
+
if (n.type === "FunctionExpression" && n.id)
|
|
135
|
+
inner.delete(n.id.name);
|
|
136
|
+
const body = n.body.type === "BlockStatement" ? { ...n.body, body: this.statements(n.body.body, inner) } : this.node(n.body, inner);
|
|
137
|
+
return { ...n, body };
|
|
138
|
+
}
|
|
139
|
+
case "BlockStatement":
|
|
140
|
+
return { ...n, body: this.statements(n.body, env) };
|
|
141
|
+
case "SwitchStatement":
|
|
142
|
+
return { ...n, discriminant: this.node(n.discriminant, env), cases: n.cases.map((c) => ({ ...c, test: this.node(c.test, env), consequent: this.statements(c.consequent, env) })) };
|
|
143
|
+
}
|
|
144
|
+
const copy = { ...n };
|
|
145
|
+
for (const key of Object.keys(n)) {
|
|
146
|
+
if (SKIP.has(key))
|
|
147
|
+
continue;
|
|
148
|
+
const v = n[key];
|
|
149
|
+
if (v && typeof v === "object")
|
|
150
|
+
copy[key] = this.node(v, env);
|
|
151
|
+
}
|
|
152
|
+
return copy;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const LOOPS = {
|
|
156
|
+
ForStatement: "a `for` loop",
|
|
157
|
+
ForOfStatement: "a `for…of` loop",
|
|
158
|
+
ForInStatement: "a `for…in` loop",
|
|
159
|
+
WhileStatement: "a `while` loop",
|
|
160
|
+
DoWhileStatement: "a `do…while` loop",
|
|
161
|
+
};
|
|
162
|
+
const OTHERS = {
|
|
163
|
+
TryStatement: "`try`, which only catches what the body throws",
|
|
164
|
+
ThrowStatement: "`throw`, which ends the render with an error",
|
|
165
|
+
LabeledStatement: "a labelled statement",
|
|
166
|
+
ContinueStatement: "`continue`",
|
|
167
|
+
FunctionDeclaration: "a function declared inside it",
|
|
168
|
+
ClassDeclaration: "a class declared inside it",
|
|
169
|
+
DebuggerStatement: "`debugger`",
|
|
170
|
+
WithStatement: "`with`",
|
|
171
|
+
};
|
|
172
|
+
const SUBSET = "it holds `const` (and `let` never assigned again), `if` / `else`, `switch` and `return`";
|
|
173
|
+
function assignments(comp, body) {
|
|
174
|
+
const lets = new Set();
|
|
175
|
+
const visit = (n) => {
|
|
176
|
+
if (!n || typeof n !== "object")
|
|
177
|
+
return;
|
|
178
|
+
if (Array.isArray(n)) {
|
|
179
|
+
n.forEach(visit);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (n.type === "VariableDeclaration" && n.kind === "let")
|
|
183
|
+
for (const d of n.declarations)
|
|
184
|
+
for (const name of patternNames(d.id))
|
|
185
|
+
lets.add(name);
|
|
186
|
+
if (n.type === "AssignmentExpression" || n.type === "UpdateExpression") {
|
|
187
|
+
const target = n.type === "AssignmentExpression" ? n.left : n.argument;
|
|
188
|
+
const shown = `\`${n.operator}\``;
|
|
189
|
+
if (target.type === "Identifier" && lets.has(target.name)) {
|
|
190
|
+
fail(comp, "FV1902", `\`let ${target.name}\` is assigned again with ${shown}, which a function's body does not translate: give each value a \`const\` of its own`, n);
|
|
191
|
+
}
|
|
192
|
+
fail(comp, "FV1902", `an assignment (${shown}) changes a value, which a function's body does not translate: give each value a \`const\` of its own`, n);
|
|
193
|
+
}
|
|
194
|
+
for (const key of Object.keys(n))
|
|
195
|
+
if (!SKIP.has(key) && n[key] && typeof n[key] === "object")
|
|
196
|
+
visit(n[key]);
|
|
197
|
+
};
|
|
198
|
+
visit(body);
|
|
199
|
+
}
|
|
200
|
+
/** How many times a body's statements may be translated again after an `if` or a `case` that
|
|
201
|
+
* falls off its end: each such path continues with the statements after it, translated on its own,
|
|
202
|
+
* so sequential `switch`es of `break`ing cases multiply them. */
|
|
203
|
+
export const PATH_BUDGET = 4096;
|
|
204
|
+
class Desugar {
|
|
205
|
+
comp;
|
|
206
|
+
paths = 0;
|
|
207
|
+
constructor(comp) {
|
|
208
|
+
this.comp = comp;
|
|
209
|
+
}
|
|
210
|
+
run(sts, k, brk, from) {
|
|
211
|
+
for (let i = 0; i < sts.length; i++) {
|
|
212
|
+
const st = sts[i];
|
|
213
|
+
const rest = sts.slice(i + 1);
|
|
214
|
+
const after = rest.length ? { sts: rest, next: k, brk } : k;
|
|
215
|
+
switch (st.type) {
|
|
216
|
+
case "EmptyStatement":
|
|
217
|
+
continue;
|
|
218
|
+
case "ReturnStatement":
|
|
219
|
+
if (!st.argument)
|
|
220
|
+
fail(this.comp, "FV1903", "`return` without a value returns `undefined`: return the value the function gives on this path", st);
|
|
221
|
+
this.dead(rest, "`return`");
|
|
222
|
+
return st.argument;
|
|
223
|
+
case "BreakStatement":
|
|
224
|
+
if (st.label || brk === undefined)
|
|
225
|
+
fail(this.comp, "FV1901", "`break` outside a `switch`'s case, which only a loop could take", st);
|
|
226
|
+
this.dead(rest, "`break`");
|
|
227
|
+
return this.go(brk, st);
|
|
228
|
+
case "BlockStatement":
|
|
229
|
+
return this.run(st.body, after, brk, st);
|
|
230
|
+
case "IfStatement": {
|
|
231
|
+
const yes = this.run(this.branch(st.consequent), after, brk, st);
|
|
232
|
+
const no = st.alternate ? this.run(this.branch(st.alternate), after, brk, st) : this.go(after, st);
|
|
233
|
+
return { type: "ConditionalExpression", test: st.test, consequent: yes, alternate: no, fvIf: true, ...spot(st) };
|
|
234
|
+
}
|
|
235
|
+
case "SwitchStatement":
|
|
236
|
+
return this.switched(st, after);
|
|
237
|
+
case "VariableDeclaration": {
|
|
238
|
+
if (st.kind !== "const" && st.kind !== "let")
|
|
239
|
+
fail(this.comp, "FV1901", `a function's body may not hold \`${st.kind}\`: declare the value with \`const\``, st);
|
|
240
|
+
const body = this.run(rest, k, brk, st);
|
|
241
|
+
return st.declarations.reduceRight((inner, d) => this.bind(d, inner), body);
|
|
242
|
+
}
|
|
243
|
+
case "ExpressionStatement":
|
|
244
|
+
return fail(this.comp, "FV1901", `a function's body may not hold an expression statement (\`${st.expression.type}\`), whose value is dropped and which could only change something: ${SUBSET}`, st);
|
|
245
|
+
default: {
|
|
246
|
+
const what = LOOPS[st.type] ?? OTHERS[st.type] ?? `\`${st.type}\``;
|
|
247
|
+
const why = LOOPS[st.type] ? `: ${SUBSET}; compute a list with \`.map()\`, \`.filter()\` or \`.reduce()\`` : `: ${SUBSET}`;
|
|
248
|
+
return fail(this.comp, "FV1901", `a function's body may not hold ${what}${why}`, st);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return this.go(k, from);
|
|
253
|
+
}
|
|
254
|
+
branch(st) {
|
|
255
|
+
return st.type === "BlockStatement" ? st.body : [st];
|
|
256
|
+
}
|
|
257
|
+
dead(rest, after) {
|
|
258
|
+
const first = rest.find((r) => r.type !== "EmptyStatement");
|
|
259
|
+
if (first)
|
|
260
|
+
fail(this.comp, "FV1904", `code after ${after}, which never runs: remove it`, first);
|
|
261
|
+
}
|
|
262
|
+
go(k, from) {
|
|
263
|
+
if (k === null) {
|
|
264
|
+
return fail(this.comp, "FV1903", "the function can reach its end here without a `return`, and JavaScript then returns `undefined`: return a value on every path", from);
|
|
265
|
+
}
|
|
266
|
+
if (k.falls)
|
|
267
|
+
fail(this.comp, "FV1904", "this case falls through into the next one, which runs its statements too: end it with `return` or `break`", k.falls);
|
|
268
|
+
if (k.sts.length && ++this.paths > PATH_BUDGET) {
|
|
269
|
+
fail(this.comp, "FV1905", `the body's \`if\`s and \`switch\`es continue with the statements after them on more than ${PATH_BUDGET} paths, each translated on its own: end the cases in \`return\`, or split the body into \`computed\`s`, from);
|
|
270
|
+
}
|
|
271
|
+
return this.run(k.sts, k.next, k.brk, from);
|
|
272
|
+
}
|
|
273
|
+
bind(d, body) {
|
|
274
|
+
if (!d.init)
|
|
275
|
+
fail(this.comp, "FV1901", `\`${d.id.fvName ?? d.id.name ?? "a pattern"}\` is declared without a value: give it one where it is declared`, d);
|
|
276
|
+
const typed = d.id.typeAnnotation?.typeAnnotation;
|
|
277
|
+
const init = typed ? typedBy(d.init, typed) : d.init;
|
|
278
|
+
return this.pattern(d.id, init, body, d);
|
|
279
|
+
}
|
|
280
|
+
pattern(p, value, body, d) {
|
|
281
|
+
switch (p.type) {
|
|
282
|
+
case "Identifier":
|
|
283
|
+
return { type: "FvLet", name: p.name, js: p.fvName ?? p.name, init: value, body, ...spot(d) };
|
|
284
|
+
case "ObjectPattern":
|
|
285
|
+
case "ArrayPattern": {
|
|
286
|
+
const tmp = `fv#${++renames}`;
|
|
287
|
+
const ref = () => ({ type: "Identifier", name: tmp, ...spot(value) });
|
|
288
|
+
const parts = [];
|
|
289
|
+
if (p.type === "ObjectPattern") {
|
|
290
|
+
for (const q of p.properties) {
|
|
291
|
+
if (q.type === "RestElement" || q.computed)
|
|
292
|
+
fail(this.comp, "FV1901", "a destructuring pattern names each field it reads, without `...rest` or a computed key", q);
|
|
293
|
+
const key = q.key.type === "Identifier" ? q.key.name : String(q.key.value);
|
|
294
|
+
parts.push([q.value, { type: "MemberExpression", computed: false, object: ref(), property: { type: "Identifier", name: key, ...spot(q.key) }, ...spot(q) }]);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
p.elements.forEach((q, i) => {
|
|
299
|
+
if (!q)
|
|
300
|
+
return;
|
|
301
|
+
if (q.type === "RestElement")
|
|
302
|
+
fail(this.comp, "FV1901", "a destructuring pattern names each item it reads, without `...rest`", q);
|
|
303
|
+
parts.push([q, { type: "MemberExpression", computed: true, object: ref(), property: { type: "NumericLiteral", value: i, ...spot(q) }, ...spot(q) }]);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
for (const [q] of parts)
|
|
307
|
+
if (q.type === "AssignmentPattern")
|
|
308
|
+
fail(this.comp, "FV1901", "a default in a destructuring pattern, which applies to `undefined` alone: write `?? fallback` where it is read", q);
|
|
309
|
+
const inner = parts.reduceRight((acc, [q, v]) => this.pattern(q, v, acc, d), body);
|
|
310
|
+
return { type: "FvLet", name: tmp, js: "the destructured value", init: value, body: inner, ...spot(d) };
|
|
311
|
+
}
|
|
312
|
+
default:
|
|
313
|
+
return fail(this.comp, "FV1901", "a declaration binds a name, or an object or array pattern of names", p);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
switched(st, after) {
|
|
317
|
+
const d = `switch#${++renames}`;
|
|
318
|
+
const subject = () => ({ type: "Identifier", name: d, ...spot(st.discriminant) });
|
|
319
|
+
const groups = [];
|
|
320
|
+
let open = null;
|
|
321
|
+
for (const c of st.cases) {
|
|
322
|
+
if (!open) {
|
|
323
|
+
open = { tests: [], body: [], dflt: false, at: c };
|
|
324
|
+
groups.push(open);
|
|
325
|
+
}
|
|
326
|
+
if (c.test)
|
|
327
|
+
open.tests.push(c.test);
|
|
328
|
+
else
|
|
329
|
+
open.dflt = true;
|
|
330
|
+
const lexical = c.consequent.find((x) => x.type === "VariableDeclaration" || x.type === "FunctionDeclaration" || x.type === "ClassDeclaration");
|
|
331
|
+
if (lexical)
|
|
332
|
+
fail(this.comp, "FV1904", "a declaration directly in a `case`, which the whole `switch` shares: wrap the case's statements in `{ … }`", lexical);
|
|
333
|
+
if (c.consequent.some((x) => x.type !== "EmptyStatement")) {
|
|
334
|
+
open.body = c.consequent;
|
|
335
|
+
open = null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const brk = after;
|
|
339
|
+
const bodyOf = (g, i) => {
|
|
340
|
+
const last = i === groups.length - 1;
|
|
341
|
+
const k = last ? after : { sts: [], next: null, brk: undefined, falls: g.body.at(-1) ?? g.at };
|
|
342
|
+
if (!g.body.length)
|
|
343
|
+
return last ? this.go(after, g.at) : this.go(k, g.at);
|
|
344
|
+
return this.run(g.body, k, brk, g.body.at(-1));
|
|
345
|
+
};
|
|
346
|
+
const fallback = groups.findIndex((g) => g.dflt);
|
|
347
|
+
let chain = fallback >= 0 ? bodyOf(groups[fallback], fallback) : this.go(after, st);
|
|
348
|
+
for (let i = groups.length - 1; i >= 0; i--) {
|
|
349
|
+
const g = groups[i];
|
|
350
|
+
if (i === fallback)
|
|
351
|
+
continue;
|
|
352
|
+
const test = g.tests
|
|
353
|
+
.map((t) => ({ type: "BinaryExpression", operator: "===", left: subject(), right: t, ...spot(t) }))
|
|
354
|
+
.reduce((a, b) => ({ type: "LogicalExpression", operator: "||", left: a, right: b, ...spot(b) }));
|
|
355
|
+
chain = { type: "ConditionalExpression", test, consequent: bodyOf(g, i), alternate: chain, ...spot(g.at) };
|
|
356
|
+
}
|
|
357
|
+
return { type: "FvLet", name: d, js: "the `switch`'s value", init: st.discriminant, body: chain, ...spot(st) };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const desugared = new WeakMap();
|
|
361
|
+
/** The expression a function's body computes: the expression of an arrow function, or a block of
|
|
362
|
+
* statements in the subset ferrovue translates, written as one expression. */
|
|
363
|
+
export function bodyExpr(comp, fn) {
|
|
364
|
+
if (fn.body.type !== "BlockStatement")
|
|
365
|
+
return fn.body;
|
|
366
|
+
const done = desugared.get(fn);
|
|
367
|
+
if (done)
|
|
368
|
+
return done;
|
|
369
|
+
assignments(comp, fn.body.body);
|
|
370
|
+
const shadowed = new Map();
|
|
371
|
+
const renamed = new Renamer(comp).statements(fn.body.body, shadowed);
|
|
372
|
+
const out = new Desugar(comp).run(renamed, null, undefined, fn.body.body.at(-1) ?? fn);
|
|
373
|
+
desugared.set(fn, out);
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
376
|
+
/** `p` (a name, or an object or array pattern of names) bound to `value` around `body`. */
|
|
377
|
+
export function destructured(comp, p, value, body) {
|
|
378
|
+
return new Desugar(comp).pattern(p, value, body, p);
|
|
379
|
+
}
|
|
380
|
+
/** `fn` with an expression for its body, where it is an arrow or function expression with a block. */
|
|
381
|
+
export function expressionBodied(comp, fn) {
|
|
382
|
+
if ((fn?.type !== "ArrowFunctionExpression" && fn?.type !== "FunctionExpression") || fn.body.type !== "BlockStatement")
|
|
383
|
+
return fn;
|
|
384
|
+
return { ...fn, body: bodyExpr(comp, fn) };
|
|
385
|
+
}
|
|
386
|
+
function simple(v) {
|
|
387
|
+
if (v.iter !== undefined || v.held !== undefined || v.format !== undefined)
|
|
388
|
+
return false;
|
|
389
|
+
if (nothing(v.ty))
|
|
390
|
+
return true;
|
|
391
|
+
return /^(?:&\*?|\*)?[A-Za-z_]\w*(?:\.\w+)*$/.test(v.code) || /^"(?:[^"\\]|\\.)*"$/.test(v.code) || /^-?\d[\w.]*$/.test(v.code);
|
|
392
|
+
}
|
|
393
|
+
function reps(v) {
|
|
394
|
+
return [v.code, v.iter, v.held, v.f64, ...(v.format?.args ?? [])].filter((r) => r !== undefined);
|
|
395
|
+
}
|
|
396
|
+
function ownedCow(v) {
|
|
397
|
+
if (!isTemporary(v))
|
|
398
|
+
return `${COW}::Owned(${atom(v.code).replace(/^&\*?/, "")}.to_owned())`;
|
|
399
|
+
const cow = asCow(v);
|
|
400
|
+
return cow.startsWith(`${COW}::Owned(`) ? cow : `${COW}::Owned(${atom(cow)}.into_owned())`;
|
|
401
|
+
}
|
|
402
|
+
/** `const name = init` around `body`: the value `body` computes with `name` bound once, as a Rust
|
|
403
|
+
* `let` in a block that hands out nothing borrowed from it. */
|
|
404
|
+
export function letIn(s, n) {
|
|
405
|
+
return bound(s, n.name, expr(s, n.init), (inner) => expr(inner, n.body));
|
|
406
|
+
}
|
|
407
|
+
/** `v` bound once to `name` for `body`, which is given the scope that reads it. */
|
|
408
|
+
export function bound(s, local, v, then) {
|
|
409
|
+
if (simple(v))
|
|
410
|
+
return then({ ...s, locals: new Map(s.locals).set(local, v) });
|
|
411
|
+
const name = `fv_c${++ctx.narrowCount}`;
|
|
412
|
+
const { init, read, owned } = bindingOf(v);
|
|
413
|
+
const held = read(name);
|
|
414
|
+
const body = then({ ...s, locals: new Map(s.locals).set(local, held) });
|
|
415
|
+
if (reps(body).join("\0") === reps(held).join("\0"))
|
|
416
|
+
return v;
|
|
417
|
+
const uses = reps(body).some((r) => occurrences(r, name) > 0);
|
|
418
|
+
if (!uses && !panicky(init))
|
|
419
|
+
return body;
|
|
420
|
+
const lead = `let ${uses ? name : "_"} = ${init};`;
|
|
421
|
+
const block = (code) => `{ ${lead} ${bare(code)} }`;
|
|
422
|
+
const lone = body.lone ? { lone: true } : {};
|
|
423
|
+
switch (body.ty.k) {
|
|
424
|
+
case "int":
|
|
425
|
+
case "float":
|
|
426
|
+
case "bool":
|
|
427
|
+
case "undef":
|
|
428
|
+
case "null":
|
|
429
|
+
return { code: block(body.code), ty: body.ty, ...lone, ...(body.f64 !== undefined ? { f64: block(body.f64) } : {}) };
|
|
430
|
+
case "str":
|
|
431
|
+
if (!owned && !isTemporary(body))
|
|
432
|
+
return { code: block(body.code), ty: body.ty, ...lone };
|
|
433
|
+
return { code: `&*${block(owned ? ownedCow(body) : asCow(body))}`, ty: body.ty, ...lone };
|
|
434
|
+
case "list": {
|
|
435
|
+
if (!owned && body.iter === undefined)
|
|
436
|
+
return { code: block(body.code), ty: body.ty, ...(body.slice ? { slice: true } : {}), ...lone };
|
|
437
|
+
const all = block(collected(body));
|
|
438
|
+
return { code: all, ty: body.ty, iter: `${all}.into_iter()`, ...lone };
|
|
439
|
+
}
|
|
440
|
+
case "opt":
|
|
441
|
+
if (body.ty.of.k === "str" && (owned || body.held !== undefined)) {
|
|
442
|
+
const kept = block(owned ? `${atom(body.code)}.map(|v| ${COW}::Owned(v.to_owned()))` : body.held);
|
|
443
|
+
return { code: `${kept}.as_deref()`, ty: body.ty, held: kept, ...lone };
|
|
444
|
+
}
|
|
445
|
+
return { code: block(body.code), ty: body.ty, ...lone };
|
|
446
|
+
default: {
|
|
447
|
+
const ref = /^\w+(?:\.[\w#]+)+$/.test(body.code) ? `&${body.code}` : body.code;
|
|
448
|
+
return { code: `(${block(ref)})`, ty: body.ty, ...lone };
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/** Whether a desugared `if` whose test is `a && b` or `a || b` should be split, so that a side
|
|
453
|
+
* that narrows a value narrows what follows it, as TypeScript narrows it. */
|
|
454
|
+
export function splits(s, test) {
|
|
455
|
+
if (test.type !== "LogicalExpression" || (test.operator !== "&&" && test.operator !== "||"))
|
|
456
|
+
return false;
|
|
457
|
+
const narrows = (side) => {
|
|
458
|
+
if (side.type === "LogicalExpression" && side.operator === test.operator)
|
|
459
|
+
return splits(s, side);
|
|
460
|
+
try {
|
|
461
|
+
return presence(s, side) !== null;
|
|
462
|
+
}
|
|
463
|
+
catch (e) {
|
|
464
|
+
if (e instanceof GenError)
|
|
465
|
+
return false;
|
|
466
|
+
throw e;
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
return narrows(test.left) || narrows(test.right);
|
|
470
|
+
}
|
|
471
|
+
/** `a && b ? x : y` as `a ? (b ? x : y) : y`, and `a || b ? x : y` as `a ? x : (b ? x : y)`. */
|
|
472
|
+
export function split(n) {
|
|
473
|
+
const { left, right, operator } = n.test;
|
|
474
|
+
const inner = { ...n, test: right };
|
|
475
|
+
return operator === "&&" ? { ...n, test: left, consequent: inner, alternate: n.alternate } : { ...n, test: left, consequent: n.consequent, alternate: inner };
|
|
476
|
+
}
|
package/dist/calls.js
CHANGED
|
@@ -6,7 +6,8 @@ import { computedListMethod, isBoolean, items, listMethod, objectCall, presentLi
|
|
|
6
6
|
import { formatted, lonely, loneOf, meet, stringMethod, stringOf, stringsEqual } from "./strings.js";
|
|
7
7
|
import { asF64, intFromF64, isNumber, numberVal, power } from "./numbers.js";
|
|
8
8
|
import { boolOf, truthy } from "./narrowing.js";
|
|
9
|
-
import { coerce, describeTy, expr } from "./expr.js";
|
|
9
|
+
import { coerce, describeTy, expr, refuseClientOnly, refuseUnavailable } from "./expr.js";
|
|
10
|
+
import { callFunction } from "./functions.js";
|
|
10
11
|
const DIGITS = new Map([
|
|
11
12
|
["toFixed", ["FV0705", 0, "number of digits"]],
|
|
12
13
|
["toPrecision", ["FV0716", 1, "precision"]],
|
|
@@ -46,6 +47,9 @@ export function call(s, n) {
|
|
|
46
47
|
const helper = s.helpers.get(callee.name);
|
|
47
48
|
if (helper)
|
|
48
49
|
return helperCall(s, helper, args, n);
|
|
50
|
+
const fn = s.locals.has(callee.name) ? undefined : s.functions.get(callee.name);
|
|
51
|
+
if (fn)
|
|
52
|
+
return callFunction(s, callee.name, fn, args, n);
|
|
49
53
|
if (isBoolean(s, callee) && args.length === 1)
|
|
50
54
|
return boolOf(truthy(expr(s, args[0])));
|
|
51
55
|
const global = !s.locals.has(callee.name) && !s.setup.has(callee.name);
|
|
@@ -97,7 +101,7 @@ export function call(s, n) {
|
|
|
97
101
|
return numberTest(s, callee.name, args, n, true);
|
|
98
102
|
if (global && BUILTINS.has(callee.name))
|
|
99
103
|
return builtin(s, `${callee.name}()`, n);
|
|
100
|
-
return
|
|
104
|
+
return refuseUnavailable(s, callee.name, n, true);
|
|
101
105
|
}
|
|
102
106
|
if (callee.type === "MemberExpression" && !callee.computed) {
|
|
103
107
|
const method = callee.property.name;
|
|
@@ -129,8 +133,13 @@ export function call(s, n) {
|
|
|
129
133
|
const helper = s.helpers.get(method);
|
|
130
134
|
if (helper)
|
|
131
135
|
return helperCall(s, helper, args, n);
|
|
136
|
+
const fn = s.functions.get(method);
|
|
137
|
+
if (fn)
|
|
138
|
+
return callFunction(s, method, fn, args, n);
|
|
132
139
|
if (s.clientOnly.has(method))
|
|
133
|
-
return
|
|
140
|
+
return refuseClientOnly(s, method, n, true);
|
|
141
|
+
if (!s.setup.has(method) && !(callee.object.name === "_ctx" && comp.props.fields.some((f) => f.js === method)))
|
|
142
|
+
return refuseUnavailable(s, method, n, true);
|
|
134
143
|
}
|
|
135
144
|
if (callee.object.type === "Identifier" && callee.object.name === "Math") {
|
|
136
145
|
if ((method === "max" || method === "min") && args.some((a) => a?.type === "SpreadElement"))
|
package/dist/children.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { isSSRSafeAttrName, propsToAttrMap } from "@vue/shared";
|
|
2
|
-
import { camelize, declares, fail, nothing, rustStr, sameTy, snake, takesAttrs } from "./model.js";
|
|
2
|
+
import { absence, 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, holdsNothing } from "./expr.js";
|
|
5
|
+
import { describeTy, 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";
|
|
@@ -337,6 +337,13 @@ export function ownInto(comp, v, want, node) {
|
|
|
337
337
|
return asF64(v);
|
|
338
338
|
if (want.k === "opt" && want.of.k === "float" && v.ty.k === "int")
|
|
339
339
|
return `Some(${asF64(v)})`;
|
|
340
|
+
if (want.k === "opt" && v.ty.k === "opt" && absence(v.ty) === absence(want)) {
|
|
341
|
+
const of = want.of.k;
|
|
342
|
+
if (v.ty.of.k === of && (of === "int" || of === "float" || (of === "bool" && absence(want) === "null")))
|
|
343
|
+
return v.code;
|
|
344
|
+
if (of === "float" && v.ty.of.k === "int")
|
|
345
|
+
return `${atom(v.code)}.map(|n| n as f64)`;
|
|
346
|
+
}
|
|
340
347
|
if (v.iter !== undefined && v.ty.k === "list" && want.k === "list" && sameTy(v.ty.of, want.of)) {
|
|
341
348
|
return want.of.k === "struct" || want.of.k === "child" ? `${v.iter}.cloned().collect()` : `${v.iter}.collect()`;
|
|
342
349
|
}
|
|
@@ -358,5 +365,5 @@ export function ownInto(comp, v, want, node) {
|
|
|
358
365
|
if (want.of.k === "int" || want.of.k === "float" || want.of.k === "bool")
|
|
359
366
|
return `${atom(v.code)}.to_vec()`;
|
|
360
367
|
}
|
|
361
|
-
return fail(comp, "FV0511", `
|
|
368
|
+
return fail(comp, "FV0511", `the prop is ${describeTy(want)}, and the value given is ${describeTy(v.ty)}`, node);
|
|
362
369
|
}
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { relativePath } from "./paths.js";
|
|
|
5
5
|
import { CONFIG_FILE, generate, isGenerated, loadConfig, VERSION, write } from "./compiler.js";
|
|
6
6
|
import { helpText, parseArgs } from "./args.js";
|
|
7
7
|
import { unifiedDiff } from "./diff.js";
|
|
8
|
-
import { diagnose, formatRefusal, formatWarning, isRefusal } from "./diagnostics.js";
|
|
8
|
+
import { diagnose, diagnoseAll, formatRefusal, formatWarning, isRefusal } from "./diagnostics.js";
|
|
9
9
|
import { affects, inputsOf } from "./inputs.js";
|
|
10
10
|
const root = process.cwd();
|
|
11
11
|
const SCHEMA_URL = `https://cdn.jsdelivr.net/npm/ferrovue@${VERSION}/schema.json`;
|
|
@@ -27,7 +27,7 @@ function report(e) {
|
|
|
27
27
|
if (!isRefusal(e))
|
|
28
28
|
throw e;
|
|
29
29
|
if (json)
|
|
30
|
-
emit({ diagnostics:
|
|
30
|
+
emit({ diagnostics: diagnoseAll(e) });
|
|
31
31
|
else
|
|
32
32
|
console.error(formatRefusal(e));
|
|
33
33
|
}
|
|
@@ -130,8 +130,6 @@ function watchProject(configPath) {
|
|
|
130
130
|
once(config);
|
|
131
131
|
}
|
|
132
132
|
catch (e) {
|
|
133
|
-
// A refusal is reported as one; anything else (a folder renamed away, a file read as it is
|
|
134
|
-
// written) is shown too, and the watch carries on for the change that puts it right.
|
|
135
133
|
if (isRefusal(e))
|
|
136
134
|
report(e);
|
|
137
135
|
else
|
|
@@ -143,8 +141,6 @@ function watchProject(configPath) {
|
|
|
143
141
|
};
|
|
144
142
|
run();
|
|
145
143
|
let timer = null;
|
|
146
|
-
// One recursive watch of the root, filtered to the inputs: watching each input directory instead
|
|
147
|
-
// would miss one created later, and the type files may be anywhere.
|
|
148
144
|
const watcher = watch(root, { recursive: true }, (_event, name) => {
|
|
149
145
|
if (!name || !affects(inputs, join(root, name)))
|
|
150
146
|
return;
|
package/dist/client-render.d.ts
CHANGED
|
@@ -10,12 +10,13 @@ export declare function renderedShape(container: Element): string;
|
|
|
10
10
|
* the selection the HTML parser gives it, from the `selected` attributes (the last one in a single
|
|
11
11
|
* `<select>`, or its first enabled option without one), which happy-dom's property misreports. */
|
|
12
12
|
export declare function parsedShape(container: Element): string;
|
|
13
|
-
/** A plugin that takes what the app is mounted on, and its `renderedShape`
|
|
14
|
-
*
|
|
13
|
+
/** A plugin that takes what the app is mounted on, and its `renderedShape` once the root
|
|
14
|
+
* component's child is mounted: as `mount` returns, or, inside a `<Suspense>`, once it resolves,
|
|
15
|
+
* before any update an `onMounted` hook scheduled has run. `shape` resolves to it. */
|
|
15
16
|
export declare function shapeAtMount(): {
|
|
16
17
|
plugin: Plugin;
|
|
17
18
|
container: () => Element | undefined;
|
|
18
|
-
shape: () => string
|
|
19
|
+
shape: () => Promise<string>;
|
|
19
20
|
};
|
|
20
21
|
/** Set the custom properties `v-bind()` in `<style>` gives each component's root elements, as Vue's
|
|
21
22
|
* client sets them once mounted, where Vue's build does not set them itself (its Node build) and a
|
package/dist/client-render.js
CHANGED
|
@@ -85,19 +85,32 @@ export function renderedShape(container) {
|
|
|
85
85
|
export function parsedShape(container) {
|
|
86
86
|
return nodes(container.childNodes, container.ownerDocument.createElement("div"), true);
|
|
87
87
|
}
|
|
88
|
-
/** A plugin that takes what the app is mounted on, and its `renderedShape`
|
|
89
|
-
*
|
|
88
|
+
/** A plugin that takes what the app is mounted on, and its `renderedShape` once the root
|
|
89
|
+
* component's child is mounted: as `mount` returns, or, inside a `<Suspense>`, once it resolves,
|
|
90
|
+
* before any update an `onMounted` hook scheduled has run. `shape` resolves to it. */
|
|
90
91
|
export function shapeAtMount() {
|
|
91
92
|
let container;
|
|
92
|
-
let
|
|
93
|
+
let taken;
|
|
94
|
+
const shape = new Promise((done) => (taken = done));
|
|
93
95
|
const plugin = {
|
|
94
96
|
install(app) {
|
|
97
|
+
app.mixin({
|
|
98
|
+
mounted() {
|
|
99
|
+
if (!this.$parent || this.$parent.$parent)
|
|
100
|
+
return;
|
|
101
|
+
const parent = this.$el?.parentNode;
|
|
102
|
+
if (parent?.nodeType === 1)
|
|
103
|
+
taken(renderedShape(parent));
|
|
104
|
+
},
|
|
105
|
+
});
|
|
95
106
|
const mount = app.mount.bind(app);
|
|
96
107
|
app.mount = (...args) => {
|
|
97
108
|
const root = mount(...args);
|
|
98
109
|
const [target] = args;
|
|
99
110
|
container = (typeof target === "string" ? document.querySelector(target) : target);
|
|
100
|
-
|
|
111
|
+
const pending = root.$.subTree.suspense?.pendingBranch;
|
|
112
|
+
if (!pending)
|
|
113
|
+
taken(renderedShape(container));
|
|
101
114
|
return root;
|
|
102
115
|
};
|
|
103
116
|
},
|