ferrovue 0.4.0 → 0.5.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.
Files changed (68) hide show
  1. package/README.md +198 -34
  2. package/dist/attrs.d.ts +1 -1
  3. package/dist/attrs.js +24 -22
  4. package/dist/calls.js +17 -15
  5. package/dist/children.d.ts +9 -2
  6. package/dist/children.js +48 -35
  7. package/dist/classes.js +11 -11
  8. package/dist/cli.js +31 -5
  9. package/dist/client.d.ts +35 -2
  10. package/dist/client.js +77 -2
  11. package/dist/compiler.d.ts +1 -1
  12. package/dist/compiler.js +7 -3
  13. package/dist/component.d.ts +3 -1
  14. package/dist/component.js +114 -19
  15. package/dist/constants.d.ts +42 -0
  16. package/dist/constants.js +292 -0
  17. package/dist/context.d.ts +25 -1
  18. package/dist/context.js +11 -5
  19. package/dist/diagnostics.d.ts +23 -0
  20. package/dist/diagnostics.js +30 -0
  21. package/dist/errors.d.ts +867 -0
  22. package/dist/errors.js +330 -0
  23. package/dist/expr.d.ts +1 -0
  24. package/dist/expr.js +120 -49
  25. package/dist/fixture.d.ts +3 -10
  26. package/dist/fixture.js +2 -6
  27. package/dist/hydration.d.ts +15 -0
  28. package/dist/hydration.js +80 -0
  29. package/dist/index.d.ts +2 -1
  30. package/dist/index.js +2 -1
  31. package/dist/link-router.d.ts +24 -0
  32. package/dist/link-router.js +36 -0
  33. package/dist/lists.js +30 -21
  34. package/dist/loops.js +14 -10
  35. package/dist/model.d.ts +32 -1
  36. package/dist/model.js +123 -6
  37. package/dist/narrowing.d.ts +8 -0
  38. package/dist/narrowing.js +40 -9
  39. package/dist/plugins/client-only.d.ts +8 -0
  40. package/dist/plugins/client-only.js +107 -0
  41. package/dist/plugins/i18n.js +14 -14
  42. package/dist/plugins/index.js +3 -1
  43. package/dist/plugins/router-link.js +33 -30
  44. package/dist/plugins/router.js +9 -9
  45. package/dist/plugins/scoped.js +1 -1
  46. package/dist/plugins/shared-types.js +1 -1
  47. package/dist/plugins/stores.js +26 -24
  48. package/dist/plugins/teleport.js +3 -3
  49. package/dist/plugins/twins.d.ts +10 -0
  50. package/dist/plugins/twins.js +164 -0
  51. package/dist/routes.d.ts +10 -0
  52. package/dist/routes.js +6 -0
  53. package/dist/rust.js +18 -14
  54. package/dist/script.d.ts +3 -0
  55. package/dist/script.js +119 -17
  56. package/dist/settle.d.ts +6 -0
  57. package/dist/settle.js +34 -0
  58. package/dist/slots.js +18 -18
  59. package/dist/strings.js +11 -11
  60. package/dist/styles.js +13 -13
  61. package/dist/template.js +11 -11
  62. package/dist/testing.d.ts +1 -0
  63. package/dist/testing.js +3 -2
  64. package/dist/typescript.d.ts +2 -1
  65. package/dist/typescript.js +46 -19
  66. package/dist/vite.d.ts +1 -1
  67. package/dist/vite.js +10 -9
  68. package/package.json +9 -5
package/dist/lists.js CHANGED
@@ -4,14 +4,14 @@ import { expr, fieldVal } from "./expr.js";
4
4
  import { cond } from "./narrowing.js";
5
5
  import { asF64, isNumber } from "./numbers.js";
6
6
  import { asCow, isTemporary, lonely, meet, yieldsCow } from "./strings.js";
7
- import { atom, operand, strArg, UNARY } from "./parens.js";
7
+ import { atom, binary, operand, strArg, UNARY } from "./parens.js";
8
8
  const COW = "std::borrow::Cow::<str>";
9
9
  function itemsTy(s, v, n) {
10
10
  if (v.ty.k !== "list")
11
- return fail(s.comp, "not a list", n);
11
+ return fail(s.comp, "FV0803", "not a list", n);
12
12
  const of = v.ty.of;
13
13
  if (!["str", "int", "float", "bool", "struct", "child"].includes(of.k)) {
14
- fail(s.comp, "this method takes a list of strings, numbers, booleans or objects", n);
14
+ fail(s.comp, "FV0804", "this method takes a list of strings, numbers, booleans or objects", n);
15
15
  }
16
16
  return of;
17
17
  }
@@ -44,13 +44,13 @@ function itemVal(of, param, byRef, lone) {
44
44
  function arrow(s, fn, list, of, byRef, method, body) {
45
45
  const comp = s.comp;
46
46
  if (fn?.type !== "ArrowFunctionExpression" || fn.async || fn.params.length > 2) {
47
- fail(comp, `\`.${method}()\` takes an arrow function of the item, and of its index: \`x => …\`, \`(x, i) => …\``, fn);
47
+ fail(comp, "FV0805", `\`.${method}()\` takes an arrow function of the item, and of its index: \`x => …\`, \`(x, i) => …\``, fn);
48
48
  }
49
49
  if (fn.body.type === "BlockStatement")
50
- fail(comp, `\`.${method}()\` takes an arrow function whose body is an expression, without \`{ return … }\``, fn);
50
+ fail(comp, "FV0806", `\`.${method}()\` takes an arrow function whose body is an expression, without \`{ return … }\``, fn);
51
51
  const [item, index] = fn.params;
52
52
  if (index && index.type !== "Identifier")
53
- fail(comp, "an arrow function's index is a plain name", index);
53
+ fail(comp, "FV0807", "an arrow function's index is a plain name", index);
54
54
  const n = ++ctx.narrowCount;
55
55
  const p = of.k === "str" ? `fv_s${n}` : `fv_a${n}`;
56
56
  const i = `fv_i${n}`;
@@ -64,14 +64,14 @@ function arrow(s, fn, list, of, byRef, method, body) {
64
64
  else if (item?.type === "ObjectPattern" && (of.k === "struct" || of.k === "child")) {
65
65
  for (const prop of item.properties) {
66
66
  if (prop.type !== "ObjectProperty" || prop.computed || prop.value.type !== "Identifier") {
67
- fail(comp, "a destructured arrow function parameter binds plain names, without defaults", prop);
67
+ fail(comp, "FV0808", "a destructured arrow function parameter binds plain names, without defaults", prop);
68
68
  }
69
69
  locals.set(prop.value.name, fieldVal(comp, v.code, of, prop.key.name ?? prop.key.value, prop));
70
70
  bound.push(prop.value.name);
71
71
  }
72
72
  }
73
73
  else if (item)
74
- fail(comp, "an arrow function's item is a name, or an object pattern of an object", item);
74
+ fail(comp, "FV0809", "an arrow function's item is a name, or an object pattern of an object", item);
75
75
  if (index) {
76
76
  locals.set(index.name, { code: i, ty: INT });
77
77
  bound.push(index.name);
@@ -110,7 +110,7 @@ function mapped(s, v, n) {
110
110
  case "child":
111
111
  return `&${operand(v.code, UNARY)}`;
112
112
  default:
113
- return fail(s.comp, `\`.map()\` makes a list of strings, numbers, booleans or objects, not ${v.ty.k === "opt" ? "optional values" : "these"}`, n);
113
+ return fail(s.comp, "FV0810", `\`.map()\` makes a list of strings, numbers, booleans or objects, not ${v.ty.k === "opt" ? "optional values" : "these"}`, n);
114
114
  }
115
115
  }
116
116
  export function listMethod(s, target, method, args, n) {
@@ -121,13 +121,13 @@ export function listMethod(s, target, method, args, n) {
121
121
  const it = items(target);
122
122
  if (method === "slice") {
123
123
  if (args.length < 1 || args.length > 2)
124
- fail(comp, "`.slice()` takes a start, and an end", n);
124
+ fail(comp, "FV0811", "`.slice()` takes a start, and an end", n);
125
125
  const [start, end] = args.map((a) => expr(s, a));
126
126
  const index = (v) => {
127
127
  if (v === undefined || v.ty.k === "undef")
128
128
  return null;
129
129
  if (!isNumber(v.ty))
130
- fail(comp, "`.slice()` takes numbers", n);
130
+ fail(comp, "FV0812", "`.slice()` takes numbers", n);
131
131
  return asF64(v);
132
132
  };
133
133
  const from = index(start) ?? "0.0";
@@ -135,7 +135,7 @@ export function listMethod(s, target, method, args, n) {
135
135
  return computed(`fv::js_slice_items(${it}, ${from}, ${to === null ? "None" : `Some(${to})`})`, of, target.lone);
136
136
  }
137
137
  if (args.length !== 1)
138
- fail(comp, `\`.${method}()\` takes one arrow function`, n);
138
+ fail(comp, "FV0813", `\`.${method}()\` takes one arrow function`, n);
139
139
  const fn = args[0];
140
140
  switch (method) {
141
141
  case "filter": {
@@ -175,19 +175,19 @@ export function objectCall(s, method, args, n) {
175
175
  const comp = s.comp;
176
176
  const r = args.length === 1 ? expr(s, args[0]) : null;
177
177
  if (r?.ty.k !== "record")
178
- return fail(comp, `\`Object.${method}()\` takes a \`Record<string, T>\``, n);
178
+ return fail(comp, "FV0814", `\`Object.${method}()\` takes a \`Record<string, T>\``, n);
179
179
  const of = r.ty.of;
180
180
  if (method === "keys")
181
181
  return computed(`${atom(r.code)}.keys().map(${COW}::Borrowed)`, STR);
182
182
  if (method === "values") {
183
183
  if (of.k === "list")
184
- fail(comp, "`Object.values()` of a record of lists, which these methods cannot walk", n);
184
+ fail(comp, "FV0815", "`Object.values()` of a record of lists, which these methods cannot walk", n);
185
185
  const values = of.k === "str" ? `.values().map(|v| ${COW}::Borrowed(&**v))` : of.k === "struct" || of.k === "child" ? ".values()" : ".values().copied()";
186
186
  return computed(`${atom(r.code)}${values}`, of);
187
187
  }
188
188
  if (method === "entries")
189
- return fail(comp, "`Object.entries()` is supported as the source of a `v-for`: `([key, value], i) in Object.entries(r)`", n);
190
- return fail(comp, `\`Object.${method}()\` is not supported: \`keys\`, \`values\` and \`entries\` of a record are`, n);
189
+ return fail(comp, "FV0816", "`Object.entries()` is supported as the source of a `v-for`: `([key, value], i) in Object.entries(r)`", n);
190
+ return fail(comp, "FV0817", `\`Object.${method}()\` is not supported: \`keys\`, \`values\` and \`entries\` of a record are`, n);
191
191
  }
192
192
  export function computedListMethod(s, target, method, args, n) {
193
193
  if (target.ty.k !== "list")
@@ -195,11 +195,20 @@ export function computedListMethod(s, target, method, args, n) {
195
195
  const of = target.ty.of;
196
196
  if (method === "includes" && args.length === 1) {
197
197
  const x = expr(s, args[0]);
198
- if (!sameTy(x.ty, of) || !["str", "int", "float", "bool"].includes(of.k))
199
- fail(s.comp, "`.includes()` looks for a value of the list's own type", args[0]);
198
+ const numbers = isNumber(x.ty) && isNumber(of);
199
+ if ((!sameTy(x.ty, of) && !numbers) || !["str", "int", "float", "bool"].includes(of.k))
200
+ fail(s.comp, "FV0801", "`.includes()` looks for a value of the list's own type", args[0]);
200
201
  if (of.k === "str")
201
202
  meet(s.comp, target, x, "`.includes()`", n, "equal");
202
- const test = of.k === "str" ? `*v == ${x.code.startsWith("&*") ? `*${x.code.slice(2)}` : `*${operand(x.code, UNARY)}`}` : of.k === "float" ? `v == ${x.code} || (v.is_nan() && ${atom(x.code)}.is_nan())` : `v == ${x.code}`;
203
+ const test = of.k === "str"
204
+ ? `*v == ${x.code.startsWith("&*") ? `*${x.code.slice(2)}` : `*${operand(x.code, UNARY)}`}`
205
+ : of.k === "float" && x.ty.k === "int"
206
+ ? binary("v", "==", asF64(x))
207
+ : of.k === "float"
208
+ ? `v == ${x.code} || (v.is_nan() && ${atom(x.code)}.is_nan())`
209
+ : of.k === "int" && x.ty.k === "float"
210
+ ? binary("v as f64", "==", x.code)
211
+ : `v == ${x.code}`;
203
212
  return { code: `${items(target)}.any(|v| ${test})`, ty: BOOL };
204
213
  }
205
214
  if (method === "join" && args.length <= 1) {
@@ -207,10 +216,10 @@ export function computedListMethod(s, target, method, args, n) {
207
216
  return null;
208
217
  const sep = args.length ? expr(s, args[0]) : { code: '","', ty: STR };
209
218
  if (sep.ty.k !== "str")
210
- fail(s.comp, "`.join()` takes a string", args[0]);
219
+ fail(s.comp, "FV0802", "`.join()` takes a string", args[0]);
211
220
  const parted = /^"[^"]/.test(sep.code);
212
221
  if (of.k === "str" && target.lone && (!parted || sep.lone))
213
- fail(s.comp, lonely("`.join()` with no literal separator"), n);
222
+ fail(s.comp, "FV0706", lonely("`.join()` with no literal separator"), n);
214
223
  const each = of.k === "str" ? "" : of.k === "bool" ? `.map(|v| if v { "true" } else { "false" })` : ".map(|v| fv::Js(v).to_string())";
215
224
  return { code: `&*${items(target)}${each}.collect::<Vec<_>>().join(${strArg(sep.code)})`, ty: STR, ...(target.lone || sep.lone ? { lone: true } : {}) };
216
225
  }
package/dist/loops.js CHANGED
@@ -8,15 +8,15 @@ import { statements } from "./template.js";
8
8
  export function list(s, e, c) {
9
9
  const fn = c.arguments[1];
10
10
  if (fn.type !== "ArrowFunctionExpression" || fn.body.type !== "BlockStatement") {
11
- fail(s.comp, "unexpected `ssrRenderList` callback", c);
11
+ fail(s.comp, "FV0006", "unexpected `ssrRenderList` callback", c);
12
12
  }
13
13
  if (isObjectCall(c.arguments[0], "entries")) {
14
14
  const [pair, index] = fn.params;
15
15
  const r = c.arguments[0].arguments.length === 1 ? expr(s, c.arguments[0].arguments[0]) : null;
16
16
  if (r?.ty.k !== "record")
17
- return fail(s.comp, "`Object.entries()` takes a `Record<string, T>`", c.arguments[0]);
17
+ return fail(s.comp, "FV0814", "`Object.entries()` takes a `Record<string, T>`", c.arguments[0]);
18
18
  if (pair?.type !== "ArrayPattern" || pair.elements.length > 2 || pair.elements.some((x) => x?.type !== "Identifier")) {
19
- return fail(s.comp, "a `v-for` over `Object.entries()` names its items `[key, value]`", pair ?? c);
19
+ return fail(s.comp, "FV0818", "a `v-for` over `Object.entries()` names its items `[key, value]`", pair ?? c);
20
20
  }
21
21
  recordLoop(s, e, r, fn, pair.elements[1], pair.elements[0], index);
22
22
  return;
@@ -55,7 +55,7 @@ export function list(s, e, c) {
55
55
  else if (src.ty.k === "list") {
56
56
  of = src.ty.of;
57
57
  if (of.k === "undef")
58
- fail(s.comp, "`v-for` over an empty array literal", c);
58
+ fail(s.comp, "FV0819", "`v-for` over an empty array literal", c);
59
59
  const itemCode = `${itemName}_ref`;
60
60
  walk = `${atom(src.code)}.iter()`;
61
61
  bound = itemCode;
@@ -64,12 +64,16 @@ export function list(s, e, c) {
64
64
  e.stmt(`let ${itemName}: &str = ${itemCode};`);
65
65
  else if (of.k === "struct" || of.k === "child")
66
66
  e.stmt(`let ${itemName} = ${itemCode};`);
67
+ else if (of.k === "opt" && of.of.k === "str")
68
+ e.stmt(`let ${itemName} = ${itemCode}.as_deref();`);
69
+ else if (of.k === "opt" && !["int", "float", "bool"].includes(of.of.k))
70
+ e.stmt(`let ${itemName} = ${itemCode}.as_ref();`);
67
71
  else
68
72
  e.stmt(`let ${itemName} = *${itemName}_ref;`);
69
73
  itemLet = e.lines.length - 1;
70
74
  }
71
75
  else {
72
- return fail(s.comp, "`v-for` walks an array, or counts to a number", c);
76
+ return fail(s.comp, "FV0820", "`v-for` walks an array, or counts to a number", c);
73
77
  }
74
78
  let idxLet = -1;
75
79
  if (idx) {
@@ -82,13 +86,13 @@ export function list(s, e, c) {
82
86
  else if (item.type === "ObjectPattern") {
83
87
  for (const p of item.properties) {
84
88
  if (p.type !== "ObjectProperty" || p.computed || p.value.type !== "Identifier") {
85
- fail(s.comp, "a destructured `v-for` item binds plain names, without defaults", p);
89
+ fail(s.comp, "FV0821", "a destructured `v-for` item binds plain names, without defaults", p);
86
90
  }
87
91
  inner.set(p.value.name, fieldVal(s.comp, itemName, of, p.key.name ?? p.key.value, p));
88
92
  }
89
93
  }
90
94
  else
91
- fail(s.comp, "a `v-for` item is a name or an object pattern", item);
95
+ fail(s.comp, "FV0822", "a `v-for` item is a name or an object pattern", item);
92
96
  if (index)
93
97
  inner.set(index.name, { code: idx, ty: INT });
94
98
  const before = e.literalBytes;
@@ -118,7 +122,7 @@ function recordLoop(s, e, r, fn, value, key, index) {
118
122
  const of = r.ty.of;
119
123
  for (const p of [key, index])
120
124
  if (p && p.type !== "Identifier")
121
- fail(s.comp, "a record's key and index in `v-for` are plain names", p);
125
+ fail(s.comp, "FV0823", "a record's key and index in `v-for` are plain names", p);
122
126
  const n = ++ctx.narrowCount;
123
127
  const v = value?.type === "Identifier" ? snake(value.name) : `fv_value${n}`;
124
128
  const k = key ? snake(key.name) : `fv_key${n}`;
@@ -135,13 +139,13 @@ function recordLoop(s, e, r, fn, value, key, index) {
135
139
  else if (value?.type === "ObjectPattern" && (of.k === "struct" || of.k === "child")) {
136
140
  for (const p of value.properties) {
137
141
  if (p.type !== "ObjectProperty" || p.computed || p.value.type !== "Identifier") {
138
- fail(s.comp, "a destructured `v-for` item binds plain names, without defaults", p);
142
+ fail(s.comp, "FV0821", "a destructured `v-for` item binds plain names, without defaults", p);
139
143
  }
140
144
  inner.set(p.value.name, fieldVal(s.comp, v, of, p.key.name ?? p.key.value, p));
141
145
  }
142
146
  }
143
147
  else if (value)
144
- fail(s.comp, "a `v-for` item is a name, or an object pattern of an object", value);
148
+ fail(s.comp, "FV0822", "a `v-for` item is a name, or an object pattern of an object", value);
145
149
  if (key)
146
150
  inner.set(key.name, { code: k, ty: STR });
147
151
  if (index)
package/dist/model.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import type { SourceMapConsumer } from "source-map-js";
2
2
  import type { Plugin } from "./plugin.ts";
3
+ import type { Const } from "./constants.ts";
4
+ import type { Code } from "./errors.ts";
3
5
  export type N = any;
4
6
  export type Ty = {
5
7
  k: "str";
@@ -11,9 +13,12 @@ export type Ty = {
11
13
  k: "bool";
12
14
  } | {
13
15
  k: "undef";
16
+ } | {
17
+ k: "null";
14
18
  } | {
15
19
  k: "opt";
16
20
  of: Ty;
21
+ none?: "null" | "either";
17
22
  } | {
18
23
  k: "list";
19
24
  of: Ty;
@@ -63,7 +68,13 @@ export declare const INT: Ty;
63
68
  export declare const FLOAT: Ty;
64
69
  export declare const BOOL: Ty;
65
70
  export declare const UNDEF: Ty;
71
+ export declare const NULL: Ty;
72
+ export type Absence = "undefined" | "null" | "either";
66
73
  export declare const opt: (of: Ty) => Ty;
74
+ export declare function absence(ty: Ty): Absence | null;
75
+ export declare function joinAbsence(a: Absence | null, b: Absence | null): Absence | null;
76
+ export declare function withAbsence(of: Ty, none: Absence | null): Ty;
77
+ export declare function nothing(ty: Ty): boolean;
67
78
  export declare const RUST_KEYWORDS: Set<string>;
68
79
  export declare const RUST_PRELUDE: Set<string>;
69
80
  export declare function snake(js: string): string;
@@ -72,8 +83,24 @@ export declare function declares(comp: Component, key: string): boolean;
72
83
  export declare function takesAttrs(comp: Component): boolean;
73
84
  export declare function rustStr(s: string): string;
74
85
  export declare function rustChar(ch: string): string;
86
+ /** Where in a file an error is: lines and columns count from 1, and the end is exclusive. */
87
+ export interface Location {
88
+ file: string;
89
+ line?: number;
90
+ column?: number;
91
+ endLine?: number;
92
+ endColumn?: number;
93
+ }
94
+ /** A construct the compiler refuses: its stable code, the message as shown, where it is, and what
95
+ * is refused without the location and the quoted line. */
75
96
  export declare class GenError extends Error {
97
+ readonly code: Code;
98
+ readonly at: Location | null;
99
+ readonly what: string;
100
+ constructor(code: Code, message: string, at?: Location | null, what?: string);
76
101
  }
102
+ /** Refuse something in a file that has no source to quote, as a locale or routes file. */
103
+ export declare function failIn(file: string, code: Code, what: string): never;
77
104
  export interface Component {
78
105
  name: string;
79
106
  module: string;
@@ -84,6 +111,7 @@ export interface Component {
84
111
  line: number;
85
112
  column: number;
86
113
  };
114
+ templateAst?: N;
87
115
  attrsDropped?: N;
88
116
  props: Struct;
89
117
  structs: Map<string, Struct>;
@@ -127,6 +155,8 @@ export interface Scope {
127
155
  attrs: string | null;
128
156
  fallthrough: string | null;
129
157
  attrsBindings: Set<string>;
158
+ slotsBindings: Set<string>;
159
+ consts: Map<string, Const>;
130
160
  sid: string | null;
131
161
  loop?: {
132
162
  item: string;
@@ -136,5 +166,6 @@ export interface Scope {
136
166
  }
137
167
  export type Origin = "source" | "template";
138
168
  export declare function tagAst(node: N, origin: Origin, seen?: WeakSet<object>): void;
139
- export declare function fail(comp: Component, what: string, node?: N): never;
169
+ export declare function sourceAt(source: string, offset: number): N;
170
+ export declare function fail(comp: Component, code: Code, what: string, node?: N): never;
140
171
  export declare function sameTy(a: Ty, b: Ty): boolean;
package/dist/model.js CHANGED
@@ -3,7 +3,32 @@ export const INT = { k: "int" };
3
3
  export const FLOAT = { k: "float" };
4
4
  export const BOOL = { k: "bool" };
5
5
  export const UNDEF = { k: "undef" };
6
- export const opt = (of) => (of.k === "opt" ? of : { k: "opt", of });
6
+ export const NULL = { k: "null" };
7
+ export const opt = (of) => (of.k !== "opt" ? { k: "opt", of } : of.none === undefined ? of : { k: "opt", of: of.of, none: "either" });
8
+ export function absence(ty) {
9
+ if (ty.k === "undef")
10
+ return "undefined";
11
+ if (ty.k === "null")
12
+ return "null";
13
+ if (ty.k === "opt")
14
+ return ty.none ?? "undefined";
15
+ return null;
16
+ }
17
+ export function joinAbsence(a, b) {
18
+ if (a === null || a === b)
19
+ return b;
20
+ if (b === null)
21
+ return a;
22
+ return "either";
23
+ }
24
+ export function withAbsence(of, none) {
25
+ if (none === null)
26
+ return of;
27
+ return none === "undefined" ? { k: "opt", of } : { k: "opt", of, none };
28
+ }
29
+ export function nothing(ty) {
30
+ return ty.k === "undef" || ty.k === "null";
31
+ }
7
32
  export const RUST_KEYWORDS = new Set("as break const continue crate else enum extern false fn for if impl in let loop match mod move mut pub ref return self Self static struct super trait true type unsafe use where while async await dyn abstract become box do final macro override priv typeof unsized virtual yield try".split(" "));
8
33
  const NOT_RAW = new Set(["self", "Self", "super", "crate"]);
9
34
  export const RUST_PRELUDE = new Set(["Option", "Some", "None", "Vec", "String", "Box", "Result", "Ok", "Err", "Cow", "Slots", "Default"]);
@@ -51,7 +76,22 @@ export function rustChar(ch) {
51
76
  return `'"'`;
52
77
  return `'${rustStr(ch).slice(1, -1)}'`;
53
78
  }
79
+ /** A construct the compiler refuses: its stable code, the message as shown, where it is, and what
80
+ * is refused without the location and the quoted line. */
54
81
  export class GenError extends Error {
82
+ code;
83
+ at;
84
+ what;
85
+ constructor(code, message, at = null, what = message) {
86
+ super(message);
87
+ this.code = code;
88
+ this.at = at;
89
+ this.what = what;
90
+ }
91
+ }
92
+ /** Refuse something in a file that has no source to quote, as a locale or routes file. */
93
+ export function failIn(file, code, what) {
94
+ throw new GenError(code, `${file}: ${what}`, { file }, what);
55
95
  }
56
96
  export function blankComponent(name, module, file, structs = new Map()) {
57
97
  return {
@@ -105,12 +145,79 @@ function locate(comp, node) {
105
145
  if (node.__fv === "template" && comp.templateMap && comp.templateStart) {
106
146
  const o = comp.templateMap.originalPositionFor({ line: start.line, column: start.column });
107
147
  if (o.line == null || o.column == null)
108
- return null;
148
+ return locateWithin(comp, node) ?? locate(comp, written(comp, node));
109
149
  const t = comp.templateStart;
110
150
  return o.line === 1 ? { line: t.line, column: t.column - 1 + o.column } : { line: t.line + o.line - 1, column: o.column };
111
151
  }
112
152
  return null;
113
153
  }
154
+ function locateWithin(comp, node) {
155
+ const start = node.loc.start;
156
+ const inner = Object.entries(node)
157
+ .filter(([k, v]) => k !== "loc" && v && typeof v === "object")
158
+ .flatMap(([, v]) => (Array.isArray(v) ? v : [v]))
159
+ .filter((c) => c?.loc?.start)
160
+ .toSorted((a, b) => a.start - b.start);
161
+ for (const child of inner) {
162
+ const at = locate(comp, child);
163
+ if (!at)
164
+ continue;
165
+ const c = child.loc.start;
166
+ const back = c.line === start.line ? c.column - start.column : 0;
167
+ return { line: at.line, column: Math.max(0, at.column - back) };
168
+ }
169
+ return null;
170
+ }
171
+ export function sourceAt(source, offset) {
172
+ const before = source.slice(0, offset);
173
+ const line = before.split("\n").length;
174
+ return { type: "VueSource", loc: { start: { line, column: offset - before.lastIndexOf("\n") - 1 } }, __fv: "source" };
175
+ }
176
+ const plain = (name) => name.replace(/[-_]/g, "").toLowerCase();
177
+ function assetName(n, prefix) {
178
+ if (n?.type === "Identifier")
179
+ return n.name.replace(prefix, "");
180
+ if (n?.type === "MemberExpression")
181
+ return n.computed ? n.property.value : n.property.name;
182
+ return null;
183
+ }
184
+ function written(comp, node) {
185
+ const call = node.type === "VariableDeclaration" ? node.declarations[0]?.init : node.type === "ExpressionStatement" ? node.expression : node;
186
+ const callee = call?.type === "CallExpression" && call.callee.type === "Identifier" ? call.callee.name : null;
187
+ const tagged = (name) => name && templateNode(comp, (el) => (plain(el.tag) === plain(name) ? el : null));
188
+ const directive = (name) => name && templateNode(comp, (el) => el.props.find((p) => p.type === 7 && plain(p.name) === plain(name)));
189
+ switch (callee) {
190
+ case "_ssrRenderComponent":
191
+ return tagged(assetName(call.arguments[0], /^_component_/));
192
+ case "_resolveComponent":
193
+ return tagged(call.arguments[0]?.value ?? null);
194
+ case "_resolveDirective":
195
+ return directive(call.arguments[0]?.value ?? null);
196
+ case "_ssrGetDirectiveProps":
197
+ return directive(assetName(call.arguments[1], /^_directive_/)?.replace(/^v(?=[A-Z])/, "") ?? null);
198
+ case "_ssrRenderSlot": {
199
+ const name = call.arguments[1]?.value;
200
+ return templateNode(comp, (el) => (el.tag === "slot" && (el.props.find((p) => p.type === 6 && p.name === "name")?.value?.content ?? "default") === name ? el : null));
201
+ }
202
+ default:
203
+ return null;
204
+ }
205
+ }
206
+ function templateNode(comp, test) {
207
+ const visit = (n) => {
208
+ const found = n.type === 1 ? test(n) : null;
209
+ if (found)
210
+ return found;
211
+ for (const c of n.children ?? []) {
212
+ const inner = visit(c);
213
+ if (inner)
214
+ return inner;
215
+ }
216
+ return null;
217
+ };
218
+ const found = comp.templateAst ? visit(comp.templateAst) : null;
219
+ return found && { type: "VueTemplate", loc: { start: { line: found.loc.start.line, column: found.loc.start.column - 1 } }, __fv: "source" };
220
+ }
114
221
  function snippet(comp, at) {
115
222
  const text = comp.source?.split("\n")[at.line - 1];
116
223
  if (text === undefined)
@@ -119,12 +226,22 @@ function snippet(comp, at) {
119
226
  const lead = text.slice(0, at.column).replace(/[^\t]/g, " ");
120
227
  return `\n ${n} | ${text}\n ${" ".repeat(n.length)} | ${lead}^`;
121
228
  }
122
- export function fail(comp, what, node) {
229
+ function locateEnd(node) {
230
+ const end = node?.__fv === "source" ? node.loc?.end : null;
231
+ return end ? { endLine: end.line, endColumn: end.column + 1 } : null;
232
+ }
233
+ export function fail(comp, code, what, node) {
123
234
  const at = locate(comp, node);
124
235
  if (!at)
125
- throw new GenError(`${comp.file}: ${what}`);
126
- throw new GenError(`${comp.file}:${at.line}:${at.column + 1}: ${what}${snippet(comp, at)}`);
236
+ throw new GenError(code, `${comp.file}: ${what}`, { file: comp.file }, what);
237
+ const where = { file: comp.file, line: at.line, column: at.column + 1, ...locateEnd(node) };
238
+ throw new GenError(code, `${comp.file}:${at.line}:${at.column + 1}: ${what}${snippet(comp, at)}`, where, what);
239
+ }
240
+ function canonical(_key, value) {
241
+ if (!value || typeof value !== "object" || Array.isArray(value))
242
+ return value;
243
+ return Object.fromEntries(Object.entries(value).toSorted(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
127
244
  }
128
245
  export function sameTy(a, b) {
129
- return JSON.stringify(a) === JSON.stringify(b);
246
+ return JSON.stringify(a, canonical) === JSON.stringify(b, canonical);
130
247
  }
@@ -2,6 +2,14 @@ import { type N, type Scope, type Ty, type Val } from "./model.ts";
2
2
  export declare function known(v: Val): boolean | undefined;
3
3
  export declare function truthy(v: Val): string;
4
4
  export declare function pathOf(n: N): string | null;
5
+ export interface NullTest {
6
+ target: N;
7
+ literal: "null" | "undefined";
8
+ strict: boolean;
9
+ is: boolean;
10
+ }
11
+ export declare function nullTest(n: N): NullTest | null;
12
+ export declare function checkNullTest(s: Scope, t: NullTest, v: Val): void;
5
13
  export interface Presence {
6
14
  path: string;
7
15
  pattern: (name: string) => string;
package/dist/narrowing.js CHANGED
@@ -1,4 +1,4 @@
1
- import { BOOL } from "./model.js";
1
+ import { absence, BOOL, fail, nothing } from "./model.js";
2
2
  import { ctx } from "./context.js";
3
3
  import { claim } from "./plugin.js";
4
4
  import { atom, bare, binary, condition, ifElse, logical, not, occurrences, receiver } from "./parens.js";
@@ -7,6 +7,8 @@ import { expr } from "./expr.js";
7
7
  export function known(v) {
8
8
  if (v.konst !== undefined)
9
9
  return v.konst;
10
+ if (nothing(v.ty))
11
+ return false;
10
12
  if (v.ty.k === "str" && /^"(?:[^"\\]|\\.)*"$/.test(v.code))
11
13
  return v.code !== '""';
12
14
  if (v.num !== undefined)
@@ -30,6 +32,7 @@ export function truthy(v) {
30
32
  case "bool":
31
33
  return v.code;
32
34
  case "undef":
35
+ case "null":
33
36
  return "false";
34
37
  case "opt":
35
38
  return `${atom(v.code)}.is_some_and(|v| ${truthy({ code: "v", ty: v.ty.of })})`;
@@ -51,6 +54,35 @@ export function pathOf(n) {
51
54
  return `${base}.${n.property.value}`;
52
55
  return null;
53
56
  }
57
+ function nothingLiteral(n) {
58
+ if (n.type === "NullLiteral")
59
+ return "null";
60
+ if (n.type === "Identifier" && n.name === "undefined")
61
+ return "undefined";
62
+ return null;
63
+ }
64
+ export function nullTest(n) {
65
+ if (n.type !== "BinaryExpression" || !["===", "!==", "==", "!="].includes(n.operator))
66
+ return null;
67
+ const right = nothingLiteral(n.right);
68
+ const left = nothingLiteral(n.left);
69
+ const literal = right ?? left;
70
+ if (literal === null)
71
+ return null;
72
+ return { target: right !== null ? n.left : n.right, literal, strict: n.operator.length === 3, is: n.operator.startsWith("=") };
73
+ }
74
+ export function checkNullTest(s, t, v) {
75
+ const none = absence(v.ty);
76
+ if (none === null || !t.strict || none === t.literal)
77
+ return;
78
+ const op = t.is ? "==" : "!=";
79
+ if (none === "either") {
80
+ fail(s.comp, "FV0626", `\`${op}= ${t.literal}\` of a value that may be \`null\` or \`undefined\`, which its Rust \`Option\` cannot tell apart: test both with \`${op} null\``, t.target);
81
+ }
82
+ const other = t.literal === "null" ? "undefined" : "null";
83
+ const what = none === "null" ? "is `T | null`, never `undefined`" : "is optional, which is `undefined` when absent, never `null`";
84
+ fail(s.comp, "FV0627", `\`${op}= ${t.literal}\` of a value that ${what}: compare with \`${other}\``, t.target);
85
+ }
54
86
  export function presence(s, n) {
55
87
  for (const p of ctx.plugins) {
56
88
  const own = p.presence?.(s, n);
@@ -64,15 +96,12 @@ export function presence(s, n) {
64
96
  target = n.argument;
65
97
  negated = true;
66
98
  }
67
- else if (n.type === "BinaryExpression" && (n.operator === "===" || n.operator === "!==")) {
68
- const isUndef = (m) => m.type === "Identifier" && m.name === "undefined";
69
- if (isUndef(n.right))
70
- target = n.left;
71
- else if (isUndef(n.left))
72
- target = n.right;
73
- else
99
+ else if (n.type === "BinaryExpression") {
100
+ const test = nullTest(n);
101
+ if (!test)
74
102
  return null;
75
- negated = n.operator === "===";
103
+ target = test.target;
104
+ negated = test.is;
76
105
  byTruth = false;
77
106
  }
78
107
  const path = pathOf(target);
@@ -81,6 +110,8 @@ export function presence(s, n) {
81
110
  const v = expr(s, target);
82
111
  if (v.ty.k !== "opt")
83
112
  return null;
113
+ if (n.type === "BinaryExpression")
114
+ checkNullTest(s, nullTest(n), v);
84
115
  const of = v.ty.of;
85
116
  const scalar = of.k === "str" || of.k === "int" || of.k === "float" || of.k === "bool";
86
117
  const filtered = byTruth && scalar;
@@ -0,0 +1,8 @@
1
+ import { type Component } from "../model.ts";
2
+ import { type Plugin } from "../plugin.ts";
3
+ interface ClientOnlyRun {
4
+ locals: Map<Component, Set<string>>;
5
+ outside: Map<Component, Set<string>>;
6
+ }
7
+ export declare const clientOnly: Plugin<ClientOnlyRun>;
8
+ export {};