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/calls.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { type N, type Scope, type Val } from "./model.ts";
|
|
2
2
|
export declare function call(s: Scope, n: N): Val;
|
|
3
|
+
/** The globals of JavaScript a template may name, which the server translates only some of. */
|
|
4
|
+
export declare const BUILTINS: Set<string>;
|
|
5
|
+
/** Refuses `what`, a use of one of JavaScript's globals the server does not translate. */
|
|
6
|
+
export declare function builtin(s: Scope, what: string, n: N): never;
|
|
3
7
|
export declare function isObjectCall(n: N, method: string): boolean;
|
|
4
8
|
export declare function helperCall(s: Scope, name: string, args: N[], n: N): Val;
|
package/dist/calls.js
CHANGED
|
@@ -2,11 +2,16 @@ import { BOOL, fail, FLOAT, INT, sameTy, STR } from "./model.js";
|
|
|
2
2
|
import { ctx } from "./context.js";
|
|
3
3
|
import { claim } from "./plugin.js";
|
|
4
4
|
import { atom, bare, binary, condition, operand, receiver, strArg, UNARY } from "./parens.js";
|
|
5
|
-
import { computedListMethod, isBoolean, items, listMethod, objectCall, presentLiteral } from "./lists.js";
|
|
6
|
-
import { lonely, loneOf, meet, stringMethod, stringsEqual } from "./strings.js";
|
|
7
|
-
import { asF64, intFromF64, isNumber } from "./numbers.js";
|
|
5
|
+
import { computedListMethod, isBoolean, items, listMethod, objectCall, presentLiteral, spreadExtreme } from "./lists.js";
|
|
6
|
+
import { formatted, lonely, loneOf, meet, stringMethod, stringOf, stringsEqual } from "./strings.js";
|
|
7
|
+
import { asF64, intFromF64, isNumber, numberVal, power } from "./numbers.js";
|
|
8
8
|
import { boolOf, truthy } from "./narrowing.js";
|
|
9
|
-
import { coerce, expr } from "./expr.js";
|
|
9
|
+
import { coerce, describeTy, expr } from "./expr.js";
|
|
10
|
+
const DIGITS = new Map([
|
|
11
|
+
["toFixed", ["FV0705", 0, "number of digits"]],
|
|
12
|
+
["toPrecision", ["FV0716", 1, "precision"]],
|
|
13
|
+
["toExponential", ["FV0717", 0, "number of digits"]],
|
|
14
|
+
]);
|
|
10
15
|
export function call(s, n) {
|
|
11
16
|
const comp = s.comp;
|
|
12
17
|
const callee = n.callee;
|
|
@@ -43,7 +48,10 @@ export function call(s, n) {
|
|
|
43
48
|
return helperCall(s, helper, args, n);
|
|
44
49
|
if (isBoolean(s, callee) && args.length === 1)
|
|
45
50
|
return boolOf(truthy(expr(s, args[0])));
|
|
46
|
-
|
|
51
|
+
const global = !s.locals.has(callee.name) && !s.setup.has(callee.name);
|
|
52
|
+
if (callee.name === "String" && global) {
|
|
53
|
+
if (args.length !== 1)
|
|
54
|
+
return fail(comp, "FV0723", "`String()` takes one value", n);
|
|
47
55
|
const a = expr(s, args[0]);
|
|
48
56
|
if (a.ty.k === "str")
|
|
49
57
|
return a;
|
|
@@ -51,6 +59,17 @@ export function call(s, n) {
|
|
|
51
59
|
return { code: `&*fv::Js(${bare(a.code)}).to_string()`, ty: STR };
|
|
52
60
|
if (a.ty.k === "bool")
|
|
53
61
|
return { code: `if ${condition(a.code)} { "true" } else { "false" }`, ty: STR };
|
|
62
|
+
return formatted(comp, [stringOf(comp, a, args[0], "`String()`", "FV0723")], n, "`String()`");
|
|
63
|
+
}
|
|
64
|
+
if ((callee.name === "encodeURIComponent" || callee.name === "encodeURI") && global) {
|
|
65
|
+
const name = `\`${callee.name}()\``;
|
|
66
|
+
if (args.length !== 1)
|
|
67
|
+
return fail(comp, "FV0723", `${name} takes one value`, n);
|
|
68
|
+
const a = expr(s, args[0]);
|
|
69
|
+
if (a.lone)
|
|
70
|
+
fail(comp, "FV0706", `${lonely(name)}; it throws a \`URIError\` on a half`, n);
|
|
71
|
+
const text = a.ty.k === "str" ? a : formatted(comp, [stringOf(comp, a, args[0], name, "FV0723")], n, name);
|
|
72
|
+
return { code: `&*fv::js_${callee.name === "encodeURI" ? "encode_uri" : "encode_uri_component"}(${strArg(text.code)})`, ty: STR };
|
|
54
73
|
}
|
|
55
74
|
if (callee.name === "Number" && args.length === 1) {
|
|
56
75
|
const a = expr(s, args[0]);
|
|
@@ -74,6 +93,10 @@ export function call(s, n) {
|
|
|
74
93
|
}
|
|
75
94
|
return { code: `fv::js_parse_int(${strArg(a.code)}, ${radix ? radix.value : 0})`, ty: FLOAT };
|
|
76
95
|
}
|
|
96
|
+
if ((callee.name === "isNaN" || callee.name === "isFinite") && global)
|
|
97
|
+
return numberTest(s, callee.name, args, n, true);
|
|
98
|
+
if (global && BUILTINS.has(callee.name))
|
|
99
|
+
return builtin(s, `${callee.name}()`, n);
|
|
77
100
|
return fail(comp, "FV0601", `\`${callee.name}()\` is not available when rendering on the server`, n);
|
|
78
101
|
}
|
|
79
102
|
if (callee.type === "MemberExpression" && !callee.computed) {
|
|
@@ -88,12 +111,17 @@ export function call(s, n) {
|
|
|
88
111
|
}
|
|
89
112
|
if (callee.object.type === "Identifier" && callee.object.name === "Object")
|
|
90
113
|
return objectCall(s, method, args, n);
|
|
114
|
+
if (callee.object.type === "Identifier" && callee.object.name === "Number" && NUMBER_TESTS.has(method))
|
|
115
|
+
return numberTest(s, method, args, n, false);
|
|
91
116
|
if (callee.object.type === "Identifier" && callee.object.name === "JSON" && method === "stringify" && args.length === 1) {
|
|
92
117
|
const v = expr(s, args[0]);
|
|
93
118
|
if (v.ty.k === "bool")
|
|
94
119
|
return { code: `if ${condition(v.code)} { "true" } else { "false" }`, ty: STR };
|
|
95
120
|
return { code: `&*${json(s, v, args[0])}`, ty: STR };
|
|
96
121
|
}
|
|
122
|
+
if (callee.object.type === "Identifier" && callee.object.name === "JSON" && method === "stringify") {
|
|
123
|
+
return fail(comp, "FV0633", "`JSON.stringify()` with a replacer or an indentation, which the server does not write: give it one value, `JSON.stringify(x)`", n);
|
|
124
|
+
}
|
|
97
125
|
const own = claim((p) => p.call?.(s, n));
|
|
98
126
|
if (own)
|
|
99
127
|
return own;
|
|
@@ -101,8 +129,17 @@ export function call(s, n) {
|
|
|
101
129
|
const helper = s.helpers.get(method);
|
|
102
130
|
if (helper)
|
|
103
131
|
return helperCall(s, helper, args, n);
|
|
132
|
+
if (s.clientOnly.has(method))
|
|
133
|
+
return fail(comp, "FV0613", `\`${method}\` is set up in a way the server cannot evaluate: ${s.clientOnly.get(method)}`, n);
|
|
104
134
|
}
|
|
105
135
|
if (callee.object.type === "Identifier" && callee.object.name === "Math") {
|
|
136
|
+
if ((method === "max" || method === "min") && args.some((a) => a?.type === "SpreadElement"))
|
|
137
|
+
return spreadExtreme(s, method, args);
|
|
138
|
+
if (method === "pow" && args.length === 2)
|
|
139
|
+
return power(s, args[0], args[1], n, "`Math.pow()`");
|
|
140
|
+
if (ROUNDED_BY_EACH.has(method)) {
|
|
141
|
+
return fail(comp, "FV0719", `\`Math.${method}()\` is computed by each JavaScript engine's own routine, which need not round its result as another engine or Rust does, so the page could show digits the browser's render does not: compute it in Rust and pass it as a prop`, n);
|
|
142
|
+
}
|
|
106
143
|
const vals = args.map((a) => expr(s, a));
|
|
107
144
|
if (vals.length && vals.every((v) => isNumber(v.ty))) {
|
|
108
145
|
const ints = vals.every((v) => v.ty.k === "int");
|
|
@@ -119,12 +156,31 @@ export function call(s, n) {
|
|
|
119
156
|
return { code: `fv::js_round(${bare(asF64(x))})`, ty: FLOAT };
|
|
120
157
|
if (method === "floor" || method === "ceil" || method === "trunc")
|
|
121
158
|
return { code: `${receiver(asF64(x))}.${method}()`, ty: FLOAT };
|
|
159
|
+
if (method === "sqrt")
|
|
160
|
+
return x.num !== undefined ? numberVal(Math.sqrt(x.num), false) : { code: `${receiver(asF64(x))}.sqrt()`, ty: FLOAT };
|
|
161
|
+
if (method === "sign") {
|
|
162
|
+
if (x.num !== undefined)
|
|
163
|
+
return numberVal(Math.sign(x.num), ints);
|
|
164
|
+
const sign = `fv::js_sign(${bare(asF64(x))})`;
|
|
165
|
+
return ints ? intFromF64(sign) : { code: sign, ty: FLOAT };
|
|
166
|
+
}
|
|
122
167
|
}
|
|
123
168
|
}
|
|
124
|
-
return fail(comp, "FV0704", `\`Math.${method}()\` is supported on numbers as \`max\`, \`min\`, \`abs\`, \`round\`, \`floor\`, \`ceil\` and \`
|
|
169
|
+
return fail(comp, "FV0704", `\`Math.${method}()\` is supported on numbers as \`max\`, \`min\`, \`abs\`, \`round\`, \`floor\`, \`ceil\`, \`trunc\`, \`sqrt\`, \`sign\` and \`pow\``, n);
|
|
170
|
+
}
|
|
171
|
+
if (callee.object.type === "Identifier" && BUILTINS.has(callee.object.name) && !s.locals.has(callee.object.name) && !s.setup.has(callee.object.name)) {
|
|
172
|
+
return builtin(s, `${callee.object.name}.${method}()`, n);
|
|
173
|
+
}
|
|
174
|
+
if ((method === "sort" || method === "reverse") && callee.object.type === "ArrayExpression") {
|
|
175
|
+
const copy = expr(s, callee.object);
|
|
176
|
+
if (copy.ty.k === "list")
|
|
177
|
+
return listMethod(s, copy, method === "sort" ? "toSorted" : "toReversed", args, n);
|
|
125
178
|
}
|
|
126
179
|
const literal = method === "filter" && args.length === 1 && isBoolean(s, args[0]) && callee.object.type === "ArrayExpression";
|
|
127
180
|
const target = (literal ? presentLiteral(s, callee.object) : null) ?? expr(s, callee.object);
|
|
181
|
+
const typed = claim((p) => p.method?.(s, target, method, args, n));
|
|
182
|
+
if (typed)
|
|
183
|
+
return typed;
|
|
128
184
|
if (target.ty.k === "str") {
|
|
129
185
|
const lone = loneOf(target);
|
|
130
186
|
switch (args.length === 0 ? method : "") {
|
|
@@ -133,7 +189,10 @@ export function call(s, n) {
|
|
|
133
189
|
case "trimStart":
|
|
134
190
|
return { code: `fv::js_trim_start(${strArg(target.code)})`, ty: STR, ...lone };
|
|
135
191
|
case "trimEnd":
|
|
192
|
+
case "trimRight":
|
|
136
193
|
return { code: `fv::js_trim_end(${strArg(target.code)})`, ty: STR, ...lone };
|
|
194
|
+
case "trimLeft":
|
|
195
|
+
return { code: `fv::js_trim_start(${strArg(target.code)})`, ty: STR, ...lone };
|
|
137
196
|
case "toUpperCase":
|
|
138
197
|
return { code: `&*${receiver(target.code)}.to_uppercase()`, ty: STR, ...lone };
|
|
139
198
|
case "toLowerCase":
|
|
@@ -145,12 +204,21 @@ export function call(s, n) {
|
|
|
145
204
|
if (m)
|
|
146
205
|
return m;
|
|
147
206
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
207
|
+
const digits = isNumber(target.ty) && args.length <= 1 ? DIGITS.get(method) : undefined;
|
|
208
|
+
if (digits) {
|
|
209
|
+
const [code, low, what] = digits;
|
|
210
|
+
const a = args[0];
|
|
211
|
+
const given = a === undefined || (a.type === "Identifier" && a.name === "undefined") ? undefined : a.type === "NumericLiteral" ? Math.trunc(a.value) : NaN;
|
|
212
|
+
if (given !== undefined && !(given >= low && given <= 100))
|
|
213
|
+
fail(comp, code, `\`.${method}()\` takes a literal ${what}, from ${low} to 100`, n);
|
|
214
|
+
const x = bare(asF64(target));
|
|
215
|
+
if (method === "toFixed")
|
|
216
|
+
return { code: `&*fv::js_to_fixed(${x}, ${given ?? 0})`, ty: STR };
|
|
217
|
+
if (method === "toExponential")
|
|
218
|
+
return { code: `&*fv::js_to_exponential(${x}, ${given === undefined ? "None" : `Some(${given})`})`, ty: STR };
|
|
219
|
+
if (given === undefined)
|
|
220
|
+
return { code: `&*fv::Js(${bare(target.code)}).to_string()`, ty: STR };
|
|
221
|
+
return { code: `&*fv::js_to_precision(${x}, ${given})`, ty: STR };
|
|
154
222
|
}
|
|
155
223
|
if (isNumber(target.ty) && method === "toString" && args.length === 0) {
|
|
156
224
|
return { code: `&*fv::Js(${bare(target.code)}).to_string()`, ty: STR };
|
|
@@ -186,6 +254,71 @@ export function call(s, n) {
|
|
|
186
254
|
}
|
|
187
255
|
return fail(comp, "FV0603", "this call is not supported", n);
|
|
188
256
|
}
|
|
257
|
+
const NUMBER_TESTS = new Set(["isNaN", "isFinite", "isInteger", "isSafeInteger"]);
|
|
258
|
+
const ROUNDED_BY_EACH = new Set("acos acosh asin asinh atan atan2 atanh cbrt cos cosh exp expm1 hypot log log10 log1p log2 sin sinh tan tanh".split(" "));
|
|
259
|
+
/** The globals of JavaScript a template may name, which the server translates only some of. */
|
|
260
|
+
export const BUILTINS = new Set("Array BigInt Boolean JSON Map Math Number Object RegExp Set String Symbol console decodeURI decodeURIComponent encodeURI encodeURIComponent isFinite isNaN parseFloat parseInt".split(" "));
|
|
261
|
+
const OF_BUILTINS = {
|
|
262
|
+
Array: "`Array.isArray(x)`",
|
|
263
|
+
Boolean: "`Boolean(x)` and `.filter(Boolean)`",
|
|
264
|
+
JSON: "`JSON.stringify(x)`",
|
|
265
|
+
Math: "`Math.max`, `min`, `abs`, `round`, `floor`, `ceil`, `trunc`, `sqrt`, `sign` and `pow`, and its constants",
|
|
266
|
+
Number: "`Number(x)`, `Number.isNaN`, `isFinite`, `isInteger` and `isSafeInteger`, and its constants",
|
|
267
|
+
Object: "`Object.keys`, `Object.values` and `Object.entries` of a record",
|
|
268
|
+
String: "`String(x)`",
|
|
269
|
+
encodeURI: "`encodeURI(s)`",
|
|
270
|
+
encodeURIComponent: "`encodeURIComponent(s)`",
|
|
271
|
+
isFinite: "`isFinite(n)`",
|
|
272
|
+
isNaN: "`isNaN(n)`",
|
|
273
|
+
parseFloat: "`parseFloat(s)`",
|
|
274
|
+
parseInt: "`parseInt(s)` and `parseInt(s, radix)`",
|
|
275
|
+
};
|
|
276
|
+
/** Refuses `what`, a use of one of JavaScript's globals the server does not translate. */
|
|
277
|
+
export function builtin(s, what, n) {
|
|
278
|
+
const name = /^\w+/.exec(what)[0];
|
|
279
|
+
const of = OF_BUILTINS[name];
|
|
280
|
+
return fail(s.comp, "FV0633", `\`${what}\` is not supported: ${of ? `of \`${name}\`, the server translates ${of}` : `the server does not translate \`${name}\``}`, n);
|
|
281
|
+
}
|
|
282
|
+
function floatTest(method, x) {
|
|
283
|
+
switch (method) {
|
|
284
|
+
case "isNaN":
|
|
285
|
+
return `${receiver(x)}.is_nan()`;
|
|
286
|
+
case "isFinite":
|
|
287
|
+
return `${receiver(x)}.is_finite()`;
|
|
288
|
+
case "isInteger":
|
|
289
|
+
return `${receiver(x)}.fract() == 0.0`;
|
|
290
|
+
default:
|
|
291
|
+
return /^[\w.]+$/.test(x) ? `${x}.fract() == 0.0 && ${x}.abs() <= 9_007_199_254_740_991.0` : `matches!(${bare(x)}, v if v.fract() == 0.0 && v.abs() <= 9_007_199_254_740_991.0)`;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function intTest(method, x) {
|
|
295
|
+
if (method === "isSafeInteger")
|
|
296
|
+
return `(-9_007_199_254_740_991..=9_007_199_254_740_991).contains(&${operand(x, UNARY)})`;
|
|
297
|
+
return String(method !== "isNaN");
|
|
298
|
+
}
|
|
299
|
+
function numberTest(s, method, args, n, global) {
|
|
300
|
+
const name = global ? `\`${method}()\`` : `\`Number.${method}()\``;
|
|
301
|
+
if (args.length !== 1)
|
|
302
|
+
return fail(s.comp, "FV0718", `${name} takes one number`, n);
|
|
303
|
+
const v = expr(s, args[0]);
|
|
304
|
+
if (isNumber(v.ty)) {
|
|
305
|
+
if (v.num !== undefined) {
|
|
306
|
+
const konst = Number[method](v.num);
|
|
307
|
+
return { code: String(konst), ty: BOOL, konst };
|
|
308
|
+
}
|
|
309
|
+
return boolOf(v.ty.k === "int" ? intTest(method, v.code) : floatTest(method, v.code));
|
|
310
|
+
}
|
|
311
|
+
if (!global && v.ty.k === "opt" && isNumber(v.ty.of)) {
|
|
312
|
+
const each = v.ty.of.k === "int" ? intTest(method, "v") : floatTest(method, "v");
|
|
313
|
+
if (each === "false")
|
|
314
|
+
return { code: "false", ty: BOOL, konst: false };
|
|
315
|
+
return boolOf(each === "true" ? `${atom(v.code)}.is_some()` : `${atom(v.code)}.is_some_and(|v| ${each})`);
|
|
316
|
+
}
|
|
317
|
+
if (global) {
|
|
318
|
+
return fail(s.comp, "FV0718", `${name} converts its argument as \`Number()\` does, which the server does for a number only: write \`Number.${method}(Number(x))\` for a string, or pass a present number`, args[0]);
|
|
319
|
+
}
|
|
320
|
+
return fail(s.comp, "FV0718", `${name} of ${describeTy(v.ty)}, for which it is \`false\` whatever it holds, since it does not convert its argument: pass a number, present or optional`, args[0]);
|
|
321
|
+
}
|
|
189
322
|
export function isObjectCall(n, method) {
|
|
190
323
|
return (n?.type === "CallExpression" && n.callee.type === "MemberExpression" && !n.callee.computed &&
|
|
191
324
|
n.callee.object.type === "Identifier" && n.callee.object.name === "Object" && n.callee.property.name === method);
|
|
@@ -204,7 +337,7 @@ function json(s, v, n) {
|
|
|
204
337
|
case "bool":
|
|
205
338
|
return `(if ${condition(code)} { "true" } else { "false" }).to_owned()`;
|
|
206
339
|
default:
|
|
207
|
-
return fail(s.comp, "FV0707", "`JSON.stringify()` of a string, a number, a boolean or a list of those, present", n);
|
|
340
|
+
return claim((p) => p.values?.json?.({ code, ty })) ?? fail(s.comp, "FV0707", "`JSON.stringify()` of a string, a number, a boolean, a date or a list of those, present", n);
|
|
208
341
|
}
|
|
209
342
|
};
|
|
210
343
|
if (v.ty.k === "list") {
|
package/dist/children.js
CHANGED
|
@@ -321,6 +321,14 @@ export function ownInto(comp, v, want, node) {
|
|
|
321
321
|
if (v.ty.k === "opt" && v.ty.of.k === "str")
|
|
322
322
|
return `${atom(v.code)}.map(std::borrow::Cow::Borrowed)`;
|
|
323
323
|
}
|
|
324
|
+
if (want.k === "date" && v.ty.k === "date")
|
|
325
|
+
return `fv::js_date_prop(${bare(v.code)})`;
|
|
326
|
+
if (want.k === "opt" && want.of.k === "date") {
|
|
327
|
+
if (v.ty.k === "date")
|
|
328
|
+
return `Some(fv::js_date_prop(${bare(v.code)}))`;
|
|
329
|
+
if (v.ty.k === "opt" && v.ty.of.k === "date")
|
|
330
|
+
return `${atom(v.code)}.map(fv::js_date_prop)`;
|
|
331
|
+
}
|
|
324
332
|
if (sameTy(v.ty, want) && (want.k === "int" || want.k === "float" || want.k === "bool"))
|
|
325
333
|
return v.code;
|
|
326
334
|
if (want.k === "opt" && sameTy(v.ty, want.of) && (v.ty.k === "int" || v.ty.k === "float" || v.ty.k === "bool"))
|
package/dist/client.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { type App, type Component, type Plugin } from "vue";
|
|
2
2
|
import type { Pinia } from "pinia";
|
|
3
3
|
import type { Router } from "vue-router";
|
|
4
|
+
/** The props a component is given, with each prop it declares a `Date` turned from what JSON
|
|
5
|
+
* carries, the ISO string `toISOString()` writes or a number of milliseconds (`NaN` for an invalid
|
|
6
|
+
* date), back into a `Date`, as the server rendered it. Vue's compiler declares a prop typed `Date`
|
|
7
|
+
* as `{ type: Date }`, and one typed `Date | null` as `{ type: [Date, null] }`. `mountIslands`,
|
|
8
|
+
* `mountPage` and `renderPage` call it; an app that hands a component props it read from JSON
|
|
9
|
+
* itself calls it too. */
|
|
10
|
+
export declare function reviveDates<P extends Record<string, unknown> | null>(component: Component, props: P): P;
|
|
4
11
|
/** The state of a page already read: the text of its state's script, as `readPage` gives it. */
|
|
5
12
|
export interface StateText {
|
|
6
13
|
text: string | undefined;
|
package/dist/client.js
CHANGED
|
@@ -11,6 +11,28 @@ function parseJson(text) {
|
|
|
11
11
|
const quoted = text.replace(BARE, (match) => (match.startsWith('"') ? match : `"${escaped}${match}"`));
|
|
12
12
|
return JSON.parse(quoted, (_key, value) => (typeof value === "string" && value.startsWith(tag) ? NON_FINITE[value.slice(tag.length)] : value));
|
|
13
13
|
}
|
|
14
|
+
function declaresDate(declared) {
|
|
15
|
+
const type = declared !== null && typeof declared === "object" && "type" in declared ? declared.type : declared;
|
|
16
|
+
return type === Date || (Array.isArray(type) && type.includes(Date));
|
|
17
|
+
}
|
|
18
|
+
/** The props a component is given, with each prop it declares a `Date` turned from what JSON
|
|
19
|
+
* carries, the ISO string `toISOString()` writes or a number of milliseconds (`NaN` for an invalid
|
|
20
|
+
* date), back into a `Date`, as the server rendered it. Vue's compiler declares a prop typed `Date`
|
|
21
|
+
* as `{ type: Date }`, and one typed `Date | null` as `{ type: [Date, null] }`. `mountIslands`,
|
|
22
|
+
* `mountPage` and `renderPage` call it; an app that hands a component props it read from JSON
|
|
23
|
+
* itself calls it too. */
|
|
24
|
+
export function reviveDates(component, props) {
|
|
25
|
+
const declared = component.props;
|
|
26
|
+
if (!props || !declared || typeof declared !== "object" || Array.isArray(declared))
|
|
27
|
+
return props;
|
|
28
|
+
let revived = null;
|
|
29
|
+
for (const [key, type] of Object.entries(declared)) {
|
|
30
|
+
const value = props[key];
|
|
31
|
+
if ((typeof value === "string" || typeof value === "number") && declaresDate(type))
|
|
32
|
+
(revived ??= { ...props })[key] = new Date(value);
|
|
33
|
+
}
|
|
34
|
+
return (revived ?? props);
|
|
35
|
+
}
|
|
14
36
|
/** Give Pinia the state the server rendered with (what `ferrovue::state_script_into` wrote) before
|
|
15
37
|
* the app mounts, so every store starts from it and the markup agrees with the server's. The state
|
|
16
38
|
* is read from the script with the `id` given (`__pinia` by default) in `doc`, or given as
|
|
@@ -303,7 +325,7 @@ export async function mountIslands(components, options = {}) {
|
|
|
303
325
|
// One island that throws as it mounts (its setup, a plugin) is reported, and leaves the others
|
|
304
326
|
// to hydrate.
|
|
305
327
|
try {
|
|
306
|
-
const app = createSSRApp(component, props);
|
|
328
|
+
const app = createSSRApp(component, reviveDates(component, props));
|
|
307
329
|
if (options.pinia)
|
|
308
330
|
app.use(options.pinia);
|
|
309
331
|
if (options.router)
|
|
@@ -415,9 +437,9 @@ async function load(component) {
|
|
|
415
437
|
export function createPageApp(layout, record, components, options = {}) {
|
|
416
438
|
const slots = Object.fromEntries(Object.entries(record.slots).map(([name, parts]) => [
|
|
417
439
|
name,
|
|
418
|
-
() => (parts.length ? parts.map((part) => h(components[part.c], part.p)) : [createTextVNode("")]),
|
|
440
|
+
() => (parts.length ? parts.map((part) => h(components[part.c], reviveDates(components[part.c], part.p))) : [createTextVNode("")]),
|
|
419
441
|
]));
|
|
420
|
-
const app = (options.hydrate === false ? createApp : createSSRApp)({ name: "FerrovuePage", render: () => h(layout, record.props, slots) });
|
|
442
|
+
const app = (options.hydrate === false ? createApp : createSSRApp)({ name: "FerrovuePage", render: () => h(layout, reviveDates(layout, record.props), slots) });
|
|
421
443
|
if (options.pinia)
|
|
422
444
|
app.use(options.pinia);
|
|
423
445
|
if (options.router)
|
package/dist/compiler.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, join, resolve } from "node:path";
|
|
3
3
|
import { relativePath } from "./paths.js";
|
|
4
|
-
import { ctx, loadConfig, tyOfName } from "./context.js";
|
|
4
|
+
import { ctx, JS_DATE, loadConfig, tyOfName } from "./context.js";
|
|
5
5
|
import { failIn, snake } from "./model.js";
|
|
6
6
|
import { readComponent, refuseOptionsApi } from "./component.js";
|
|
7
7
|
import { listDir } from "./files.js";
|
|
@@ -9,7 +9,7 @@ import { scopeFor } from "./script.js";
|
|
|
9
9
|
import { attrsFlow } from "./fallthrough.js";
|
|
10
10
|
import { prepareDynamic } from "./dynamic.js";
|
|
11
11
|
import { componentSource, GENERATED, isIsland, modSource } from "./rust.js";
|
|
12
|
-
import { requireWhole } from "./typescript.js";
|
|
12
|
+
import { refuseNestedDates, requireWhole } from "./typescript.js";
|
|
13
13
|
import { renderParams } from "./plugin.js";
|
|
14
14
|
import { PLUGINS } from "./plugins/index.js";
|
|
15
15
|
/** Every generated file, keyed by its name in the output directory. `sources` holds the text of
|
|
@@ -19,6 +19,7 @@ export function generate(root, config = loadConfig(root), sources = new Map()) {
|
|
|
19
19
|
ctx.componentsDir = config.components.replace(/\/?$/, "/");
|
|
20
20
|
ctx.helperModule = config.helpers?.module ?? null;
|
|
21
21
|
ctx.trustedHtml = config.trustedHtml ?? null;
|
|
22
|
+
ctx.dateType = config.dateType ?? JS_DATE;
|
|
22
23
|
ctx.builders = config.builders ?? true;
|
|
23
24
|
ctx.clientDirectives = new Set(config.clientDirectives ?? []);
|
|
24
25
|
ctx.rootDir = root;
|
|
@@ -74,6 +75,8 @@ export function generate(root, config = loadConfig(root), sources = new Map()) {
|
|
|
74
75
|
for (const c of components.values())
|
|
75
76
|
for (const f of c.props.fields)
|
|
76
77
|
requireWhole(c, f.ty);
|
|
78
|
+
for (const c of components.values())
|
|
79
|
+
refuseNestedDates(c, c.props.fields, c.propsAt);
|
|
77
80
|
const scopes = read.map((r) => scopeFor(r.comp, r.ast, components).scope);
|
|
78
81
|
const analysed = prepareDynamic(read.map((r, i) => ({ ...r, scope: scopes[i] })));
|
|
79
82
|
const all = read.map((r, i) => ({ comp: r.comp, ssr: analysed[i], children: scopes[i].children, attrsBindings: scopes[i].attrsBindings }));
|
package/dist/component.js
CHANGED
|
@@ -213,10 +213,10 @@ export function readComponent(file, root, name = basename(file, ".vue")) {
|
|
|
213
213
|
if (!propsTy) {
|
|
214
214
|
}
|
|
215
215
|
else if (propsTy.type === "TSTypeLiteral") {
|
|
216
|
-
comp.props = structOf(comp, "Props", propsTy.members, comp.structs);
|
|
216
|
+
comp.props = structOf(comp, "Props", propsTy.members, comp.structs, false, true);
|
|
217
217
|
}
|
|
218
218
|
else if (propsTy.type === "TSTypeReference" && comp.structs.has(propsTy.typeName.name)) {
|
|
219
|
-
reachLocal(comp, propsTy.typeName.name);
|
|
219
|
+
reachLocal(comp, propsTy.typeName.name, true);
|
|
220
220
|
const st = comp.structs.get(propsTy.typeName.name);
|
|
221
221
|
const refused = st.refused?.values().next().value;
|
|
222
222
|
if (refused !== undefined)
|
|
@@ -246,6 +246,7 @@ export function readComponent(file, root, name = basename(file, ".vue")) {
|
|
|
246
246
|
comp.models.set(d.id.name, named);
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
|
+
comp.propsAt = propsTy;
|
|
249
250
|
const defaults = runtimeDefaults(comp, script?.content ?? "");
|
|
250
251
|
for (const f of comp.props.fields) {
|
|
251
252
|
if (f.ty.k !== "opt")
|
package/dist/context.d.ts
CHANGED
|
@@ -49,6 +49,10 @@ export interface Config {
|
|
|
49
49
|
* `crate::render::Trusted<'a>`. `ferrovue::InlineHtml` makes every `TrustedHtml` prop inline HTML.
|
|
50
50
|
* Without it, only an `InlineHtml` prop may reach `v-html`. */
|
|
51
51
|
trustedHtml?: string;
|
|
52
|
+
/** The Rust type a `Date` prop is, which must implement `ferrovue::JsTime`:
|
|
53
|
+
* `ferrovue::JsDate` by default, or `chrono::DateTime<chrono::Utc>` with the crate's `chrono`
|
|
54
|
+
* feature. */
|
|
55
|
+
dateType?: string;
|
|
52
56
|
/** A JSON file listing the app's routes as vue-router paths (`/users/:id`), which `<RouterLink>`
|
|
53
57
|
* resolves against, or `{ pages }`: a folder of pages whose file names give the routes, as
|
|
54
58
|
* vue-router's file-based routing reads them. Without it, a component cannot use `<RouterLink>`. */
|
|
@@ -126,6 +130,8 @@ export declare function checkConfig(raw: unknown, file: string, warn: (warning:
|
|
|
126
130
|
export declare function tyOfName(name: TypeName): Ty;
|
|
127
131
|
/** The Rust type of an `InlineHtml` prop, and of a `TrustedHtml` one when `trustedHtml` names it. */
|
|
128
132
|
export declare const INLINE_HTML = "ferrovue::InlineHtml";
|
|
133
|
+
/** The Rust type of a `Date` prop when `dateType` names none. */
|
|
134
|
+
export declare const JS_DATE = "ferrovue::JsDate";
|
|
129
135
|
/** Where `TrustedHtml` comes from. Only that import names the type `v-html` will write raw. */
|
|
130
136
|
export declare const TYPES_MODULE = "ferrovue/types";
|
|
131
137
|
export declare const ctx: {
|
|
@@ -137,6 +143,7 @@ export declare const ctx: {
|
|
|
137
143
|
}>;
|
|
138
144
|
helperModule: string | null;
|
|
139
145
|
trustedHtml: string | null;
|
|
146
|
+
dateType: string;
|
|
140
147
|
builders: boolean;
|
|
141
148
|
clientDirectives: Set<string>;
|
|
142
149
|
plugins: readonly Plugin[];
|
package/dist/context.js
CHANGED
|
@@ -100,7 +100,7 @@ export function checkConfig(raw, file, warn, schema = CONFIG_SCHEMA) {
|
|
|
100
100
|
if (raw.builders !== undefined && typeof raw.builders !== "boolean") {
|
|
101
101
|
throw new GenError("FV1106", `\`builders\` in ${file} is \`true\` or \`false\`: whether each props struct gets \`new()\` and a setter per optional field`, { file });
|
|
102
102
|
}
|
|
103
|
-
for (const key of ["$schema", "stores", "trustedHtml", "viteRoot"])
|
|
103
|
+
for (const key of ["$schema", "stores", "trustedHtml", "dateType", "viteRoot"])
|
|
104
104
|
string(raw, key, key);
|
|
105
105
|
if (raw.isProduction !== undefined && typeof raw.isProduction !== "boolean")
|
|
106
106
|
wrong("isProduction", "`true` or `false`: whether `@vitejs/plugin-vue` compiles for production");
|
|
@@ -216,12 +216,15 @@ export function tyOfName(name) {
|
|
|
216
216
|
}
|
|
217
217
|
/** The Rust type of an `InlineHtml` prop, and of a `TrustedHtml` one when `trustedHtml` names it. */
|
|
218
218
|
export const INLINE_HTML = "ferrovue::InlineHtml";
|
|
219
|
+
/** The Rust type of a `Date` prop when `dateType` names none. */
|
|
220
|
+
export const JS_DATE = "ferrovue::JsDate";
|
|
219
221
|
/** Where `TrustedHtml` comes from. Only that import names the type `v-html` will write raw. */
|
|
220
222
|
export const TYPES_MODULE = "ferrovue/types";
|
|
221
223
|
export const ctx = {
|
|
222
224
|
helpers: {},
|
|
223
225
|
helperModule: null,
|
|
224
226
|
trustedHtml: null,
|
|
227
|
+
dateType: JS_DATE,
|
|
225
228
|
builders: true,
|
|
226
229
|
clientDirectives: new Set(),
|
|
227
230
|
plugins: [],
|
package/dist/errors.d.ts
CHANGED
|
@@ -325,7 +325,7 @@ export declare const ERRORS: {
|
|
|
325
325
|
readonly title: "Unsupported call";
|
|
326
326
|
};
|
|
327
327
|
readonly FV0604: {
|
|
328
|
-
readonly title: "Field read on a value that is not an object";
|
|
328
|
+
readonly title: "Field read on a value that is not an object, or that may be absent";
|
|
329
329
|
};
|
|
330
330
|
readonly FV0605: {
|
|
331
331
|
readonly title: "Field the type does not have";
|
|
@@ -343,7 +343,7 @@ export declare const ERRORS: {
|
|
|
343
343
|
readonly title: "Comparison between unsupported or absent values";
|
|
344
344
|
};
|
|
345
345
|
readonly FV0610: {
|
|
346
|
-
readonly title: "
|
|
346
|
+
readonly title: "Hole in an array literal";
|
|
347
347
|
};
|
|
348
348
|
readonly FV0611: {
|
|
349
349
|
readonly title: "Array literal of different types";
|
|
@@ -399,6 +399,23 @@ export declare const ERRORS: {
|
|
|
399
399
|
readonly FV0628: {
|
|
400
400
|
readonly title: "`as` or `satisfies` stating a type the value does not have";
|
|
401
401
|
};
|
|
402
|
+
readonly FV0629: {
|
|
403
|
+
readonly title: "Optional call `?.()`";
|
|
404
|
+
};
|
|
405
|
+
readonly FV0630: {
|
|
406
|
+
readonly title: "`??` between a list an optional chain computes and another list";
|
|
407
|
+
};
|
|
408
|
+
readonly FV0631: {
|
|
409
|
+
readonly title: "`in` or `instanceof`";
|
|
410
|
+
readonly 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)`.";
|
|
411
|
+
};
|
|
412
|
+
readonly FV0632: {
|
|
413
|
+
readonly title: "Bitwise or shift operator";
|
|
414
|
+
};
|
|
415
|
+
readonly FV0633: {
|
|
416
|
+
readonly title: "Part of JavaScript's own globals the server does not translate";
|
|
417
|
+
readonly 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.";
|
|
418
|
+
};
|
|
402
419
|
readonly FV0701: {
|
|
403
420
|
readonly title: "`Number()` of an unsupported value";
|
|
404
421
|
};
|
|
@@ -415,14 +432,14 @@ export declare const ERRORS: {
|
|
|
415
432
|
readonly title: "`.toFixed()` without a literal number of digits from 0 to 100";
|
|
416
433
|
};
|
|
417
434
|
readonly FV0706: {
|
|
418
|
-
readonly title: "String that may hold half of a surrogate pair compared, searched or
|
|
419
|
-
readonly 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).";
|
|
435
|
+
readonly title: "String that may hold half of a surrogate pair compared, searched, joined or encoded";
|
|
436
|
+
readonly 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).";
|
|
420
437
|
};
|
|
421
438
|
readonly FV0707: {
|
|
422
439
|
readonly title: "`JSON.stringify()` of an unsupported value";
|
|
423
440
|
};
|
|
424
441
|
readonly FV0708: {
|
|
425
|
-
readonly title: "Template literal of
|
|
442
|
+
readonly title: "Template literal of a value it cannot write";
|
|
426
443
|
};
|
|
427
444
|
readonly FV0709: {
|
|
428
445
|
readonly title: "String method with the wrong number of arguments";
|
|
@@ -445,6 +462,33 @@ export declare const ERRORS: {
|
|
|
445
462
|
readonly FV0715: {
|
|
446
463
|
readonly title: "Case mapping by the server's locale";
|
|
447
464
|
};
|
|
465
|
+
readonly FV0716: {
|
|
466
|
+
readonly title: "`.toPrecision()` without a literal precision from 1 to 100";
|
|
467
|
+
};
|
|
468
|
+
readonly FV0717: {
|
|
469
|
+
readonly title: "`.toExponential()` without a literal number of digits from 0 to 100";
|
|
470
|
+
};
|
|
471
|
+
readonly FV0718: {
|
|
472
|
+
readonly title: "`isNaN()`, `isFinite()` or a `Number.is…()` test of a value that is not a number";
|
|
473
|
+
readonly 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))`.";
|
|
474
|
+
};
|
|
475
|
+
readonly FV0719: {
|
|
476
|
+
readonly title: "`Math` function engines round differently";
|
|
477
|
+
readonly 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.";
|
|
478
|
+
};
|
|
479
|
+
readonly FV0720: {
|
|
480
|
+
readonly title: "`**` or `Math.pow()` other than an exact integer power known when compiling";
|
|
481
|
+
readonly 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.";
|
|
482
|
+
};
|
|
483
|
+
readonly FV0721: {
|
|
484
|
+
readonly title: "Value that may be `null` or `undefined` written as a string";
|
|
485
|
+
};
|
|
486
|
+
readonly FV0722: {
|
|
487
|
+
readonly title: "`typeof` the compiler cannot work out";
|
|
488
|
+
};
|
|
489
|
+
readonly FV0723: {
|
|
490
|
+
readonly title: "`String()`, `.concat()` or `encodeURI…()` of a value it cannot write";
|
|
491
|
+
};
|
|
448
492
|
readonly FV0801: {
|
|
449
493
|
readonly title: "`.includes()` of a value of another type than the list's";
|
|
450
494
|
};
|
|
@@ -514,6 +558,36 @@ export declare const ERRORS: {
|
|
|
514
558
|
readonly FV0823: {
|
|
515
559
|
readonly title: "`v-for` key or index that is not a plain name";
|
|
516
560
|
};
|
|
561
|
+
readonly FV0824: {
|
|
562
|
+
readonly title: "List item read by an index that is not an integer";
|
|
563
|
+
};
|
|
564
|
+
readonly FV0825: {
|
|
565
|
+
readonly title: "`.at()` of a list given anything but one number";
|
|
566
|
+
};
|
|
567
|
+
readonly FV0826: {
|
|
568
|
+
readonly title: "`.indexOf()` or `.lastIndexOf()` given anything but one value of the list's type";
|
|
569
|
+
};
|
|
570
|
+
readonly FV0827: {
|
|
571
|
+
readonly title: "`.concat()` of anything but lists of the same item type";
|
|
572
|
+
};
|
|
573
|
+
readonly FV0828: {
|
|
574
|
+
readonly title: "`.reduce()` or `.reduceRight()` without an initial value";
|
|
575
|
+
};
|
|
576
|
+
readonly FV0829: {
|
|
577
|
+
readonly title: "`.reduce()` or `.reduceRight()` whose accumulator changes type or is not a string, number or boolean";
|
|
578
|
+
};
|
|
579
|
+
readonly FV0830: {
|
|
580
|
+
readonly title: "`.toSorted()` with a comparator of a shape the server does not order by";
|
|
581
|
+
};
|
|
582
|
+
readonly FV0831: {
|
|
583
|
+
readonly title: "`.toSorted()` without a comparator of a list of objects";
|
|
584
|
+
};
|
|
585
|
+
readonly FV0832: {
|
|
586
|
+
readonly title: "`.sort()` or `.reverse()` of a list in place";
|
|
587
|
+
};
|
|
588
|
+
readonly FV0833: {
|
|
589
|
+
readonly title: "Array spread of anything but lists of the same item type";
|
|
590
|
+
};
|
|
517
591
|
readonly FV0901: {
|
|
518
592
|
readonly title: "Slots that are not an object literal";
|
|
519
593
|
};
|
|
@@ -782,6 +856,7 @@ export declare const ERRORS: {
|
|
|
782
856
|
};
|
|
783
857
|
readonly FV1234: {
|
|
784
858
|
readonly title: "`typeof` of anything but a query value";
|
|
859
|
+
readonly retired: "0.9.0";
|
|
785
860
|
};
|
|
786
861
|
readonly FV1235: {
|
|
787
862
|
readonly title: "`??` after a query value with a fallback that is not a string";
|
|
@@ -1069,6 +1144,42 @@ export declare const ERRORS: {
|
|
|
1069
1144
|
readonly FV1711: {
|
|
1070
1145
|
readonly title: "`innerHTML` or `textContent` on a head tag that has no content";
|
|
1071
1146
|
};
|
|
1147
|
+
readonly FV1801: {
|
|
1148
|
+
readonly title: "The current time: `new Date()` or `Date.now()`";
|
|
1149
|
+
readonly 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).";
|
|
1150
|
+
};
|
|
1151
|
+
readonly FV1802: {
|
|
1152
|
+
readonly title: "`new Date()` or `Date.parse()` of anything but one present string or number";
|
|
1153
|
+
readonly 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).";
|
|
1154
|
+
};
|
|
1155
|
+
readonly FV1803: {
|
|
1156
|
+
readonly title: "Date string that browsers may read differently";
|
|
1157
|
+
readonly 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).";
|
|
1158
|
+
};
|
|
1159
|
+
readonly FV1804: {
|
|
1160
|
+
readonly title: "`Date` method or function ferrovue does not translate";
|
|
1161
|
+
readonly 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).";
|
|
1162
|
+
};
|
|
1163
|
+
readonly FV1805: {
|
|
1164
|
+
readonly title: "Locale formatting with `Intl`, `.toLocaleString()`, `.localeCompare()`, `$d` or `$n`";
|
|
1165
|
+
readonly 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).";
|
|
1166
|
+
};
|
|
1167
|
+
readonly FV1806: {
|
|
1168
|
+
readonly title: "`Date` anywhere but a component's own prop";
|
|
1169
|
+
readonly 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).";
|
|
1170
|
+
};
|
|
1171
|
+
readonly FV1807: {
|
|
1172
|
+
readonly title: "`Date.UTC()` of anything but one to seven numbers that are present";
|
|
1173
|
+
readonly 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).";
|
|
1174
|
+
};
|
|
1175
|
+
readonly FV1808: {
|
|
1176
|
+
readonly title: "`Date.UTC()` of parts engines compute differently";
|
|
1177
|
+
readonly 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).";
|
|
1178
|
+
};
|
|
1179
|
+
readonly FV1809: {
|
|
1180
|
+
readonly title: "`===`, `!==`, `==` or `!=` with a `Date`";
|
|
1181
|
+
readonly 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).";
|
|
1182
|
+
};
|
|
1072
1183
|
};
|
|
1073
1184
|
/** A stable error code, as `FV0604`. */
|
|
1074
1185
|
export type Code = keyof typeof ERRORS;
|