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/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: "
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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;
|