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/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 = 1;
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: rustTy(ty, comp), value: arg };
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
- const attrs = f.ty.k === "opt" && f.ty.none === undefined
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 = 1;
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 opening = [...preludes.flatMap((p) => p.before), ...lets, ...preludes.flatMap((p) => p.after)];
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
@@ -37,6 +37,7 @@ export const CONFIG_SCHEMA = {
37
37
  },
38
38
  },
39
39
  trustedHtml: STRING,
40
+ dateType: { type: "string", default: "ferrovue::JsDate" },
40
41
  routes: ROUTES,
41
42
  router: {
42
43
  type: "object",
package/dist/script.d.ts CHANGED
@@ -10,4 +10,5 @@ export declare function asyncChildren(comp: Component, ast: N[]): Map<string, st
10
10
  export declare function scopeFor(comp: Component, ast: N[], components: Map<string, Component>): {
11
11
  scope: Scope;
12
12
  lets: string[];
13
+ lazies: Map<string, string>;
13
14
  };
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.message.replace(/^[^:]*: /, ""));
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 lone = v.lone ? { lone: true } : {};
311
- if (v.slice) {
312
- lets.push(`let ${name} = ${bare(v.code)};`);
313
- scope.setup.set(local, { code: name, ty: v.ty, slice: true, ...lone });
314
- continue;
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
- export declare function formatted(comp: Component, parts: (string | Val)[], n: N, what: string): Val;
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.]+$)/.test(v.code) || /\.unwrap_or\(&\*(?!\**[\w.]+\)$)/.test(v.code);
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
- export function formatted(comp, parts, n, what) {
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 = part;
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
- fail(comp, "FV0708", "a template literal interpolates strings, numbers and booleans that are present", n);
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 that is present`, args[i]);
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, 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, 1);
227
+ arity(1, 2);
159
228
  const x = str(0);
160
229
  meet(comp, target, x, `\`.${method}()\``, n, "search");
161
- return { code: `fv::js_${method === "indexOf" ? "index_of" : "last_index_of"}(${t}, ${strArg(x.code)})`, ty: INT };
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, 1);
269
+ arity(1, 2);
182
270
  const sep = str(0);
183
271
  meet(comp, target, sep, "`.split()`", n, "search");
184
- return computed(`fv::js_split(${t}, ${strArg(sep.code)}).into_iter()`, STR, target.lone || !nonEmptyLiteral(sep));
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;
@@ -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 function tyOfTs(comp: Component, t: N, structs: Map<string, Struct>, seen?: Set<string>): Ty;
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 reaches it. */
12
- export declare function reachLocal(comp: Component, name: string): void;
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;