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/errors.js CHANGED
@@ -18,6 +18,7 @@ export const AREAS = {
18
18
  "15": { name: "Raw HTML, client-only content, teleports and Rust twins", guide: "errors_and_limits" },
19
19
  "16": { name: "Provide and inject", guide: "provide_inject" },
20
20
  "17": { name: "Page head", guide: "head" },
21
+ "18": { name: "Dates and locale formatting", guide: "dates_and_prices" },
21
22
  };
22
23
  /** Every code the compiler raises. A code is never reused: one the compiler stops raising is marked
23
24
  * `retired` and stays listed. `pnpm errors:generate` writes the guide's index from this list. */
@@ -149,13 +150,13 @@ export const ERRORS = {
149
150
  FV0601: { title: "Name or function not available on the server" },
150
151
  FV0602: { title: "Unsupported method" },
151
152
  FV0603: { title: "Unsupported call" },
152
- FV0604: { title: "Field read on a value that is not an object" },
153
+ FV0604: { title: "Field read on a value that is not an object, or that may be absent" },
153
154
  FV0605: { title: "Field the type does not have" },
154
155
  FV0606: { title: "Value that may be absent where its target cannot take it as it is" },
155
156
  FV0607: { title: "Value of an unexpected type" },
156
157
  FV0608: { title: "`+` between unsupported types" },
157
158
  FV0609: { title: "Comparison between unsupported or absent values" },
158
- FV0610: { title: "Spread or hole in an array literal" },
159
+ FV0610: { title: "Hole in an array literal" },
159
160
  FV0611: { title: "Array literal of different types" },
160
161
  FV0612: { title: "Computed member access" },
161
162
  FV0613: { title: "Value set up in a way the server cannot evaluate" },
@@ -174,17 +175,28 @@ export const ERRORS = {
174
175
  FV0626: { title: "Strict test of a value that may be `null` or `undefined`" },
175
176
  FV0627: { title: "Strict test against the absence a value cannot have" },
176
177
  FV0628: { title: "`as` or `satisfies` stating a type the value does not have" },
178
+ FV0629: { title: "Optional call `?.()`" },
179
+ FV0630: { title: "`??` between a list an optional chain computes and another list" },
180
+ FV0631: {
181
+ title: "`in` or `instanceof`",
182
+ detail: "`in` and `instanceof` read an object's keys and prototype as the page renders, which the server's typed values do not carry. Test a record's key with `Object.keys(r).includes(key)`.",
183
+ },
184
+ FV0632: { title: "Bitwise or shift operator" },
185
+ FV0633: {
186
+ title: "Part of JavaScript's own globals the server does not translate",
187
+ detail: "`JSON`, `Math`, `Number`, `String` and the other globals a template may name are translated only in part: `JSON.stringify(x)` of one value, `String(x)`, `Number(x)` and the `Number.is…()` tests, the `Math` methods and constants the [`numbers`](crate::guide::numbers) page lists, `encodeURI` and `encodeURIComponent`. The message names what the server translates of the one used.",
188
+ },
177
189
  FV0701: { title: "`Number()` of an unsupported value" },
178
190
  FV0702: { title: "`parseInt()` or `parseFloat()` of a value that is not a string" },
179
191
  FV0703: { title: "`parseInt()` with a radix other than a literal 10 or 16" },
180
192
  FV0704: { title: "Unsupported `Math` method" },
181
193
  FV0705: { title: "`.toFixed()` without a literal number of digits from 0 to 100" },
182
194
  FV0706: {
183
- title: "String that may hold half of a surrogate pair compared, searched or joined",
184
- detail: "`slice`, `substring`, `at`, `charAt` and `split(\"\")` can cut a surrogate pair in half. JavaScript keeps the half where ferrovue holds U+FFFD, so two such strings may compare, search or join differently. See [`strings`](crate::guide::strings#halves-of-surrogate-pairs).",
195
+ title: "String that may hold half of a surrogate pair compared, searched, joined or encoded",
196
+ detail: "`slice`, `substring`, `substr`, `at`, `charAt` and `split(\"\")` can cut a surrogate pair in half. JavaScript keeps the half where ferrovue holds U+FFFD, so two such strings may compare, search or join differently, `charCodeAt` and `codePointAt` would read U+FFFD's code, and `encodeURI` and `encodeURIComponent`, which throw a `URIError` on a half, would encode U+FFFD. See [`strings`](crate::guide::strings#halves-of-surrogate-pairs).",
185
197
  },
186
198
  FV0707: { title: "`JSON.stringify()` of an unsupported value" },
187
- FV0708: { title: "Template literal of values that are not present strings, numbers or booleans" },
199
+ FV0708: { title: "Template literal of a value it cannot write" },
188
200
  FV0709: { title: "String method with the wrong number of arguments" },
189
201
  FV0710: { title: "Regular expression given to a string method" },
190
202
  FV0711: { title: "String method given a value that is not a present string" },
@@ -192,6 +204,23 @@ export const ERRORS = {
192
204
  FV0713: { title: "Function given to a string method" },
193
205
  FV0714: { title: "`.repeat()` by a negative or infinite count" },
194
206
  FV0715: { title: "Case mapping by the server's locale" },
207
+ FV0716: { title: "`.toPrecision()` without a literal precision from 1 to 100" },
208
+ FV0717: { title: "`.toExponential()` without a literal number of digits from 0 to 100" },
209
+ FV0718: {
210
+ title: "`isNaN()`, `isFinite()` or a `Number.is…()` test of a value that is not a number",
211
+ detail: "`Number.isNaN()`, `Number.isFinite()`, `Number.isInteger()` and `Number.isSafeInteger()` take a number, present or optional, and are `false` for any other value, which they do not convert; the global `isNaN()` and `isFinite()` convert theirs as `Number()` does and take a present number. Test a date with `Number.isNaN(d.getTime())`, and a string with `Number.isNaN(Number(s))`.",
212
+ },
213
+ FV0719: {
214
+ title: "`Math` function engines round differently",
215
+ detail: "`Math.cbrt`, `Math.hypot`, `Math.exp`, `Math.log`, the trigonometric functions and their kin are computed by each JavaScript engine's own routine, which the specification leaves free to round differently, and Rust's do not promise V8's digits either. Compute the value in Rust and pass it as a prop.",
216
+ },
217
+ FV0720: {
218
+ title: "`**` or `Math.pow()` other than an exact integer power known when compiling",
219
+ detail: "Engines compute powers by different routines: Node 22's V8 writes `10 ** -20` as `1.0000000000000001e-20`, Node 24's and Firefox's as `1e-20`, and above 2⁵³ they round in their own ways. The compiler works out `a ** b` and `Math.pow(a, b)` when both are known, `a` an integer and `b` a whole number from 0, with a result within ±2⁵³, which every engine gives exactly.",
220
+ },
221
+ FV0721: { title: "Value that may be `null` or `undefined` written as a string" },
222
+ FV0722: { title: "`typeof` the compiler cannot work out" },
223
+ FV0723: { title: "`String()`, `.concat()` or `encodeURI…()` of a value it cannot write" },
195
224
  FV0801: { title: "`.includes()` of a value of another type than the list's" },
196
225
  FV0802: { title: "`.join()` with a separator that is not a string" },
197
226
  FV0803: { title: "List method on a value that is not a list" },
@@ -215,6 +244,16 @@ export const ERRORS = {
215
244
  FV0821: { title: "Defaults or nested patterns in a `v-for` item" },
216
245
  FV0822: { title: "`v-for` item bound by an unsupported pattern" },
217
246
  FV0823: { title: "`v-for` key or index that is not a plain name" },
247
+ FV0824: { title: "List item read by an index that is not an integer" },
248
+ FV0825: { title: "`.at()` of a list given anything but one number" },
249
+ FV0826: { title: "`.indexOf()` or `.lastIndexOf()` given anything but one value of the list's type" },
250
+ FV0827: { title: "`.concat()` of anything but lists of the same item type" },
251
+ FV0828: { title: "`.reduce()` or `.reduceRight()` without an initial value" },
252
+ FV0829: { title: "`.reduce()` or `.reduceRight()` whose accumulator changes type or is not a string, number or boolean" },
253
+ FV0830: { title: "`.toSorted()` with a comparator of a shape the server does not order by" },
254
+ FV0831: { title: "`.toSorted()` without a comparator of a list of objects" },
255
+ FV0832: { title: "`.sort()` or `.reverse()` of a list in place" },
256
+ FV0833: { title: "Array spread of anything but lists of the same item type" },
218
257
  FV0901: { title: "Slots that are not an object literal" },
219
258
  FV0902: { title: "Computed or spread slot name" },
220
259
  FV0903: { title: "Content for a slot the child does not have" },
@@ -325,7 +364,7 @@ export const ERRORS = {
325
364
  FV1231: { title: "Route field not available on the server" },
326
365
  FV1232: { title: "Routes file that is not an array" },
327
366
  FV1233: { title: "Route entry of an unsupported shape" },
328
- FV1234: { title: "`typeof` of anything but a query value" },
367
+ FV1234: { title: "`typeof` of anything but a query value", retired: "0.9.0" },
329
368
  FV1235: { title: "`??` after a query value with a fallback that is not a string" },
330
369
  FV1236: { title: "`<RouterView>` in a component with `<style scoped>`" },
331
370
  FV1237: { title: "`<RouterView>` in a child component" },
@@ -448,6 +487,42 @@ export const ERRORS = {
448
487
  detail: "unhead's server renderer throws on a `class` or `style` of `null`. Write `?? undefined`, which leaves the attribute out.",
449
488
  },
450
489
  FV1711: { title: "`innerHTML` or `textContent` on a head tag that has no content" },
490
+ FV1801: {
491
+ title: "The current time: `new Date()` or `Date.now()`",
492
+ detail: "The server renders at one moment and the browser hydrates at another, so the text would differ. Read the time in Rust and pass it as a prop: an ISO string, or a number of milliseconds. See [`dates_and_prices`](crate::guide::dates_and_prices).",
493
+ },
494
+ FV1802: {
495
+ title: "`new Date()` or `Date.parse()` of anything but one present string or number",
496
+ detail: "`new Date(year, month, …)` reads its parts in the time zone of the machine running it. A value that may be absent gives an invalid date for `undefined` and the epoch for `null`. See [`dates_and_prices`](crate::guide::dates_and_prices).",
497
+ },
498
+ FV1803: {
499
+ title: "Date string that browsers may read differently",
500
+ detail: "ECMAScript specifies how a date alone (`2024-05-01`, `2024-05`, `2024`) and a date and time with a `Z` or an offset (`2024-05-01T09:30:00.000Z`, `2024-05-01T09:30+02:00`) are read. A date and time with neither is read in the time zone of the machine running it, and every other string by each engine's own rules, as are fields out of range (`2024-02-30`, `T24:00`) and fields beyond the range of dates that an offset brings back within it (`+275760-09-13T01:00+01:00`), which Safari reads as invalid. A literal is checked when the component is compiled; a string from the props is checked as it renders, and one outside the format panics. See [`dates_and_prices`](crate::guide::dates_and_prices).",
501
+ },
502
+ FV1804: {
503
+ title: "`Date` method or function ferrovue does not translate",
504
+ detail: "A date is read with `.toISOString()`, `.toUTCString()`, `.toJSON()`, `.getTime()`, `.valueOf()` and the `.getUTC…()` methods, which do not depend on the time zone. The local getters (`.getFullYear()`, `.getHours()`), `.getTimezoneOffset()` and `.toString()` read the time zone of the machine running them, which differs between the server and the reader's browser. See [`dates_and_prices`](crate::guide::dates_and_prices).",
505
+ },
506
+ FV1805: {
507
+ title: "Locale formatting with `Intl`, `.toLocaleString()`, `.localeCompare()`, `$d` or `$n`",
508
+ detail: "`Intl` formats from locale data each JavaScript engine ships, and Node, Chromium, Firefox and Safari write some dates, times and numbers differently (spaces, separators, abbreviations), so no server can write what every reader's browser writes. This refusal is permanent. Format the value in Rust and pass it as a string prop, or write it with `.toFixed()` and `.toISOString()` into a message `$t` translates. See [`dates_and_prices`](crate::guide::dates_and_prices).",
509
+ },
510
+ FV1806: {
511
+ title: "`Date` anywhere but a component's own prop",
512
+ detail: "A prop typed `Date` (optional or nullable too) is a `ferrovue::JsDate`, or the type `dateType` names, and an island's `data-props` carries it as its ISO string, which the client turns back into a `Date` because the prop's declared type says it is one. A `Date` inside a list, a dictionary or an object prop, and one in an interface the props do not declare, in Pinia state, `inject`, `ref` or `defineModel`, have no declared type the client could read, so they would reach the browser as strings. Pass the ISO string or the number of milliseconds and build the date with `new Date()`. See [`dates_and_prices`](crate::guide::dates_and_prices).",
513
+ },
514
+ FV1807: {
515
+ title: "`Date.UTC()` of anything but one to seven numbers that are present",
516
+ detail: "`Date.UTC(year, month, date, hours, minutes, seconds, ms)` takes numbers, each truncated toward zero, the month counted from 0. A value that may be absent would give `NaN` for `undefined` and `0` for `null`: narrow it with `v-if` or give it a default. See [`dates_and_prices`](crate::guide::dates_and_prices).",
517
+ },
518
+ FV1808: {
519
+ title: "`Date.UTC()` of parts engines compute differently",
520
+ detail: "V8 computes a year beyond ±1,000,000, a month beyond ±10,000,000 and a year before −399,999 by rules of its own, which Firefox and Safari do not share, so a day or hours that bring the date back within the range give another date in each; and Node rounds a part worth more than 2⁵³ ms that the others bring back within the range otherwise than the browsers. Literal parts are checked when the component is compiled; parts from the props are checked as it renders, and `js_date_utc` panics on them. See [`dates_and_prices`](crate::guide::dates_and_prices).",
521
+ },
522
+ FV1809: {
523
+ title: "`===`, `!==`, `==` or `!=` with a `Date`",
524
+ detail: "Two `Date`s are equal in JavaScript only when they are one object, so two dates of the same time are unequal, and `==` with a string compares the date's `.toString()`, in the time zone of the machine running it. Compare time values: `a.getTime() === b.getTime()`. `<`, `<=`, `>` and `>=` between two dates compare their time values, and are false when either is invalid. See [`dates_and_prices`](crate::guide::dates_and_prices).",
525
+ },
451
526
  };
452
527
  /** Where a code is documented. */
453
528
  export function docsUrl(code) {
package/dist/expr.js CHANGED
@@ -2,13 +2,16 @@ import { absence, BOOL, fail, FLOAT, GenError, INT, joinAbsence, nothing, NULL,
2
2
  import { ctx } from "./context.js";
3
3
  import { lookupStruct, markHome, tyOfTs } from "./typescript.js";
4
4
  import { claim } from "./plugin.js";
5
- import { atom, bare, binary, enclosed, logical, negate, not, occurrences, strArg } from "./parens.js";
6
- import { collected, sliced, typedEmpty, untypedEmpty } from "./lists.js";
5
+ import { atom, bare, binary, enclosed, logical, negate, not, occurrences, receiver, strArg } from "./parens.js";
6
+ import { collected, picked, sliced, spreadLiteral, typedEmpty, untypedEmpty } from "./lists.js";
7
7
  import { asCow, formatted, isTemporary, loneOf, meet, stringsEqual } from "./strings.js";
8
- import { arithmetic, asF64, compare, intFromF64, isNumber, negatedOrder, numberVal } from "./numbers.js";
9
- import { boolOf, checkNullTest, choice, known, narrowing, narrowTo, nullTest, pathOf, presence, truthy } from "./narrowing.js";
10
- import { call, isObjectCall } from "./calls.js";
8
+ import { arithmetic, asF64, compare, intFromF64, isNumber, negatedOrder, numberVal, power } from "./numbers.js";
9
+ import { boolOf, checkNullTest, choice, known, narrowing, narrowTo, nullTest, pathOf, presence, truthy, typeofCompare, typeofTest, typeofVal } from "./narrowing.js";
10
+ import { BUILTINS, builtin, call, isObjectCall } from "./calls.js";
11
11
  import { constOf, constVal } from "./constants.js";
12
+ import { localeRefusal } from "./plugins/dates.js";
13
+ const MATH_CONSTANTS = new Map(["E", "LN10", "LN2", "LOG10E", "LOG2E", "PI", "SQRT1_2", "SQRT2"].map((k) => [k, Math[k]]));
14
+ const NUMBER_CONSTANTS = new Map(["EPSILON", "MAX_SAFE_INTEGER", "MAX_VALUE", "MIN_SAFE_INTEGER", "MIN_VALUE", "NaN", "NEGATIVE_INFINITY", "POSITIVE_INFINITY"].map((k) => [k, Number[k]]));
12
15
  export function describeTy(ty) {
13
16
  switch (ty.k) {
14
17
  case "str":
@@ -41,8 +44,11 @@ export function describeTy(ty) {
41
44
  }
42
45
  }
43
46
  export function fieldVal(comp, base, ty, js, node) {
44
- if (ty.k !== "struct")
45
- fail(comp, "FV0604", `\`.${js}\` on a value that is not an object`, node);
47
+ if (ty.k !== "struct") {
48
+ if (ty.k === "opt" || nothing(ty))
49
+ fail(comp, "FV0604", `\`.${js}\` of ${describeTy(ty)}, which may be absent: narrow it with \`v-if\` first, or read it with \`?.${js}\``, node);
50
+ fail(comp, "FV0604", `\`.${js}\` of ${describeTy(ty)}, which has no field \`${js}\``, node);
51
+ }
46
52
  const { st, owner } = lookupStruct(comp, ty);
47
53
  const found = st?.fields.find((x) => x.js === js);
48
54
  if (!found) {
@@ -69,7 +75,11 @@ export function fieldVal(comp, base, ty, js, node) {
69
75
  case "html":
70
76
  case "child":
71
77
  return { code: `&${place}`, ty: f.ty };
78
+ case "date":
79
+ return { code: `fv::js_time_of(&${place})`, ty: f.ty };
72
80
  case "opt":
81
+ if (f.ty.of.k === "date")
82
+ return { code: `${place}.as_ref().map(fv::js_time_of)`, ty: f.ty };
73
83
  if (f.ty.of.k === "html")
74
84
  return { code: `${place}.as_ref()`, ty: f.ty };
75
85
  if (f.ty.of.k === "str")
@@ -170,6 +180,76 @@ function stated(s, n) {
170
180
  return v;
171
181
  return fail(comp, "FV0628", `\`${keyword}\` states ${describeTy(want)} of ${describeTy(v.ty)}, which the server keeps as it is: write the value with the type it has`, n);
172
182
  }
183
+ function indexed(s, list, key, n) {
184
+ const i = expr(s, key);
185
+ if (i.ty.k !== "int") {
186
+ const is = i.ty.k === "float" ? "a `Float`, which may hold a fraction" : describeTy(i.ty);
187
+ return fail(s.comp, "FV0824", `\`[…]\` reads a list's item by an integer index, a \`number\` that is present, and this index is ${is}: \`.at()\` takes any number`, key);
188
+ }
189
+ if (i.num !== undefined && i.num >= 0 && list.iter === undefined)
190
+ return picked(s, list, n, (l) => (i.num === 0 ? `${l}.first()` : `${l}.get(${i.num})`), (it) => it);
191
+ const at = `usize::try_from(${bare(i.code)}).unwrap_or(usize::MAX)`;
192
+ return picked(s, list, n, (l) => `${l}.get(${at})`, (it) => `${atom(it)}.nth(${at})`);
193
+ }
194
+ function plainChain(n, link, hole) {
195
+ if (n === link)
196
+ return { ...n, type: "MemberExpression", optional: undefined, object: hole };
197
+ if (n.type === "OptionalCallExpression")
198
+ return { ...n, type: "CallExpression", optional: undefined, callee: plainChain(n.callee, link, hole) };
199
+ return { ...n, type: "MemberExpression", optional: undefined, object: plainChain(n.object, link, hole) };
200
+ }
201
+ function ownedCow(v) {
202
+ if (!isTemporary(v))
203
+ return `std::borrow::Cow::<str>::Owned(${receiver(v.code)}.to_owned())`;
204
+ const cow = asCow(v);
205
+ return cow.startsWith("std::borrow::Cow::<str>::Owned(") ? cow : `std::borrow::Cow::<str>::Owned(${atom(cow)}.into_owned())`;
206
+ }
207
+ function optionalChain(s, n) {
208
+ const comp = s.comp;
209
+ let link = n;
210
+ while (!link.optional) {
211
+ link = link.type === "OptionalCallExpression" ? link.callee : link.object;
212
+ if (link?.type !== "OptionalMemberExpression" && link?.type !== "OptionalCallExpression")
213
+ return fail(comp, "FV0625", "this optional chain is not supported in a template", n);
214
+ }
215
+ if (link.type === "OptionalCallExpression") {
216
+ return fail(comp, "FV0629", "`?.()` calls a function that may be absent, and a template's functions are always present: write the call without `?.`", link);
217
+ }
218
+ const base = expr(s, link.object);
219
+ if (nothing(base.ty))
220
+ return { code: "None", ty: UNDEF };
221
+ const k = ++ctx.narrowCount;
222
+ const hole = { ...link.object, type: "Identifier", name: `fv$o${k}` };
223
+ const rest = plainChain(n, link, hole);
224
+ const within = (v) => ({ ...s, locals: new Map(s.locals).set(hole.name, v) });
225
+ if (base.ty.k !== "opt")
226
+ return expr(within(base), rest);
227
+ const name = `fv_o${k}`;
228
+ const inner = expr(within({ code: name, ty: base.ty.of, ...loneOf(base) }), rest);
229
+ if (nothing(inner.ty))
230
+ return { code: "None", ty: UNDEF };
231
+ const lone = loneOf(base, inner);
232
+ const over = (body, flat) => {
233
+ if (bare(body) === name)
234
+ return base.code;
235
+ const named = new RegExp(`^([\\w:]+)\\(${name}\\)$`).exec(bare(body))?.[1];
236
+ return `${atom(base.code)}.${flat ? "and_then" : "map"}(${named ?? `|${name}| ${bare(body)}`})`;
237
+ };
238
+ const heldBy = (held, ty) => ({ code: `${atom(held)}.as_deref()`, ty, held, ...lone });
239
+ if (inner.ty.k === "list" && inner.iter !== undefined)
240
+ return heldBy(over(collected(inner), false), opt(inner.ty));
241
+ if (inner.ty.k === "opt") {
242
+ if (inner.held !== undefined)
243
+ return heldBy(over(inner.held, true), opt(inner.ty));
244
+ if (base.held !== undefined && inner.ty.of.k === "str")
245
+ return heldBy(over(`${atom(inner.code)}.map(|v| std::borrow::Cow::<str>::Owned(v.to_owned()))`, true), opt(inner.ty));
246
+ return { code: over(inner.code, true), ty: opt(inner.ty), ...lone };
247
+ }
248
+ if (inner.ty.k === "str" && (isTemporary(inner) || base.held !== undefined))
249
+ return heldBy(over(ownedCow(inner), false), opt(STR));
250
+ const place = ["list", "record", "struct"].includes(inner.ty.k) && new RegExp(`^${name}(?:\\.[\\w#]+)+$`).test(inner.code);
251
+ return { code: over(place ? `&${inner.code}` : inner.code, false), ty: opt(inner.ty), ...lone };
252
+ }
173
253
  export function expr(s, n) {
174
254
  const comp = s.comp;
175
255
  const path = pathOf(n);
@@ -215,10 +295,24 @@ export function expr(s, n) {
215
295
  return arithmetic(a, "+", b, a.ty.k === "int" && b.ty.k === "int");
216
296
  return fail(comp, "FV0608", "`+` joins a string to a string or a number, or adds two numbers", n);
217
297
  }
298
+ if (n.type === "BinaryExpression" && n.operator === "**")
299
+ return power(s, n.left, n.right, n, "`**`");
300
+ if (n.type === "BinaryExpression" && (n.operator === "in" || n.operator === "instanceof")) {
301
+ const what = n.operator === "in"
302
+ ? "tests whether an object has a key, which the server does not translate: for a record, write `Object.keys(r).includes(key)`"
303
+ : "tests an object's prototype, which the server's typed values do not carry: each value's type is known when compiling";
304
+ return fail(comp, "FV0631", `\`${n.operator}\` ${what}`, n);
305
+ }
306
+ if (n.type === "BinaryExpression" && ["&", "|", "^", "<<", ">>", ">>>"].includes(n.operator)) {
307
+ return fail(comp, "FV0632", `\`${n.operator}\` works on the 32-bit integers JavaScript converts its operands to, which the server does not translate: compute it in Rust and pass it as a prop`, n);
308
+ }
218
309
  if (n.type === "BinaryExpression" && ["-", "*", "%", "/", "<", ">", "<=", ">="].includes(n.operator)) {
219
310
  const a = expr(s, n.left);
220
311
  const b = expr(s, n.right);
221
312
  const comparison = ["<", ">", "<=", ">="].includes(n.operator);
313
+ const ordered = comparison ? claim((p) => p.values?.order?.(a, n.operator, b)) : undefined;
314
+ if (ordered)
315
+ return ordered;
222
316
  if (comparison && a.ty.k === "str" && b.ty.k === "str") {
223
317
  meet(comp, a, b, `\`${n.operator}\``, n, "order");
224
318
  const is = { "<": "lt", ">": "gt", "<=": "le", ">=": "ge" }[n.operator];
@@ -228,7 +322,7 @@ export function expr(s, n) {
228
322
  const why = a.ty.k === "opt" || b.ty.k === "opt"
229
323
  ? "narrow an optional one with `v-if` first"
230
324
  : `the other is ${describeTy(isNumber(a.ty) ? b.ty : a.ty)}`;
231
- const between = comparison ? "two numbers, or two strings," : "two numbers";
325
+ const between = comparison ? "two numbers, two strings or two dates" : "two numbers";
232
326
  return fail(comp, "FV0609", `\`${n.operator}\` is supported between ${between} that are present: ${why}`, n);
233
327
  }
234
328
  if (["<", ">", "<=", ">="].includes(n.operator))
@@ -250,9 +344,11 @@ export function expr(s, n) {
250
344
  case "ArrayExpression": {
251
345
  if (n.elements.length === 0)
252
346
  return { code: "[]", ty: { k: "list", of: UNDEF } };
347
+ if (n.elements.some((el) => el?.type === "SpreadElement"))
348
+ return spreadLiteral(s, n);
253
349
  const values = n.elements.map((el) => {
254
- if (!el || el.type === "SpreadElement")
255
- fail(comp, "FV0610", "an array literal holds plain values", n);
350
+ if (!el)
351
+ fail(comp, "FV0610", "an array literal holds plain values, without holes", n);
256
352
  return expr(s, el);
257
353
  });
258
354
  const of = values[0].ty;
@@ -261,21 +357,9 @@ export function expr(s, n) {
261
357
  }
262
358
  return { code: `[${values.map((v) => bare(v.code)).join(", ")}]`, ty: { k: "list", of }, ...loneOf(...values) };
263
359
  }
264
- case "OptionalMemberExpression": {
265
- if (n.computed)
266
- return fail(comp, "FV0612", "computed member access", n);
267
- const base = expr(s, n.object);
268
- if (nothing(base.ty))
269
- return { code: "None", ty: UNDEF };
270
- if (base.ty.k !== "opt")
271
- return fieldVal(comp, base.code, base.ty, n.property.name, n);
272
- const field = fieldVal(comp, "v", base.ty.of, n.property.name, n);
273
- const place = ["list", "record", "struct"].includes(field.ty.k) && /^v(?:\.[\w#]+)+$/.test(field.code);
274
- const f = place ? { ...field, code: `&${field.code}` } : field;
275
- if (f.ty.k === "opt")
276
- return { code: `${atom(base.code)}.and_then(|v| ${f.code})`, ty: opt(f.ty) };
277
- return { code: `${atom(base.code)}.map(|v| ${f.code})`, ty: opt(f.ty) };
278
- }
360
+ case "OptionalMemberExpression":
361
+ case "OptionalCallExpression":
362
+ return optionalChain(s, n);
279
363
  case "StringLiteral":
280
364
  return { code: rustStr(n.value), ty: STR };
281
365
  case "NumericLiteral":
@@ -295,6 +379,12 @@ export function expr(s, n) {
295
379
  }
296
380
  if (n.name === s.propsIdent)
297
381
  return { code: "props", ty: { k: "struct", name: "Props" } };
382
+ if (n.name === "Infinity" || n.name === "NaN")
383
+ return numberVal(Number(n.name), false);
384
+ if (n.name === "Intl")
385
+ return localeRefusal(s, "`Intl`", n);
386
+ if (BUILTINS.has(n.name))
387
+ return builtin(s, n.name, n);
298
388
  return fail(comp, "FV0601", `\`${n.name}\` is not available when rendering on the server`, n);
299
389
  }
300
390
  case "MemberExpression": {
@@ -305,13 +395,15 @@ export function expr(s, n) {
305
395
  if (n.object.type === "Identifier" && n.object.name === "$setup" && n.property.type === "StringLiteral") {
306
396
  return expr(s, { ...n.property, type: "Identifier", name: n.property.value });
307
397
  }
398
+ const base = expr(s, n.object);
308
399
  if (n.property.type === "StringLiteral") {
309
- const base = expr(s, n.object);
310
400
  const own = claim((p) => p.member?.(s, base, n.property.value, n, true));
311
401
  if (own)
312
402
  return own;
313
403
  }
314
- return fail(comp, "FV0612", "computed member access", n);
404
+ if (base.ty.k === "list")
405
+ return indexed(s, base, n.property, n);
406
+ return fail(comp, "FV0612", `computed member access of ${describeTy(base.ty)}: \`[…]\` reads an item of a list, by an integer index`, n);
315
407
  }
316
408
  const prop = n.property.name;
317
409
  const slots = slotsObject(s, n.object);
@@ -320,6 +412,12 @@ export function expr(s, n) {
320
412
  fail(comp, "FV0907", `\`${slots}.${prop}\` names a slot this template does not render`, n);
321
413
  return { code: `fv_slots.${snake(prop)}.is_some()`, ty: BOOL };
322
414
  }
415
+ if (n.object.type === "Identifier" && BUILTINS.has(n.object.name) && !s.locals.has(n.object.name) && !s.setup.has(n.object.name)) {
416
+ const value = n.object.name === "Math" ? MATH_CONSTANTS.get(prop) : n.object.name === "Number" ? NUMBER_CONSTANTS.get(prop) : undefined;
417
+ if (value !== undefined)
418
+ return numberVal(value, Number.isSafeInteger(value));
419
+ return builtin(s, `${n.object.name}.${prop}`, n);
420
+ }
323
421
  if (n.object.type === "Identifier") {
324
422
  switch (n.object.name) {
325
423
  case "$props":
@@ -365,7 +463,7 @@ export function expr(s, n) {
365
463
  }
366
464
  const a = expr(s, n.left);
367
465
  const b = expr(s, n.right);
368
- if (n.operator === "??") {
466
+ if (n.operator === "??" || (n.operator === "||" && (a.ty.k === "opt" ? a.ty.of : a.ty).k === "date")) {
369
467
  const own = claim((p) => p.values?.nullish?.(s, a, b, n));
370
468
  if (own)
371
469
  return own;
@@ -377,8 +475,10 @@ export function expr(s, n) {
377
475
  return { ...a, ty: withAbsence(a.ty.of, absence(b.ty)) };
378
476
  if (b.iter !== undefined)
379
477
  fail(comp, "FV0615", "`??` falling back to a computed list", n);
478
+ if (a.held !== undefined && b.ty.k === "list" && !untypedEmpty(b))
479
+ fail(comp, "FV0630", "`??` after an optional chain that computes a list, falling back to another list: fall back to `[]`", n);
380
480
  if (a.ty.of.k === "list" && b.ty.k === "list" && (untypedEmpty(b) || (sameTy(a.ty.of, b.ty) && !b.code.startsWith("[")))) {
381
- return { code: `${atom(a.code)}.map(|v| &v[..]).unwrap_or(${untypedEmpty(b) ? "&[]" : sliced(b)})`, ty: a.ty.of, slice: true, ...loneOf(a, b) };
481
+ return { code: `${atom(a.code)}${a.held !== undefined ? "" : ".map(|v| &v[..])"}.unwrap_or(${untypedEmpty(b) ? "&[]" : sliced(b)})`, ty: a.ty.of, slice: true, ...loneOf(a, b) };
382
482
  }
383
483
  if (a.held !== undefined && b.ty.k === "str")
384
484
  return { code: `&*${atom(a.held)}.unwrap_or(${asCow(b)})`, ty: STR, ...loneOf(a, b) };
@@ -393,7 +493,7 @@ export function expr(s, n) {
393
493
  return { code: `${atom(a.code)}.unwrap_or(${bare(asF64(b))})`, ty: FLOAT };
394
494
  if (a.ty.of.k === "int" && b.ty.k === "float")
395
495
  return { code: `${atom(a.code)}.map(|v| v as f64).unwrap_or(${bare(b.code)})`, ty: FLOAT };
396
- return fail(comp, "FV0616", "`??` between different types", n);
496
+ return n.operator === "??" ? fail(comp, "FV0616", "`??` between different types", n) : fail(comp, "FV0617", "`||` between these types", n);
397
497
  }
398
498
  if (n.operator === "||") {
399
499
  if (a.ty.k === "bool" && b.ty.k === "bool")
@@ -434,9 +534,18 @@ export function expr(s, n) {
434
534
  if (a.ty.k === "float")
435
535
  return { code: negate(a.code), ty: FLOAT };
436
536
  }
537
+ if (n.operator === "typeof")
538
+ return typeofVal(s, n);
437
539
  return fail(comp, "FV0618", `unary \`${n.operator}\``, n);
438
540
  case "BinaryExpression": {
541
+ const tested = typeofCompare(n);
542
+ if (tested)
543
+ return claim((p) => p.equality?.(s, n)) ?? typeofTest(s, tested, n);
439
544
  const test = nullTest(n);
545
+ if (n.operator !== "===" && n.operator !== "!==" && !test) {
546
+ const [a, b] = [expr(s, n.left), expr(s, n.right)];
547
+ claim((p) => p.values?.refuseEquality?.(s, a, b, n));
548
+ }
440
549
  if (n.operator !== "===" && n.operator !== "!==" && !test)
441
550
  fail(comp, "FV0619", `\`${n.operator}\`: \`==\` and \`!=\` compare with \`null\` or \`undefined\` only; use \`===\``, n);
442
551
  const whole = claim((p) => p.equality?.(s, n));
@@ -467,6 +576,7 @@ export function expr(s, n) {
467
576
  const strish = (t) => t.k === "str" || (t.k === "opt" && t.of.k === "str");
468
577
  if (strish(a.ty) && strish(b.ty))
469
578
  meet(comp, a, b, `\`${n.operator}\``, n, "equal");
579
+ claim((p) => p.values?.refuseEquality?.(s, a, b, n));
470
580
  const own = claim((p) => p.values?.equals?.(a, b));
471
581
  if (own !== undefined)
472
582
  eq = own;
@@ -583,6 +693,9 @@ export function expr(s, n) {
583
693
  case "TSSatisfiesExpression":
584
694
  return stated(s, n);
585
695
  case "NewExpression": {
696
+ const own = claim((p) => p.construct?.(s, n));
697
+ if (own)
698
+ return own;
586
699
  const made = n.callee.type === "Identifier" ? n.callee.name : n.callee.type === "MemberExpression" && !n.callee.computed ? `${n.callee.object.name ?? "…"}.${n.callee.property.name}` : "…";
587
700
  return fail(comp, "FV0624", `\`new ${made}(…)\` builds an object the server has no twin for: compute the value in Rust and pass it as a prop`, n);
588
701
  }
package/dist/fixture.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createApp, createSSRApp, createStaticVNode, createTextVNode, defineComponent, h } from "vue";
2
2
  import { routeRecords } from "./routes.js";
3
+ import { reviveDates } from "./client.js";
3
4
  export { routeRecords } from "./routes.js";
4
5
  /** A fixture's JSON, split into the props and the rest. */
5
6
  export function readFixture(json) {
@@ -64,7 +65,7 @@ export async function fixtureApp(component, fixture, routes, options = {}) {
64
65
  const slots = Object.fromEntries(Object.entries(fixture.slots)
65
66
  .filter(([name]) => name !== "routerView")
66
67
  .map(([name, html]) => [name, () => [staticNode(html)]]));
67
- const app = (options.client ? createApp : createSSRApp)({ render: () => h(component, fixture.props, slots) });
68
+ const app = (options.client ? createApp : createSSRApp)({ render: () => h(component, reviveDates(component, fixture.props), slots) });
68
69
  const head = await optionalHead(options.hydrate ?? false);
69
70
  if (head)
70
71
  app.use(head);
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { ClientOnly, hydrateState, mountIslands, type IslandComponent, type Islands, type MountOptions, type StateText } from "./client.ts";
1
+ export { ClientOnly, hydrateState, mountIslands, reviveDates, type IslandComponent, type Islands, type MountOptions, type StateText } from "./client.ts";
2
2
  export { createPageApp, mountPage, readPage, renderPage, type PageAppOptions, type PageOptions, type PagePart, type PageRecord, type ReadPage, type ReadPageOptions, type RenderPageOptions, } from "./client.ts";
3
3
  export type { Float, InlineHtml, TrustedHtml } from "./types.ts";
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { ClientOnly, hydrateState, mountIslands } from "./client.js";
1
+ export { ClientOnly, hydrateState, mountIslands, reviveDates } from "./client.js";
2
2
  export { createPageApp, mountPage, readPage, renderPage, } from "./client.js";
package/dist/lists.d.ts CHANGED
@@ -18,6 +18,15 @@ export declare function isBoolean(s: Scope, n: N): boolean;
18
18
  * be absent, which is an ordinary array literal.
19
19
  */
20
20
  export declare function presentLiteral(s: Scope, n: N): Val | null;
21
+ /**
22
+ * The item of `target` that `fromList` (given the list) or `fromIter` (given its items) picks, as
23
+ * an optional value: `undefined` where the list has no item there.
24
+ */
25
+ export declare function picked(s: Scope, target: Val, n: N, fromList: (list: string) => string, fromIter: (items: string) => string): Val;
26
+ /** An array literal holding spread lists (`[...a, x, ...b]`): the items of each part in turn, as a computed list. */
27
+ export declare function spreadLiteral(s: Scope, n: N): Val;
28
+ /** `Math.max(...xs)` and `Math.min(...xs)`: every number of the parts folded from `-Infinity` or `Infinity`, as JavaScript folds them. */
29
+ export declare function spreadExtreme(s: Scope, method: "max" | "min", args: N[]): Val;
21
30
  export declare function listMethod(s: Scope, target: Val, method: string, args: N[], n: N): Val | null;
22
31
  export declare function objectCall(s: Scope, method: string, args: N[], n: N): Val;
23
32
  export declare function computedListMethod(s: Scope, target: Val, method: string, args: N[], n: N): Val | null;