ferrovue 0.7.2 → 0.9.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 +154 -691
- package/dist/calls.d.ts +4 -0
- package/dist/calls.js +146 -13
- package/dist/children.js +8 -0
- package/dist/client.d.ts +7 -0
- package/dist/client.js +25 -3
- package/dist/compiler.js +5 -2
- package/dist/component.js +3 -2
- package/dist/context.d.ts +7 -0
- package/dist/context.js +4 -1
- package/dist/errors.d.ts +116 -5
- package/dist/errors.js +81 -6
- package/dist/expr.js +143 -30
- package/dist/fixture.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lists.d.ts +9 -0
- package/dist/lists.js +421 -24
- package/dist/model.d.ts +1 -0
- package/dist/narrowing.d.ts +9 -0
- package/dist/narrowing.js +98 -9
- package/dist/numbers.d.ts +4 -0
- package/dist/numbers.js +61 -2
- package/dist/parens.js +7 -0
- package/dist/plugin.d.ts +5 -0
- package/dist/plugins/dates.d.ts +25 -0
- package/dist/plugins/dates.js +224 -0
- package/dist/plugins/i18n.js +16 -14
- package/dist/plugins/index.js +2 -1
- package/dist/plugins/router.js +8 -5
- package/dist/rust.d.ts +1 -1
- package/dist/rust.js +22 -9
- package/dist/schema.js +1 -0
- package/dist/script.d.ts +1 -0
- package/dist/script.js +41 -32
- package/dist/strings.d.ts +9 -1
- package/dist/strings.js +113 -23
- package/dist/styles.d.ts +3 -0
- package/dist/styles.js +22 -0
- package/dist/typescript.d.ts +12 -4
- package/dist/typescript.js +50 -12
- package/package.json +20 -20
- package/schema.json +6 -0
package/dist/rust.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export declare function structSource(st: Struct, comp: Component, doc: string):
|
|
|
7
7
|
/** The version of generated code this compiler writes, which `mod.rs` hands `ferrovue::__compat!`:
|
|
8
8
|
* the highest version the `ferrovue` crate of the same release supports. RELEASING.md says when
|
|
9
9
|
* it changes. */
|
|
10
|
-
export declare const GENERATED_VERSION =
|
|
10
|
+
export declare const GENERATED_VERSION = 3;
|
|
11
11
|
/** How every file ferrovue writes begins. */
|
|
12
12
|
export declare const GENERATED = "// @generated by ferrovue";
|
|
13
13
|
export declare function header(source: string, edit?: string): string;
|
package/dist/rust.js
CHANGED
|
@@ -7,7 +7,7 @@ import { childOf } from "./expr.js";
|
|
|
7
7
|
import { Emitter } from "./emitter.js";
|
|
8
8
|
import { occurrences } from "./parens.js";
|
|
9
9
|
import { statements } from "./template.js";
|
|
10
|
-
import { inlineCssVars } from "./styles.js";
|
|
10
|
+
import { cssVarsReads, inlineCssVars } from "./styles.js";
|
|
11
11
|
import { slotFieldBorrows, slotFieldTy, slotTypeName } from "./slots.js";
|
|
12
12
|
import { extraParams, fieldInit, takesSlots } from "./children.js";
|
|
13
13
|
import { paramsOf, renderParams, slotContextOf, slotFieldsOf } from "./plugin.js";
|
|
@@ -33,6 +33,8 @@ export function needsLifetime(ty, comp, seen = new Set()) {
|
|
|
33
33
|
}
|
|
34
34
|
case "html":
|
|
35
35
|
return !ty.inline && (ctx.trustedHtml?.includes("'a") ?? false);
|
|
36
|
+
case "date":
|
|
37
|
+
return ctx.dateType.includes("'a");
|
|
36
38
|
default:
|
|
37
39
|
return false;
|
|
38
40
|
}
|
|
@@ -75,13 +77,26 @@ export function rustTy(ty, comp) {
|
|
|
75
77
|
throw new GenError("FV0309", "no Rust type for `undefined`");
|
|
76
78
|
}
|
|
77
79
|
}
|
|
80
|
+
/** The Rust type of a struct's field: a prop's own `Date` is the type `dateType` names. */
|
|
81
|
+
function fieldTy(ty, comp) {
|
|
82
|
+
if (ty.k === "date")
|
|
83
|
+
return ctx.dateType;
|
|
84
|
+
if (ty.k === "opt" && ty.of.k === "date")
|
|
85
|
+
return `Option<${ctx.dateType}>`;
|
|
86
|
+
return rustTy(ty, comp);
|
|
87
|
+
}
|
|
78
88
|
function param(ty, comp, arg) {
|
|
79
89
|
if (ty.k === "str")
|
|
80
90
|
return { ty: "impl Into<Cow<'a, str>>", value: `${arg}.into()` };
|
|
81
91
|
if (ty.k === "list" && ty.of.k === "str") {
|
|
82
92
|
return { ty: "impl IntoIterator<Item = impl Into<Cow<'a, str>>>", value: `${arg}.into_iter().map(Into::into).collect()` };
|
|
83
93
|
}
|
|
84
|
-
return { ty:
|
|
94
|
+
return { ty: fieldTy(ty, comp), value: arg };
|
|
95
|
+
}
|
|
96
|
+
function serdeAttrs(f) {
|
|
97
|
+
const absent = f.ty.k === "opt" && f.ty.none === undefined;
|
|
98
|
+
const date = f.ty.k === "date" ? ', with = "fv::date_prop"' : f.ty.k === "opt" && f.ty.of.k === "date" ? `${absent ? "" : ", default"}, with = "fv::opt_date_prop"` : "";
|
|
99
|
+
return ` #[serde(rename = ${rustStr(f.js)}${absent ? ', default, skip_serializing_if = "Option::is_none"' : ""}${date})]`;
|
|
85
100
|
}
|
|
86
101
|
function absentNote(optional) {
|
|
87
102
|
const nulls = optional.filter((f) => f.ty.k === "opt" && f.ty.none === "null").length;
|
|
@@ -124,10 +139,7 @@ export function structSource(st, comp, doc) {
|
|
|
124
139
|
const life = structLifetime(st, comp) ? "<'a>" : "";
|
|
125
140
|
const fields = st.fields
|
|
126
141
|
.map((f) => {
|
|
127
|
-
|
|
128
|
-
? ` #[serde(rename = ${rustStr(f.js)}, default, skip_serializing_if = "Option::is_none")]`
|
|
129
|
-
: ` #[serde(rename = ${rustStr(f.js)})]`;
|
|
130
|
-
return `${attrs}\n pub ${f.rust}: ${rustTy(f.ty, comp)},`;
|
|
142
|
+
return `${serdeAttrs(f)}\n pub ${f.rust}: ${fieldTy(f.ty, comp)},`;
|
|
131
143
|
})
|
|
132
144
|
.join("\n");
|
|
133
145
|
const derives = st.fields.some((f) => f.ty.k !== "opt") ? "Debug, Clone" : "Debug, Clone, Default";
|
|
@@ -141,7 +153,7 @@ ${ctx.builders ? `\n${builderSource(st, comp, life)}\n` : ""}`;
|
|
|
141
153
|
/** The version of generated code this compiler writes, which `mod.rs` hands `ferrovue::__compat!`:
|
|
142
154
|
* the highest version the `ferrovue` crate of the same release supports. RELEASING.md says when
|
|
143
155
|
* it changes. */
|
|
144
|
-
export const GENERATED_VERSION =
|
|
156
|
+
export const GENERATED_VERSION = 3;
|
|
145
157
|
/** How every file ferrovue writes begins. */
|
|
146
158
|
export const GENERATED = "// @generated by ferrovue";
|
|
147
159
|
export function header(source, edit = "the `.vue` file") {
|
|
@@ -222,7 +234,7 @@ export function componentSource(comp, ast, ssr, components) {
|
|
|
222
234
|
if (takesAttrs(comp) && comp.inheritAttrs && comp.attrsDropped) {
|
|
223
235
|
fail(comp, "FV0416", `a root \`<Transition>\` or \`<KeepAlive>\` around a \`v-if\` in ${comp.name}, which a parent passes attributes: Vue's server drops them, where its client puts them on the element; set \`inheritAttrs: false\` and bind \`$attrs\` on the element`, comp.attrsDropped);
|
|
224
236
|
}
|
|
225
|
-
const { scope, lets } = scopeFor(comp, ast, components);
|
|
237
|
+
const { scope, lets, lazies } = scopeFor(comp, ast, components);
|
|
226
238
|
const program = parseJs(ssr, { sourceType: "module" }).program;
|
|
227
239
|
tagAst(program, "template");
|
|
228
240
|
const fn = program.body.find((s) => s.type === "ExportNamedDeclaration" && s.declaration?.id?.name === "ssrRender")?.declaration;
|
|
@@ -230,7 +242,8 @@ export function componentSource(comp, ast, ssr, components) {
|
|
|
230
242
|
fail(comp, "FV0006", "the compiled template has no `ssrRender`");
|
|
231
243
|
const e = new Emitter();
|
|
232
244
|
const preludes = ctx.plugins.flatMap((p) => (p.prelude ? [p.prelude(scope)] : []));
|
|
233
|
-
const
|
|
245
|
+
const forced = [...cssVarsReads(fn.body.body)].flatMap((name) => (lazies.has(name) ? [`std::cell::LazyCell::force(&${lazies.get(name)});`] : []));
|
|
246
|
+
const opening = [...preludes.flatMap((p) => p.before), ...lets, ...forced, ...preludes.flatMap((p) => p.after)];
|
|
234
247
|
for (const l of opening)
|
|
235
248
|
e.stmt(l);
|
|
236
249
|
statements(scope, e, inlineCssVars(comp, fn.body.body));
|
package/dist/schema.js
CHANGED
package/dist/script.d.ts
CHANGED
package/dist/script.js
CHANGED
|
@@ -6,7 +6,7 @@ import { constOfDecl, declareConsts } from "./constants.js";
|
|
|
6
6
|
import { localTy } from "./rust.js";
|
|
7
7
|
import { expr, fieldVal, typedBy } from "./expr.js";
|
|
8
8
|
import { collected, heldList } from "./lists.js";
|
|
9
|
-
import { bare, operand, UNARY } from "./parens.js";
|
|
9
|
+
import { bare, occurrences, operand, UNARY } from "./parens.js";
|
|
10
10
|
import { known } from "./narrowing.js";
|
|
11
11
|
import { heldStr, isTemporary } from "./strings.js";
|
|
12
12
|
import { intFromF64 } from "./numbers.js";
|
|
@@ -126,6 +126,36 @@ export function asyncChildren(comp, ast) {
|
|
|
126
126
|
}
|
|
127
127
|
return found;
|
|
128
128
|
}
|
|
129
|
+
const PANICS = /\bfv::js_(?:repeat|pad_start|pad_end|date_parse|date_utc|to_iso_string)\(|\bfv_i18n\.t\(/;
|
|
130
|
+
function mayPanic(scope, lazies, init) {
|
|
131
|
+
if (PANICS.test(init))
|
|
132
|
+
return true;
|
|
133
|
+
if ([...scope.helpers.values()].some((h) => init.includes(`${ctx.helpers[h].rust}(`)))
|
|
134
|
+
return true;
|
|
135
|
+
return [...lazies.values()].some((name) => occurrences(init, name) > 0);
|
|
136
|
+
}
|
|
137
|
+
function binding(v) {
|
|
138
|
+
const lone = v.lone ? { lone: true } : {};
|
|
139
|
+
if (v.slice)
|
|
140
|
+
return { init: bare(v.code), read: (at) => ({ code: at, ty: v.ty, slice: true, ...lone }) };
|
|
141
|
+
if (v.ty.k === "list" && v.iter !== undefined)
|
|
142
|
+
return { init: collected(v), read: (at) => heldList(at, v.ty.k === "list" ? v.ty.of : v.ty, v.lone) };
|
|
143
|
+
if (v.held !== undefined)
|
|
144
|
+
return { init: v.held, read: (at) => ({ code: `${at}.as_deref()`, ty: v.ty, ...lone }) };
|
|
145
|
+
if (v.ty.k === "int" && v.f64 !== undefined && (v.code.endsWith(" as i64") || Object.is(v.num, -0))) {
|
|
146
|
+
return {
|
|
147
|
+
init: bare(v.f64),
|
|
148
|
+
read: (at) => ({ ...intFromF64(at), ...lone, ...(v.num !== undefined ? { num: v.num } : {}), ...(v.konst !== undefined ? { konst: v.konst } : {}) }),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (isTemporary(v))
|
|
152
|
+
return { init: heldStr(v), read: (at) => ({ code: `&*${at}`, ty: v.ty, ...lone }) };
|
|
153
|
+
const place = v.ty.k === "list" || v.ty.k === "record" || v.ty.k === "struct" || v.ty.k === "child";
|
|
154
|
+
return {
|
|
155
|
+
init: place ? `&${operand(v.code, UNARY)}` : bare(v.code),
|
|
156
|
+
read: (at) => ({ code: at, ty: v.ty, ...lone, ...(v.konst !== undefined ? { konst: v.konst } : {}) }),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
129
159
|
export function scopeFor(comp, ast, components) {
|
|
130
160
|
const scope = {
|
|
131
161
|
comp,
|
|
@@ -168,6 +198,7 @@ export function scopeFor(comp, ast, components) {
|
|
|
168
198
|
};
|
|
169
199
|
const locals = declareConsts(comp, ast);
|
|
170
200
|
const lets = [];
|
|
201
|
+
const lazies = new Map();
|
|
171
202
|
const asyncs = asyncChildren(comp, ast);
|
|
172
203
|
let useAttrsName = null;
|
|
173
204
|
let useSlotsName = null;
|
|
@@ -284,6 +315,7 @@ export function scopeFor(comp, ast, components) {
|
|
|
284
315
|
const isRef = init0.type === "CallExpression" && init0.callee.type === "Identifier" && ["ref", "shallowRef", "computed"].includes(init0.callee.name);
|
|
285
316
|
if (isRef)
|
|
286
317
|
scope.refs.add(local);
|
|
318
|
+
const isComputed = isRef && init0.callee.name === "computed";
|
|
287
319
|
const typed = isRef ? init0.typeParameters?.params?.[0] : (d.id.typeAnnotation?.typeAnnotation ?? statedTy(init0));
|
|
288
320
|
const untyped = setupSource(init0);
|
|
289
321
|
const source = untyped && typed ? typedBy(untyped, typed) : untyped;
|
|
@@ -299,7 +331,7 @@ export function scopeFor(comp, ast, components) {
|
|
|
299
331
|
catch (e) {
|
|
300
332
|
if (!(e instanceof GenError))
|
|
301
333
|
throw e;
|
|
302
|
-
scope.clientOnly.set(local, e.
|
|
334
|
+
scope.clientOnly.set(local, e.what);
|
|
303
335
|
continue;
|
|
304
336
|
}
|
|
305
337
|
if (nothing(v.ty) || (v.ty.k === "str" && known(v) !== undefined)) {
|
|
@@ -307,36 +339,13 @@ export function scopeFor(comp, ast, components) {
|
|
|
307
339
|
continue;
|
|
308
340
|
}
|
|
309
341
|
const name = `s_${snake(local).replace(/^r#/, "")}`;
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
if (v.ty.k === "list" && v.iter !== undefined) {
|
|
317
|
-
lets.push(`let ${name} = ${collected(v)};`);
|
|
318
|
-
scope.setup.set(local, heldList(name, v.ty.of, v.lone));
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
if (v.held !== undefined) {
|
|
322
|
-
lets.push(`let ${name} = ${v.held};`);
|
|
323
|
-
scope.setup.set(local, { code: `${name}.as_deref()`, ty: v.ty, ...lone });
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
if (v.ty.k === "int" && v.f64 !== undefined && (v.code.endsWith(" as i64") || Object.is(v.num, -0))) {
|
|
327
|
-
lets.push(`let ${name} = ${bare(v.f64)};`);
|
|
328
|
-
scope.setup.set(local, { ...intFromF64(name), ...lone, ...(v.num !== undefined ? { num: v.num } : {}), ...(v.konst !== undefined ? { konst: v.konst } : {}) });
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
331
|
-
if (isTemporary(v)) {
|
|
332
|
-
lets.push(`let ${name} = ${heldStr(v)};`);
|
|
333
|
-
scope.setup.set(local, { code: `&*${name}`, ty: v.ty, ...lone });
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
336
|
-
const place = v.ty.k === "list" || v.ty.k === "record" || v.ty.k === "struct" || v.ty.k === "child";
|
|
337
|
-
lets.push(`let ${name} = ${place ? `&${operand(v.code, UNARY)}` : bare(v.code)};`);
|
|
338
|
-
scope.setup.set(local, { code: name, ty: v.ty, ...lone, ...(v.konst !== undefined ? { konst: v.konst } : {}) });
|
|
342
|
+
const { init, read } = binding(v);
|
|
343
|
+
const lazy = isComputed && mayPanic(scope, lazies, init);
|
|
344
|
+
lets.push(lazy ? `let ${name} = std::cell::LazyCell::new(|| ${init});` : `let ${name} = ${init};`);
|
|
345
|
+
scope.setup.set(local, read(lazy ? `(*${name})` : name));
|
|
346
|
+
if (lazy)
|
|
347
|
+
lazies.set(local, name);
|
|
339
348
|
}
|
|
340
349
|
}
|
|
341
|
-
return { scope, lets };
|
|
350
|
+
return { scope, lets, lazies };
|
|
342
351
|
}
|
package/dist/strings.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Component, type N, type Scope, type Val } from "./model.ts";
|
|
2
|
+
import type { Code } from "./errors.ts";
|
|
2
3
|
export declare function lonely(what: string): string;
|
|
3
4
|
export declare function meet(comp: Component, a: Val, b: Val, what: string, n: N, how: "equal" | "order" | "search" | "join"): void;
|
|
4
5
|
export declare function loneOf(...vs: (Val | undefined)[]): {
|
|
@@ -12,5 +13,12 @@ export declare function heldStr(v: Val): string;
|
|
|
12
13
|
export declare function yieldsCow(code: string): boolean;
|
|
13
14
|
export declare function unquote(literal: string): string;
|
|
14
15
|
export declare function stringsEqual(a: Val, b: Val): string;
|
|
15
|
-
|
|
16
|
+
/**
|
|
17
|
+
* `v` as JavaScript's `String()` and a template literal write it, where that is not the value
|
|
18
|
+
* itself: an absent value as `undefined` or `null`, a list as its items joined by `,` (an absent
|
|
19
|
+
* item as nothing). Strings, numbers and booleans are returned as they are; any other value is
|
|
20
|
+
* refused with `code`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function stringOf(comp: Component, v: Val, n: N, what: string, code: Code): Val;
|
|
23
|
+
export declare function formatted(comp: Component, given: (string | Val)[], n: N, what: string): Val;
|
|
16
24
|
export declare function stringMethod(s: Scope, target: Val, method: string, args: N[], n: N): Val | null;
|
package/dist/strings.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { BOOL, fail, INT, opt, rustStr, STR } from "./model.js";
|
|
1
|
+
import { BOOL, fail, FLOAT, INT, nothing, opt, rustStr, STR } from "./model.js";
|
|
2
2
|
import { atom, bare, binary, condition, enclosed, receiver, strArg } from "./parens.js";
|
|
3
|
-
import { computed } from "./lists.js";
|
|
4
|
-
import { expr } from "./expr.js";
|
|
3
|
+
import { computed, items } from "./lists.js";
|
|
4
|
+
import { describeTy, expr } from "./expr.js";
|
|
5
5
|
import { known } from "./narrowing.js";
|
|
6
6
|
import { asF64, isNumber } from "./numbers.js";
|
|
7
7
|
export function lonely(what) {
|
|
@@ -30,7 +30,7 @@ export function isTemporary(v) {
|
|
|
30
30
|
const trimmed = /^fv::js_trim(?:_start|_end)?\(([^]*)\)$/.exec(v.code);
|
|
31
31
|
if (trimmed?.[1] !== undefined)
|
|
32
32
|
return isTemporary({ code: trimmed[1].replace(/^&(?!\*)/, "&*"), ty: v.ty });
|
|
33
|
-
return /^&\*(?!\**[\w.]
|
|
33
|
+
return /^&\*(?!\**[\w.]+$|\(\*\w+\)$)/.test(v.code) || /\.unwrap_or\(&\*(?!\**[\w.]+\)$)/.test(v.code);
|
|
34
34
|
}
|
|
35
35
|
export function asCow(v) {
|
|
36
36
|
if (!isTemporary(v))
|
|
@@ -54,7 +54,7 @@ export function heldStr(v) {
|
|
|
54
54
|
return owned !== undefined && enclosed(`(${owned})`) ? owned : cow;
|
|
55
55
|
}
|
|
56
56
|
export function yieldsCow(code) {
|
|
57
|
-
const routine = /^fv::js_(?:slice|substring|replace|replace_all|pad_start|pad_end)(?=\()/.exec(code);
|
|
57
|
+
const routine = /^fv::js_(?:slice|substring|substr|replace|replace_all|pad_start|pad_end)(?=\()/.exec(code);
|
|
58
58
|
if (routine !== null)
|
|
59
59
|
return enclosed(code.slice(routine[0].length));
|
|
60
60
|
const marker = ".unwrap_or(";
|
|
@@ -78,10 +78,64 @@ export function stringsEqual(a, b) {
|
|
|
78
78
|
return `${receiver(a.code === '""' ? b.code : a.code)}.is_empty()`;
|
|
79
79
|
return binary(a.code, "==", b.code);
|
|
80
80
|
}
|
|
81
|
-
|
|
81
|
+
const SCALAR = new Set(["str", "int", "float", "bool"]);
|
|
82
|
+
/**
|
|
83
|
+
* `v` as JavaScript's `String()` and a template literal write it, where that is not the value
|
|
84
|
+
* itself: an absent value as `undefined` or `null`, a list as its items joined by `,` (an absent
|
|
85
|
+
* item as nothing). Strings, numbers and booleans are returned as they are; any other value is
|
|
86
|
+
* refused with `code`.
|
|
87
|
+
*/
|
|
88
|
+
export function stringOf(comp, v, n, what, code) {
|
|
89
|
+
if (SCALAR.has(v.ty.k))
|
|
90
|
+
return v;
|
|
91
|
+
if (nothing(v.ty))
|
|
92
|
+
return { code: rustStr(v.ty.k === "null" ? "null" : "undefined"), ty: STR };
|
|
93
|
+
const lone = loneOf(v);
|
|
94
|
+
const written = (arg) => ({ code: `&*format!("{}", ${arg})`, ty: STR, format: { text: "{}", args: [arg] }, ...lone });
|
|
95
|
+
if (v.ty.k === "opt" && SCALAR.has(v.ty.of.k)) {
|
|
96
|
+
if (v.ty.none === "either") {
|
|
97
|
+
fail(comp, "FV0721", `${what} of a value that may be \`null\` or \`undefined\`, which JavaScript writes as \`null\` or \`undefined\` and its Rust \`Option\` cannot tell apart: give it a fallback with \`??\``, n);
|
|
98
|
+
}
|
|
99
|
+
const word = rustStr(v.ty.none === "null" ? "null" : "undefined");
|
|
100
|
+
switch (v.ty.of.k) {
|
|
101
|
+
case "str":
|
|
102
|
+
return { code: `${atom(v.code)}.unwrap_or(${word})`, ty: STR, ...lone };
|
|
103
|
+
case "bool":
|
|
104
|
+
return written(`fv::JsOr(${atom(v.code)}.map(|v| if v { "true" } else { "false" }), ${word})`);
|
|
105
|
+
default:
|
|
106
|
+
return written(`fv::JsOr(${atom(v.code)}.map(fv::Js), ${word})`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (v.ty.k === "list") {
|
|
110
|
+
const of = v.ty.of;
|
|
111
|
+
const joined = (each) => ({ code: `&*${each}.collect::<Vec<_>>().join(",")`, ty: STR, ...lone });
|
|
112
|
+
if (of.k === "undef")
|
|
113
|
+
return { code: '""', ty: STR };
|
|
114
|
+
if (of.k === "str")
|
|
115
|
+
return joined(items(v));
|
|
116
|
+
if (of.k === "bool")
|
|
117
|
+
return joined(`${items(v)}.map(|v| if v { "true" } else { "false" })`);
|
|
118
|
+
if (of.k === "int" || of.k === "float")
|
|
119
|
+
return joined(`${items(v)}.map(|v| fv::Js(v).to_string())`);
|
|
120
|
+
if (of.k === "opt" && v.iter === undefined && SCALAR.has(of.of.k)) {
|
|
121
|
+
const each = `${atom(v.code)}.iter()`;
|
|
122
|
+
if (of.of.k === "str")
|
|
123
|
+
return joined(`${each}.map(|v| v.as_deref().unwrap_or(""))`);
|
|
124
|
+
if (of.of.k === "bool")
|
|
125
|
+
return joined(`${each}.map(|v| match v { Some(true) => "true", Some(false) => "false", None => "" })`);
|
|
126
|
+
return joined(`${each}.map(|v| v.map_or_else(String::new, |v| fv::Js(v).to_string()))`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return fail(comp, code, `${what} of ${describeTy(v.ty)}: it writes strings, numbers and booleans, present or optional, and lists of them`, n);
|
|
130
|
+
}
|
|
131
|
+
function mayBeEmpty(v) {
|
|
132
|
+
return v.ty.k === "str" && !nonEmptyLiteral(v);
|
|
133
|
+
}
|
|
134
|
+
export function formatted(comp, given, n, what) {
|
|
82
135
|
let fmt = "";
|
|
83
136
|
const args = [];
|
|
84
137
|
let before;
|
|
138
|
+
const parts = given.map((p) => (typeof p === "string" ? p : stringOf(comp, p, n, what, "FV0708")));
|
|
85
139
|
const lone = loneOf(...parts.map((p) => (typeof p === "string" ? undefined : p)));
|
|
86
140
|
for (const part of parts) {
|
|
87
141
|
if (typeof part === "string" || (part.ty.k === "str" && known(part) !== undefined)) {
|
|
@@ -93,7 +147,8 @@ export function formatted(comp, parts, n, what) {
|
|
|
93
147
|
}
|
|
94
148
|
if (before)
|
|
95
149
|
meet(comp, before, part, what, n, "join");
|
|
96
|
-
before
|
|
150
|
+
if (!before?.lone || part.lone || !mayBeEmpty(part))
|
|
151
|
+
before = part;
|
|
97
152
|
if (part.format) {
|
|
98
153
|
fmt += part.format.text;
|
|
99
154
|
args.push(...part.format.args);
|
|
@@ -103,10 +158,8 @@ export function formatted(comp, parts, n, what) {
|
|
|
103
158
|
args.push(receiver(part.code).replace(/^(fv::Js\(.*\))\.to_string\(\)$/s, "$1"));
|
|
104
159
|
else if (isNumber(part.ty))
|
|
105
160
|
args.push(`fv::Js(${bare(part.code)})`);
|
|
106
|
-
else if (part.ty.k === "bool")
|
|
107
|
-
args.push(`if ${condition(part.code)} { "true" } else { "false" }`);
|
|
108
161
|
else
|
|
109
|
-
|
|
162
|
+
args.push(`if ${condition(part.code)} { "true" } else { "false" }`);
|
|
110
163
|
fmt += "{}";
|
|
111
164
|
}
|
|
112
165
|
if (!args.length)
|
|
@@ -114,6 +167,8 @@ export function formatted(comp, parts, n, what) {
|
|
|
114
167
|
const one = parts.filter((p) => (typeof p === "string" ? p !== "" : p.code !== '""'));
|
|
115
168
|
if (fmt === "{}" && one.length === 1 && typeof one[0] !== "string" && one[0].ty.k === "str")
|
|
116
169
|
return one[0];
|
|
170
|
+
if (fmt === "{}" && one.length === 1 && typeof one[0] !== "string" && one[0].ty.k === "bool")
|
|
171
|
+
return { code: args[0], ty: STR };
|
|
117
172
|
return { code: `&*format!(${rustStr(fmt)}, ${args.join(", ")})`, ty: STR, format: { text: fmt, args }, ...lone };
|
|
118
173
|
}
|
|
119
174
|
export function stringMethod(s, target, method, args, n) {
|
|
@@ -132,43 +187,76 @@ export function stringMethod(s, target, method, args, n) {
|
|
|
132
187
|
fail(comp, "FV0711", `\`.${method}()\` takes a string that is present`, args[i]);
|
|
133
188
|
return v;
|
|
134
189
|
};
|
|
135
|
-
const num = (i, fallback) => {
|
|
190
|
+
const num = (i, fallback, absent) => {
|
|
136
191
|
if (args[i] === undefined)
|
|
137
192
|
return fallback;
|
|
138
193
|
const v = expr(s, args[i]);
|
|
139
194
|
if (v.ty.k === "undef")
|
|
140
195
|
return fallback;
|
|
196
|
+
if (v.ty.k === "null")
|
|
197
|
+
return "0.0";
|
|
198
|
+
if (v.ty.k === "opt" && isNumber(v.ty.of) && v.ty.none !== "either") {
|
|
199
|
+
return `${atom(v.code)}.map_or(${v.ty.none === "null" ? "0.0" : absent}, |v| ${v.ty.of.k === "int" ? "v as f64" : "v"})`;
|
|
200
|
+
}
|
|
141
201
|
if (!isNumber(v.ty))
|
|
142
|
-
fail(comp, "FV0712", `\`.${method}()\` takes a number
|
|
202
|
+
fail(comp, "FV0712", `\`.${method}()\` takes a number, present or optional`, args[i]);
|
|
143
203
|
return asF64(v);
|
|
144
204
|
};
|
|
145
205
|
const cut = { lone: true };
|
|
206
|
+
const noHalves = () => {
|
|
207
|
+
if (target.lone)
|
|
208
|
+
fail(comp, "FV0706", lonely(`\`.${method}()\``), n);
|
|
209
|
+
};
|
|
146
210
|
switch (method) {
|
|
147
211
|
case "includes":
|
|
148
212
|
case "startsWith":
|
|
149
213
|
case "endsWith": {
|
|
150
|
-
arity(1,
|
|
214
|
+
arity(1, 2);
|
|
151
215
|
const x = str(0);
|
|
152
216
|
meet(comp, target, x, `\`.${method}()\``, n, "search");
|
|
217
|
+
const at = num(1, "", method === "endsWith" ? "f64::INFINITY" : "0.0");
|
|
218
|
+
if (at) {
|
|
219
|
+
const rust = { includes: "includes_from", startsWith: "starts_with_at", endsWith: "ends_with_at" }[method];
|
|
220
|
+
return { code: `fv::js_${rust}(${t}, ${strArg(x.code)}, ${bare(at)})`, ty: BOOL };
|
|
221
|
+
}
|
|
153
222
|
const rust = { includes: "contains", startsWith: "starts_with", endsWith: "ends_with" }[method];
|
|
154
223
|
return { code: `${receiver(target.code)}.${rust}(${bare(x.code)})`, ty: BOOL };
|
|
155
224
|
}
|
|
156
225
|
case "indexOf":
|
|
157
226
|
case "lastIndexOf": {
|
|
158
|
-
arity(1,
|
|
227
|
+
arity(1, 2);
|
|
159
228
|
const x = str(0);
|
|
160
229
|
meet(comp, target, x, `\`.${method}()\``, n, "search");
|
|
161
|
-
|
|
230
|
+
const at = num(1, "", method === "indexOf" ? "0.0" : "f64::NAN");
|
|
231
|
+
const rust = method === "indexOf" ? "index_of" : "last_index_of";
|
|
232
|
+
if (at)
|
|
233
|
+
return { code: `fv::js_${rust}_from(${t}, ${strArg(x.code)}, ${bare(at)})`, ty: INT };
|
|
234
|
+
return { code: `fv::js_${rust}(${t}, ${strArg(x.code)})`, ty: INT };
|
|
162
235
|
}
|
|
236
|
+
case "substr": {
|
|
237
|
+
arity(0, 2);
|
|
238
|
+
const length = num(1, "", "f64::INFINITY");
|
|
239
|
+
return { code: `&*fv::js_substr(${t}, ${num(0, "0.0", "0.0")}, ${length ? `Some(${bare(length)})` : "None"})`, ty: STR, ...cut };
|
|
240
|
+
}
|
|
241
|
+
case "charCodeAt":
|
|
242
|
+
arity(0, 1);
|
|
243
|
+
noHalves();
|
|
244
|
+
return { code: `fv::js_char_code_at(${t}, ${num(0, "0.0", "0.0")})`, ty: FLOAT };
|
|
245
|
+
case "codePointAt":
|
|
246
|
+
arity(0, 1);
|
|
247
|
+
noHalves();
|
|
248
|
+
return { code: `fv::js_code_point_at(${t}, ${num(0, "0.0", "0.0")})`, ty: opt(INT) };
|
|
249
|
+
case "concat":
|
|
250
|
+
return formatted(comp, [target, ...args.map((a) => stringOf(comp, expr(s, a), a, "`.concat()`", "FV0723"))], n, "`.concat()`");
|
|
163
251
|
case "slice":
|
|
164
252
|
case "substring": {
|
|
165
253
|
arity(0, 2);
|
|
166
|
-
const end = num(1, "");
|
|
167
|
-
return { code: `&*fv::js_${method}(${t}, ${num(0, "0.0")}, ${end ? `Some(${end})` : "None"})`, ty: STR, ...cut };
|
|
254
|
+
const end = num(1, "", "f64::INFINITY");
|
|
255
|
+
return { code: `&*fv::js_${method}(${t}, ${num(0, "0.0", "0.0")}, ${end ? `Some(${end})` : "None"})`, ty: STR, ...cut };
|
|
168
256
|
}
|
|
169
257
|
case "at": {
|
|
170
258
|
arity(1, 1);
|
|
171
|
-
const at = `fv::js_at(${t}, ${num(0, "0.0")})`;
|
|
259
|
+
const at = `fv::js_at(${t}, ${num(0, "0.0", "0.0")})`;
|
|
172
260
|
if (!isTemporary(target))
|
|
173
261
|
return { code: at, ty: opt(STR), ...cut };
|
|
174
262
|
const held = `${at}.map(|v| std::borrow::Cow::<str>::Owned(v.to_owned()))`;
|
|
@@ -176,12 +264,14 @@ export function stringMethod(s, target, method, args, n) {
|
|
|
176
264
|
}
|
|
177
265
|
case "charAt":
|
|
178
266
|
arity(0, 1);
|
|
179
|
-
return { code: `${isTemporary(target) ? "&*" : ""}fv::js_char_at(${t}, ${num(0, "0.0")})`, ty: STR, ...cut };
|
|
267
|
+
return { code: `${isTemporary(target) ? "&*" : ""}fv::js_char_at(${t}, ${num(0, "0.0", "0.0")})`, ty: STR, ...cut };
|
|
180
268
|
case "split": {
|
|
181
|
-
arity(1,
|
|
269
|
+
arity(1, 2);
|
|
182
270
|
const sep = str(0);
|
|
183
271
|
meet(comp, target, sep, "`.split()`", n, "search");
|
|
184
|
-
|
|
272
|
+
const limit = num(1, "", "4_294_967_295.0");
|
|
273
|
+
const pieces = limit ? `fv::js_split_limit(${t}, ${strArg(sep.code)}, ${bare(limit)})` : `fv::js_split(${t}, ${strArg(sep.code)})`;
|
|
274
|
+
return computed(`${pieces}.into_iter()`, STR, target.lone || !nonEmptyLiteral(sep));
|
|
185
275
|
}
|
|
186
276
|
case "replace":
|
|
187
277
|
case "replaceAll": {
|
|
@@ -202,7 +292,7 @@ export function stringMethod(s, target, method, args, n) {
|
|
|
202
292
|
const whole = fill.code.startsWith('"') && !/[\u{10000}-\u{10FFFF}]/u.test(fill.code);
|
|
203
293
|
if (fill.lone || (method === "padStart" && target.lone && !whole))
|
|
204
294
|
fail(comp, "FV0706", lonely(`\`.${method}()\``), n);
|
|
205
|
-
return { code: `&*fv::js_${method === "padStart" ? "pad_start" : "pad_end"}(${t}, ${num(0, "0.0")}, ${strArg(fill.code)})`, ty: STR, ...(target.lone || !whole ? cut : {}) };
|
|
295
|
+
return { code: `&*fv::js_${method === "padStart" ? "pad_start" : "pad_end"}(${t}, ${num(0, "0.0", "0.0")}, ${strArg(fill.code)})`, ty: STR, ...(target.lone || !whole ? cut : {}) };
|
|
206
296
|
}
|
|
207
297
|
case "repeat": {
|
|
208
298
|
arity(1, 1);
|
|
@@ -212,7 +302,7 @@ export function stringMethod(s, target, method, args, n) {
|
|
|
212
302
|
}
|
|
213
303
|
if (target.lone)
|
|
214
304
|
fail(comp, "FV0706", lonely("`.repeat()`"), n);
|
|
215
|
-
return { code: `&*fv::js_repeat(${t}, ${num(0, "0.0")})`, ty: STR };
|
|
305
|
+
return { code: `&*fv::js_repeat(${t}, ${num(0, "0.0", "0.0")})`, ty: STR };
|
|
216
306
|
}
|
|
217
307
|
case "toLocaleUpperCase":
|
|
218
308
|
case "toLocaleLowerCase":
|
package/dist/styles.d.ts
CHANGED
|
@@ -4,6 +4,9 @@ import { Emitter } from "./emitter.ts";
|
|
|
4
4
|
* `<style>`, in place of each use of it in the compiled template's statements, with each value
|
|
5
5
|
* marked to be written as `ssrRenderStyle` writes a variable. */
|
|
6
6
|
export declare function inlineCssVars(comp: Component, body: N[]): N[];
|
|
7
|
+
/** The setup bindings the `_cssVars` object reads, which Vue's server evaluates as its render
|
|
8
|
+
* starts. */
|
|
9
|
+
export declare function cssVarsReads(body: N[]): Set<string>;
|
|
7
10
|
export declare function mergedStyle(s: Scope, e: Emitter, n: N): void;
|
|
8
11
|
export declare function styleAttr(s: Scope, n: N): string;
|
|
9
12
|
export interface StyleItem {
|
package/dist/styles.js
CHANGED
|
@@ -72,6 +72,28 @@ export function inlineCssVars(comp, body) {
|
|
|
72
72
|
swap(rest);
|
|
73
73
|
return rest;
|
|
74
74
|
}
|
|
75
|
+
/** The setup bindings the `_cssVars` object reads, which Vue's server evaluates as its render
|
|
76
|
+
* starts. */
|
|
77
|
+
export function cssVarsReads(body) {
|
|
78
|
+
const names = new Set();
|
|
79
|
+
const declared = body.find((st) => st.type === "VariableDeclaration" && st.declarations[0]?.id?.name === "_cssVars");
|
|
80
|
+
const walk = (n) => {
|
|
81
|
+
if (!n || typeof n !== "object")
|
|
82
|
+
return;
|
|
83
|
+
if (Array.isArray(n))
|
|
84
|
+
return n.forEach(walk);
|
|
85
|
+
if (n.type === "MemberExpression" && n.object.type === "Identifier" && (n.object.name === "$setup" || n.object.name === "_ctx")) {
|
|
86
|
+
const name = n.computed ? n.property.value : n.property.name;
|
|
87
|
+
if (typeof name === "string")
|
|
88
|
+
names.add(name);
|
|
89
|
+
}
|
|
90
|
+
for (const [k, v] of Object.entries(n))
|
|
91
|
+
if (k !== "loc" && v && typeof v === "object")
|
|
92
|
+
walk(v);
|
|
93
|
+
};
|
|
94
|
+
walk(declared?.declarations[0].init);
|
|
95
|
+
return names;
|
|
96
|
+
}
|
|
75
97
|
function cssVarOf(s, n) {
|
|
76
98
|
const v = expr(s, n.value);
|
|
77
99
|
const of = v.ty.k === "opt" ? v.ty.of : v.ty;
|
package/dist/typescript.d.ts
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
import { type Component, type Field, type N, type Struct, type Ty } from "./model.ts";
|
|
2
2
|
export declare function typesImports(comp: Component, body: N[]): void;
|
|
3
3
|
export declare const ONE_NOTHING = "its Rust type is an `Option`, whose `None` cannot be both, as `=== null` and the props sent to the client would need";
|
|
4
|
-
export declare
|
|
4
|
+
export declare const DATE_PROPS_ONLY = "only a component's own prop may be a `Date`, which the client turns back into one from the prop's declared type: pass an ISO string or a number of milliseconds and build the date with `new Date()` (see the `dates_and_prices` guide)";
|
|
5
|
+
/** A TypeScript type's type, where `dates` says whether it may be a `Date`: a component's own prop,
|
|
6
|
+
* which may be optional or nullable too. */
|
|
7
|
+
export declare function tyOfTs(comp: Component, t: N, structs: Map<string, Struct>, seen?: Set<string>, dates?: boolean): Ty;
|
|
5
8
|
export declare function declareTypes(comp: Component, body: N[], structs: Map<string, Struct>, aliases: Map<string, N>): N[];
|
|
6
9
|
export declare function refusePrelude(comp: Component, d: N): void;
|
|
7
10
|
export declare function resolveImport(fromFile: string, spec: string): string | null;
|
|
8
11
|
export declare function readTypeFile(file: string): void;
|
|
9
12
|
/** Translate a type a `.ts` file declares, the first time something the server renders reaches it. */
|
|
10
13
|
export declare function reachType(name: string): void;
|
|
11
|
-
/** Translate an interface a component declares, the first time something the server renders
|
|
12
|
-
|
|
14
|
+
/** Translate an interface a component declares, the first time something the server renders
|
|
15
|
+
* reaches it; `dates` when it is the component's props. */
|
|
16
|
+
export declare function reachLocal(comp: Component, name: string, dates?: boolean): void;
|
|
13
17
|
/** Refuse a type whose every field a value of it must carry, as props and store state do, where one
|
|
14
18
|
* of its fields, or of the types it holds, has no Rust type. */
|
|
15
19
|
export declare function requireWhole(comp: Component, ty: Ty, seen?: Set<Struct>): void;
|
|
20
|
+
/** Whether a value of the type holds a `Date` inside a list, a dictionary or an object. */
|
|
21
|
+
export declare function holdsDate(comp: Component, ty: Ty, seen?: Set<Struct | Component>): boolean;
|
|
22
|
+
/** Refuse a prop that holds a `Date` anywhere but as itself, optional or nullable. */
|
|
23
|
+
export declare function refuseNestedDates(comp: Component, fields: Field[], node: N): void;
|
|
16
24
|
/** Whether `types.rs` holds the type: one built from a constant list, or a declared one something reached. */
|
|
17
25
|
export declare function typeWritten(name: string): boolean;
|
|
18
26
|
export declare function markHome(ty: Ty, home: string): Ty;
|
|
@@ -23,7 +31,7 @@ export declare function lookupStruct(comp: Component, ty: Ty & {
|
|
|
23
31
|
owner: Component;
|
|
24
32
|
path: string;
|
|
25
33
|
};
|
|
26
|
-
export declare function structOf(comp: Component, name: string, members: N[], structs: Map<string, Struct>, lenient?: boolean): Struct;
|
|
34
|
+
export declare function structOf(comp: Component, name: string, members: N[], structs: Map<string, Struct>, lenient?: boolean, dates?: boolean): Struct;
|
|
27
35
|
export declare function definePropsType(call: N): N | null;
|
|
28
36
|
export declare function runtimeDefaults(comp: Component, content: string): Map<string, N>;
|
|
29
37
|
export declare function defaultValue(comp: Component, f: Field, node: N, written?: N): string;
|