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
@@ -0,0 +1,292 @@
1
+ import { blankComponent, BOOL, fail, FLOAT, INT, NULL, RUST_PRELUDE, rustStr, sameTy, snake, STR, UNDEF, withAbsence } from "./model.js";
2
+ import { ONE_NOTHING } from "./typescript.js";
3
+ import { ctx } from "./context.js";
4
+ import { numberVal } from "./numbers.js";
5
+ import { structLifetime } from "./rust.js";
6
+ const files = new WeakMap();
7
+ const vals = new WeakMap();
8
+ const evaluated = new WeakMap();
9
+ const inProgress = new WeakSet();
10
+ /** Remember the constants and enums a file declares at its top level, to evaluate when one is used. */
11
+ export function declareConsts(home, body) {
12
+ const found = new Map();
13
+ for (const st of body) {
14
+ const exported = st.type === "ExportNamedDeclaration";
15
+ const d = exported ? st.declaration : st;
16
+ if (d?.type === "TSEnumDeclaration")
17
+ found.set(d.id.name, { node: d, home, exported });
18
+ if (d?.type === "VariableDeclaration" && d.kind === "const") {
19
+ for (const v of d.declarations)
20
+ if (v.id.type === "Identifier" && v.init)
21
+ found.set(v.id.name, { node: v, home, exported });
22
+ }
23
+ }
24
+ files.set(home, found);
25
+ return found;
26
+ }
27
+ /** What a declared constant or enum stands for, evaluated once. */
28
+ export function constOfDecl(d) {
29
+ const done = evaluated.get(d.node);
30
+ if (done)
31
+ return done;
32
+ if (inProgress.has(d.node))
33
+ fail(d.home, "FV0201", "a constant that refers to itself", d.node);
34
+ inProgress.add(d.node);
35
+ try {
36
+ const c = d.node.type === "TSEnumDeclaration" ? enumConst(d.home, d.node) : evaluate(d.home, d.node.init, d.node.id.name, typedAs(d.node));
37
+ evaluated.set(d.node, c);
38
+ return c;
39
+ }
40
+ finally {
41
+ inProgress.delete(d.node);
42
+ }
43
+ }
44
+ function typedAs(decl) {
45
+ const names = [decl.id.typeAnnotation?.typeAnnotation];
46
+ for (let n = decl.init; n?.type === "TSAsExpression" || n?.type === "TSSatisfiesExpression"; n = n.expression)
47
+ names.push(n.typeAnnotation);
48
+ for (let t of names) {
49
+ if (t?.type === "TSTypeOperator")
50
+ t = t.typeAnnotation;
51
+ const item = t?.type === "TSArrayType" ? t.elementType : t?.type === "TSTypeReference" && ["Array", "ReadonlyArray"].includes(t.typeName.name) ? t.typeParameters?.params?.[0] : null;
52
+ if (item?.type === "TSTypeReference" && item.typeName.type === "Identifier" && ctx.typeStructs.has(item.typeName.name))
53
+ return item.typeName.name;
54
+ }
55
+ return null;
56
+ }
57
+ function enumConst(home, d) {
58
+ const fields = new Map();
59
+ let next = 0;
60
+ for (const m of d.members) {
61
+ const key = m.id.type === "Identifier" ? m.id.name : m.id.value;
62
+ let value;
63
+ if (!m.initializer) {
64
+ if (next === null)
65
+ fail(home, "FV0202", `enum member \`${key}\` follows a string member, so it needs a value`, m);
66
+ value = next;
67
+ }
68
+ else {
69
+ const c = evaluate(home, m.initializer, key, null);
70
+ if (c.k !== "scalar" || typeof c.js === "boolean" || c.js === null)
71
+ return fail(home, "FV0203", "an enum member's value is a string or a number literal", m.initializer);
72
+ value = c.js;
73
+ }
74
+ fields.set(key, { k: "scalar", js: value });
75
+ if (typeof value === "number")
76
+ fields.set(String(value), { k: "scalar", js: key });
77
+ next = typeof value === "number" ? value + 1 : null;
78
+ }
79
+ return { k: "object", fields, node: d, home, name: d.id.name };
80
+ }
81
+ /** The type an enum stands for: the union of its members' literal types. */
82
+ export function enumType(home, d) {
83
+ const c = enumConst(home, d);
84
+ const members = c.k === "object" ? d.members.map((m) => c.fields.get(m.id.type === "Identifier" ? m.id.name : m.id.value)) : [];
85
+ const literal = (js) => typeof js === "string" ? { type: "StringLiteral", value: js } : { type: "NumericLiteral", value: js };
86
+ return { type: "TSUnionType", types: members.map((m) => ({ type: "TSLiteralType", literal: literal(m.js), loc: d.loc, __fv: d.__fv })), loc: d.loc, __fv: d.__fv };
87
+ }
88
+ function evaluate(home, n, path, typed) {
89
+ switch (n.type) {
90
+ case "TSAsExpression":
91
+ case "TSSatisfiesExpression":
92
+ case "TSNonNullExpression":
93
+ case "ParenthesizedExpression":
94
+ return evaluate(home, n.expression, path, typed);
95
+ case "StringLiteral":
96
+ case "NumericLiteral":
97
+ case "BooleanLiteral":
98
+ return { k: "scalar", js: n.value };
99
+ case "NullLiteral":
100
+ return { k: "scalar", js: null };
101
+ case "TemplateLiteral":
102
+ if (n.expressions.length === 0)
103
+ return { k: "scalar", js: n.quasis[0].value.cooked };
104
+ break;
105
+ case "UnaryExpression":
106
+ if (n.operator === "-" && n.argument.type === "NumericLiteral")
107
+ return { k: "scalar", js: -n.argument.value };
108
+ break;
109
+ case "Identifier": {
110
+ const d = files.get(home)?.get(n.name);
111
+ if (d)
112
+ return constOfDecl(d);
113
+ break;
114
+ }
115
+ case "MemberExpression": {
116
+ const key = n.computed ? (n.property.type === "StringLiteral" || n.property.type === "NumericLiteral" ? String(n.property.value) : null) : n.property.name;
117
+ const base = evaluate(home, n.object, path, null);
118
+ if (base.k === "object" && key !== null) {
119
+ const f = base.fields.get(key);
120
+ if (f)
121
+ return f;
122
+ return fail(home, "FV0204", `\`${base.name}\` has no field \`${key}\``, n.property);
123
+ }
124
+ break;
125
+ }
126
+ case "ArrayExpression":
127
+ return {
128
+ k: "list",
129
+ items: n.elements.map((el) => (el && el.type !== "SpreadElement" ? evaluate(home, el, path, null) : fail(home, "FV0205", "a constant list holds plain values", n))),
130
+ node: n,
131
+ home,
132
+ path,
133
+ typed,
134
+ };
135
+ case "ObjectExpression": {
136
+ const fields = new Map();
137
+ for (const p of n.properties) {
138
+ if (p.type !== "ObjectProperty" || p.computed)
139
+ fail(home, "FV0206", "a constant object holds plain keys and values", p);
140
+ const key = String(p.key.name ?? p.key.value);
141
+ fields.set(key, evaluate(home, p.value, `${path}_${key}`, null));
142
+ }
143
+ return { k: "object", fields, node: n, home, name: path };
144
+ }
145
+ }
146
+ return fail(home, "FV0207", "a constant the compiler evaluates is a literal, a list or an object of literals, or another such constant of the same file", n);
147
+ }
148
+ function scalarVal(js, float = false) {
149
+ if (js === null)
150
+ return { code: "None", ty: NULL };
151
+ if (typeof js === "string")
152
+ return { code: rustStr(js), ty: STR };
153
+ if (typeof js === "boolean")
154
+ return { code: String(js), ty: BOOL, konst: js };
155
+ return numberVal(js, !float && Number.isSafeInteger(js));
156
+ }
157
+ function scalarTy(js) {
158
+ if (js === null)
159
+ return NULL;
160
+ return typeof js === "string" ? STR : typeof js === "boolean" ? BOOL : Number.isSafeInteger(js) ? INT : FLOAT;
161
+ }
162
+ /** What a constant is as a value in Rust; an object has none, only its fields. */
163
+ export function constVal(c, at) {
164
+ if (c.k === "scalar")
165
+ return scalarVal(c.js);
166
+ if (c.k === "object")
167
+ return fail(at.comp, "FV0208", `\`${at.what}\` is an object, read one field at a time: \`${at.what}.${[...c.fields.keys()][0] ?? "x"}\``, at.node);
168
+ const done = vals.get(c);
169
+ if (done)
170
+ return done;
171
+ const val = listVal(c);
172
+ vals.set(c, val);
173
+ return val;
174
+ }
175
+ function listVal(c) {
176
+ if (c.items.length === 0)
177
+ return { code: "[]", ty: { k: "list", of: UNDEF } };
178
+ if (c.items.every((i) => i.k === "scalar")) {
179
+ const js = c.items.map((i) => i.js);
180
+ if (js.includes(null))
181
+ fail(c.home, "FV0209", "a constant list of strings, numbers or booleans holds no `null`", c.node);
182
+ const tys = js.map(scalarTy);
183
+ const numbers = tys.every((t) => t.k === "int" || t.k === "float");
184
+ const float = numbers && tys.some((t) => t.k === "float");
185
+ if (!numbers && tys.some((t) => !sameTy(t, tys[0])))
186
+ fail(c.home, "FV0210", "a constant list holds strings, numbers or booleans, all of one type", c.node);
187
+ const values = js.map((v) => scalarVal(v, float));
188
+ return { code: `[${values.map((v) => v.code).join(", ")}]`, ty: { k: "list", of: values[0].ty } };
189
+ }
190
+ if (!c.items.every((i) => i.k === "object"))
191
+ fail(c.home, "FV0211", "a constant list holds plain values of one kind, or objects", c.node);
192
+ const objects = c.items;
193
+ const st = c.typed !== null ? ctx.typeStructs.get(c.typed) : itemStruct(c, objects);
194
+ const name = rustConstName(c.path);
195
+ const owner = ctx.typeConsts.get(name);
196
+ if (owner && owner.of !== c)
197
+ fail(c.home, "FV0212", `a constant list called \`${name}\` in Rust comes from ${owner.file} too; rename one`, c.node);
198
+ const life = structLifetime(st, blankComponent("types", "types", "types", ctx.typeStructs)) ? "<'static>" : "";
199
+ const rows = objects.map((o, i) => {
200
+ for (const key of o.fields.keys())
201
+ if (!st.fields.some((f) => f.js === key))
202
+ fail(c.home, "FV0204", `\`${st.name}\` has no field \`${key}\``, o.node);
203
+ const inits = st.fields.map((f) => `${f.rust}: ${fieldLiteral(c, f, o.fields.get(f.js), objects[i].node)}`);
204
+ return ` ${st.name} { ${inits.join(", ")} },`;
205
+ });
206
+ ctx.typeConsts.set(name, { of: c, file: c.home.file, text: `/// \`${c.path.replaceAll("_", ".")}\` in \`${c.home.file}\`.\npub const ${name}: &[${st.name}${life}] = &[\n${rows.join("\n")}\n];\n` });
207
+ return { code: `super::types::${name}`, ty: { k: "list", of: { k: "struct", name: st.name, home: "types" } } };
208
+ }
209
+ function fieldLiteral(c, f, v, node) {
210
+ const want = f.ty.k === "opt" ? f.ty.of : f.ty;
211
+ const none = f.ty.k === "opt" ? (f.ty.none ?? "undefined") : null;
212
+ if (v === undefined)
213
+ return none === "undefined" || none === "either" ? "None" : fail(c.home, "FV0213", `this object needs \`${f.js}\``, node);
214
+ if (v.k === "scalar" && v.js === null)
215
+ return none === "null" || none === "either" ? "None" : fail(c.home, "FV0214", `\`${f.js}\` in a constant list's object is not \`null\``, node);
216
+ const plain = v.k === "scalar" && (scalarTy(v.js).k === want.k || (want.k === "float" && typeof v.js === "number"));
217
+ if (!plain || v.k !== "scalar")
218
+ return fail(c.home, "FV0215", `\`${f.js}\` in a constant list's object is ${describe(want)}`, node);
219
+ const code = typeof v.js === "string" ? `Cow::Borrowed(${rustStr(v.js)})` : typeof v.js === "number" ? numberVal(v.js, want.k === "int").code.replace(/i64$/, "") : String(v.js);
220
+ return f.ty.k === "opt" ? `Some(${code})` : code;
221
+ }
222
+ function describe(t) {
223
+ return t.k === "str" ? "a string" : t.k === "bool" ? "a boolean" : t.k === "int" || t.k === "float" ? "a number" : "a string, a number or a boolean";
224
+ }
225
+ function itemStruct(c, objects) {
226
+ const name = `${pascal(c.path)}Item`;
227
+ const keys = [...new Set(objects.flatMap((o) => [...o.fields.keys()]))];
228
+ const fields = keys.map((key) => {
229
+ const values = objects.map((o) => o.fields.get(key));
230
+ const present = values.filter((v) => v !== undefined);
231
+ if (present.some((v) => v.k !== "scalar"))
232
+ fail(c.home, "FV0216", `\`${key}\` in a constant list's objects is a string, a number or a boolean`, c.node);
233
+ const nulls = present.filter((v) => v.js === null).length;
234
+ const tys = present.filter((v) => v.js !== null).map((v) => scalarTy(v.js));
235
+ if (!tys.length)
236
+ fail(c.home, "FV0217", `\`${key}\` in a constant list's objects is only ever \`null\`: declare the list with an interface that gives its type`, c.node);
237
+ if (nulls && present.length < values.length)
238
+ fail(c.home, "FV0218", `\`${key}\` in a constant list's objects is \`null\` in some and absent in others: ${ONE_NOTHING}; write \`null\` in each`, c.node);
239
+ const numbers = tys.every((t) => t.k === "int" || t.k === "float");
240
+ const ty = numbers && tys.some((t) => t.k === "float") ? FLOAT : tys[0];
241
+ if (!numbers && tys.some((t) => !sameTy(t, ty)))
242
+ fail(c.home, "FV0219", `\`${key}\` in a constant list's objects holds values of different types`, c.node);
243
+ return { js: key, rust: snake(key), ty: withAbsence(ty, nulls ? "null" : present.length < values.length ? "undefined" : null) };
244
+ });
245
+ const existing = ctx.typeStructs.get(name);
246
+ if (existing && ctx.typeFiles.get(name) !== c.home.file)
247
+ fail(c.home, "FV0220", `\`${name}\`, the type of this list's objects, is declared by ${ctx.typeFiles.get(name)} too`, c.node);
248
+ if (RUST_PRELUDE.has(name))
249
+ fail(c.home, "FV0221", `\`${name}\`, the type of this list's objects, would hide Rust's own; rename the constant`, c.node);
250
+ const st = { name, fields };
251
+ ctx.typeStructs.set(name, st);
252
+ ctx.typeFiles.set(name, c.home.file);
253
+ return st;
254
+ }
255
+ function pascal(path) {
256
+ return path
257
+ .split(/_|(?=[A-Z][a-z])/)
258
+ .filter(Boolean)
259
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
260
+ .join("");
261
+ }
262
+ function rustConstName(path) {
263
+ return path
264
+ .split("_")
265
+ .map((w) => (/^[A-Z0-9]+$/.test(w) ? w : snake(w).replace(/^r#/, "").toUpperCase()))
266
+ .join("_");
267
+ }
268
+ /** The constant an expression names: an imported or enum binding, or a field of one. */
269
+ export function constOf(s, n) {
270
+ if (n.type === "Identifier") {
271
+ const c = !s.locals.has(n.name) && !s.setup.has(n.name) ? s.consts.get(n.name) : undefined;
272
+ return c ? { c, what: n.name } : null;
273
+ }
274
+ if (n.type !== "MemberExpression")
275
+ return null;
276
+ if (n.object.type === "Identifier" && (n.object.name === "$setup" || n.object.name === "_ctx")) {
277
+ const name = n.computed ? (n.property.type === "StringLiteral" ? n.property.value : undefined) : n.property.name;
278
+ const c = name !== undefined ? s.consts.get(name) : undefined;
279
+ return c && name !== undefined ? { c, what: name } : null;
280
+ }
281
+ const base = constOf(s, n.object);
282
+ if (base?.c.k !== "object")
283
+ return null;
284
+ const key = n.computed ? (n.property.type === "StringLiteral" || n.property.type === "NumericLiteral" ? String(n.property.value) : null) : n.property.name;
285
+ if (key === null) {
286
+ return fail(s.comp, "FV0222", `\`${base.what}\` read by a key chosen at run time, which may name none of its fields; write the choices out with \`v-if\`, or pass the value as a prop`, n);
287
+ }
288
+ const f = base.c.fields.get(key);
289
+ if (!f)
290
+ return fail(s.comp, "FV0204", `\`${base.what}\` has no field \`${key}\``, n);
291
+ return { c: f, what: `${base.what}.${key}` };
292
+ }
package/dist/context.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Plugin } from "./plugin.ts";
2
+ import type { Declared } from "./constants.ts";
2
3
  import { type Component, type Struct, type Ty } from "./model.ts";
3
4
  /** A type a helper takes or returns, as the configuration spells it. */
4
5
  export type TypeName = "string" | "string?" | "int" | "int?" | "float" | "float?" | "bool";
@@ -11,6 +12,16 @@ export interface HelperSpec {
11
12
  /** The longest string it returns, which is what a call adds to the buffer reservation. */
12
13
  maxLen?: number;
13
14
  }
15
+ /** A component ferrovue does not compile, rendered on the server by a Rust function of the
16
+ * project's: its twin. ferrovue cannot check that the function writes what Vue would. */
17
+ export interface TwinSpec {
18
+ /** The Rust path the generated parent calls, e.g. `crate::ui::v_btn`. */
19
+ rust: string;
20
+ /** Each prop a template may pass it, by its name in Vue, with its type. */
21
+ props?: Record<string, TypeName>;
22
+ /** The slots a template may fill, by name: `["default", "prepend"]`. */
23
+ slots?: string[];
24
+ }
14
25
  /** What `ferrovue.config.json` holds. Paths are relative to the project root. */
15
26
  export interface Config {
16
27
  /** The directory of `.vue` files to compile. */
@@ -22,6 +33,9 @@ export interface Config {
22
33
  module: string;
23
34
  functions: Record<string, HelperSpec>;
24
35
  };
36
+ /** Components ferrovue does not compile, by the name a template gives them (`VBtn`, which
37
+ * `<v-btn>` finds too), each rendered by a Rust function of the project's. */
38
+ twins?: Record<string, TwinSpec>;
25
39
  /** The Rust type a `TrustedHtml` prop is, which must implement `ferrovue::TrustedHtml`, e.g.
26
40
  * `crate::sanitize::SafeHtml`. A type that borrows names the props' lifetime, `'a`:
27
41
  * `crate::render::Trusted<'a>`. Without it, a component cannot use `v-html` at all. */
@@ -40,7 +54,7 @@ export interface Config {
40
54
  /** The directory of Pinia stores (`.ts` files), whose state a component may read while it renders
41
55
  * on the server. */
42
56
  stores?: string;
43
- /** Custom directives that render nothing on the server — no `getSSRProps` — by name, without
57
+ /** Custom directives that render nothing on the server (no `getSSRProps`), by name, without
44
58
  * `v-`: `["focus", "click-outside"]`. Any other custom directive is refused, since the server
45
59
  * cannot know what its `getSSRProps` would add. */
46
60
  clientDirectives?: string[];
@@ -58,6 +72,9 @@ export interface Config {
58
72
  scopeId?: ScopeIdMode;
59
73
  /** Vite's root, from which a scope id hashes a component's path: the project root by default. */
60
74
  viteRoot?: string;
75
+ /** Whether each props struct and shared type gets `new()` and a chainable setter per optional
76
+ * field: `true` by default. With `false`, props are built as struct literals. */
77
+ builders?: boolean;
61
78
  }
62
79
  export type ScopeIdMode = "filepath" | "filepath-source";
63
80
  export declare const CONFIG_FILE = "ferrovue.config.json";
@@ -75,6 +92,7 @@ export declare const ctx: {
75
92
  }>;
76
93
  helperModule: string | null;
77
94
  trustedHtml: string | null;
95
+ builders: boolean;
78
96
  clientDirectives: Set<string>;
79
97
  plugins: readonly Plugin[];
80
98
  runs: Map<Plugin<unknown, unknown>, unknown>;
@@ -83,6 +101,12 @@ export declare const ctx: {
83
101
  typeAliases: Map<string, any>;
84
102
  typeFiles: Map<string, string>;
85
103
  typeRead: Set<string>;
104
+ constDecls: Map<string, Map<string, Declared>>;
105
+ typeConsts: Map<string, {
106
+ of: unknown;
107
+ file: string;
108
+ text: string;
109
+ }>;
86
110
  components: Map<string, Component>;
87
111
  narrowCount: number;
88
112
  componentsDir: string;
package/dist/context.js CHANGED
@@ -12,7 +12,7 @@ export function loadConfig(root, configPath) {
12
12
  }
13
13
  catch (e) {
14
14
  if (e.code === "ENOENT") {
15
- throw new GenError(`cannot find \`${displayName}\` in ${root}`);
15
+ throw new GenError("FV1102", `cannot find \`${displayName}\` in ${root}`, { file: displayName });
16
16
  }
17
17
  throw e;
18
18
  }
@@ -21,13 +21,16 @@ export function loadConfig(root, configPath) {
21
21
  raw = JSON.parse(text);
22
22
  }
23
23
  catch (e) {
24
- throw new GenError(`failed to parse \`${displayName}\`: ${e.message}`);
24
+ throw new GenError("FV1103", `failed to parse \`${displayName}\`: ${e.message}`, { file: displayName });
25
25
  }
26
26
  if (typeof raw.components !== "string" || typeof raw.out !== "string") {
27
- throw new GenError(`${displayName} needs \`components\` and \`out\` directories`);
27
+ throw new GenError("FV1104", `${displayName} needs \`components\` and \`out\` directories`, { file: displayName });
28
28
  }
29
29
  if (raw.scopeId !== undefined && raw.scopeId !== "filepath" && raw.scopeId !== "filepath-source") {
30
- throw new GenError(`\`scopeId\` in ${displayName} is "filepath" or "filepath-source", as \`@vitejs/plugin-vue\` computes it`);
30
+ throw new GenError("FV1105", `\`scopeId\` in ${displayName} is "filepath" or "filepath-source", as \`@vitejs/plugin-vue\` computes it`, { file: displayName });
31
+ }
32
+ if (raw.builders !== undefined && typeof raw.builders !== "boolean") {
33
+ throw new GenError("FV1106", `\`builders\` in ${displayName} is \`true\` or \`false\`: whether each props struct gets \`new()\` and a setter per optional field`, { file: displayName });
31
34
  }
32
35
  return raw;
33
36
  }
@@ -48,7 +51,7 @@ export function tyOfName(name) {
48
51
  case "bool":
49
52
  return BOOL;
50
53
  default:
51
- throw new GenError(`unknown helper type \`${String(name)}\``);
54
+ throw new GenError("FV1107", `unknown helper type \`${String(name)}\``);
52
55
  }
53
56
  }
54
57
  /** Where `TrustedHtml` comes from. Only that import names the type `v-html` will write raw. */
@@ -57,6 +60,7 @@ export const ctx = {
57
60
  helpers: {},
58
61
  helperModule: null,
59
62
  trustedHtml: null,
63
+ builders: true,
60
64
  clientDirectives: new Set(),
61
65
  plugins: [],
62
66
  runs: new Map(),
@@ -65,6 +69,8 @@ export const ctx = {
65
69
  typeAliases: new Map(),
66
70
  typeFiles: new Map(),
67
71
  typeRead: new Set(),
72
+ constDecls: new Map(),
73
+ typeConsts: new Map(),
68
74
  components: new Map(),
69
75
  narrowCount: 0,
70
76
  componentsDir: "",
@@ -0,0 +1,23 @@
1
+ import { type Code } from "./errors.ts";
2
+ import { GenError } from "./model.ts";
3
+ /** One refusal, as `ferrovue --format json` writes it. Lines and columns count from 1; the end is
4
+ * exclusive, and `null` where it is not known. */
5
+ export interface Diagnostic {
6
+ file: string | null;
7
+ line: number | null;
8
+ column: number | null;
9
+ endLine: number | null;
10
+ endColumn: number | null;
11
+ code: Code;
12
+ severity: "error";
13
+ title: string;
14
+ message: string;
15
+ docs: string;
16
+ }
17
+ /** Whether the compiler raised `e` for its input: a refusal, or a file that does not parse. */
18
+ export declare function isRefusal(e: unknown): e is GenError | SyntaxError;
19
+ /** The diagnostic for a refusal. */
20
+ export declare function diagnose(e: GenError | SyntaxError): Diagnostic;
21
+ /** A refusal as the CLI and the Vite plugin show it: `error[FV0604]: `, the message with its
22
+ * location and quoted line, and where the code is documented. */
23
+ export declare function formatRefusal(e: GenError | SyntaxError): string;
@@ -0,0 +1,30 @@
1
+ import { docsUrl, ERRORS } from "./errors.js";
2
+ import { GenError } from "./model.js";
3
+ const UNPARSED = "FV0001";
4
+ /** Whether the compiler raised `e` for its input: a refusal, or a file that does not parse. */
5
+ export function isRefusal(e) {
6
+ return e instanceof GenError || e instanceof SyntaxError;
7
+ }
8
+ /** The diagnostic for a refusal. */
9
+ export function diagnose(e) {
10
+ const code = e instanceof GenError ? e.code : UNPARSED;
11
+ const at = e instanceof GenError ? e.at : null;
12
+ return {
13
+ file: at?.file ?? null,
14
+ line: at?.line ?? null,
15
+ column: at?.column ?? null,
16
+ endLine: at?.endLine ?? null,
17
+ endColumn: at?.endColumn ?? null,
18
+ code,
19
+ severity: "error",
20
+ title: ERRORS[code].title,
21
+ message: e instanceof GenError ? e.what : e.message.split("\n")[0],
22
+ docs: docsUrl(code),
23
+ };
24
+ }
25
+ /** A refusal as the CLI and the Vite plugin show it: `error[FV0604]: `, the message with its
26
+ * location and quoted line, and where the code is documented. */
27
+ export function formatRefusal(e) {
28
+ const code = e instanceof GenError ? e.code : UNPARSED;
29
+ return `error[${code}]: ${e.message}\n = docs: ${docsUrl(code)}`;
30
+ }