ferrovue 0.3.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 (106) hide show
  1. package/README.md +225 -42
  2. package/dist/attrs.d.ts +3 -53
  3. package/dist/attrs.js +41 -469
  4. package/dist/calls.d.ts +4 -0
  5. package/dist/calls.js +221 -0
  6. package/dist/children.d.ts +16 -0
  7. package/dist/children.js +286 -0
  8. package/dist/classes.d.ts +13 -0
  9. package/dist/classes.js +176 -0
  10. package/dist/cli.js +31 -22
  11. package/dist/client.d.ts +35 -2
  12. package/dist/client.js +77 -14
  13. package/dist/compiler.d.ts +1 -1
  14. package/dist/compiler.js +31 -92
  15. package/dist/component.d.ts +3 -1
  16. package/dist/component.js +123 -104
  17. package/dist/constants.d.ts +42 -0
  18. package/dist/constants.js +292 -0
  19. package/dist/context.d.ts +29 -67
  20. package/dist/context.js +13 -38
  21. package/dist/diagnostics.d.ts +23 -0
  22. package/dist/diagnostics.js +30 -0
  23. package/dist/diff.d.ts +0 -3
  24. package/dist/diff.js +0 -9
  25. package/dist/emitter.d.ts +0 -8
  26. package/dist/emitter.js +0 -12
  27. package/dist/errors.d.ts +867 -0
  28. package/dist/errors.js +330 -0
  29. package/dist/expr.d.ts +1 -70
  30. package/dist/expr.js +142 -934
  31. package/dist/fallthrough.d.ts +9 -0
  32. package/dist/fallthrough.js +78 -0
  33. package/dist/fixture.d.ts +4 -10
  34. package/dist/fixture.js +38 -27
  35. package/dist/hydration.d.ts +15 -0
  36. package/dist/hydration.js +80 -0
  37. package/dist/index.d.ts +3 -0
  38. package/dist/index.js +2 -0
  39. package/dist/islands.d.ts +1 -0
  40. package/dist/islands.js +1 -0
  41. package/dist/link-router.d.ts +24 -0
  42. package/dist/link-router.js +36 -0
  43. package/dist/lists.d.ts +0 -11
  44. package/dist/lists.js +34 -60
  45. package/dist/loops.d.ts +3 -0
  46. package/dist/loops.js +168 -0
  47. package/dist/model.d.ts +46 -144
  48. package/dist/model.js +150 -18
  49. package/dist/narrowing.d.ts +27 -0
  50. package/dist/narrowing.js +157 -0
  51. package/dist/numbers.d.ts +8 -0
  52. package/dist/numbers.js +73 -0
  53. package/dist/parens.d.ts +0 -27
  54. package/dist/parens.js +0 -44
  55. package/dist/plugin.d.ts +84 -0
  56. package/dist/plugin.js +26 -0
  57. package/dist/plugins/client-only.d.ts +8 -0
  58. package/dist/plugins/client-only.js +107 -0
  59. package/dist/plugins/i18n.d.ts +23 -0
  60. package/dist/{i18n.js → plugins/i18n.js} +88 -31
  61. package/dist/plugins/index.d.ts +2 -0
  62. package/dist/plugins/index.js +10 -0
  63. package/dist/plugins/router-link.d.ts +8 -0
  64. package/dist/{router.js → plugins/router-link.js} +52 -91
  65. package/dist/plugins/router.d.ts +42 -0
  66. package/dist/plugins/router.js +256 -0
  67. package/dist/plugins/scoped.d.ts +12 -0
  68. package/dist/plugins/scoped.js +125 -0
  69. package/dist/plugins/shared-types.d.ts +2 -0
  70. package/dist/plugins/shared-types.js +7 -0
  71. package/dist/plugins/stores.d.ts +37 -0
  72. package/dist/plugins/stores.js +379 -0
  73. package/dist/plugins/teleport.d.ts +5 -0
  74. package/dist/plugins/teleport.js +55 -0
  75. package/dist/plugins/twins.d.ts +10 -0
  76. package/dist/plugins/twins.js +164 -0
  77. package/dist/routes.d.ts +10 -0
  78. package/dist/routes.js +6 -0
  79. package/dist/rust.d.ts +1 -7
  80. package/dist/rust.js +56 -174
  81. package/dist/script.d.ts +3 -6
  82. package/dist/script.js +127 -140
  83. package/dist/settle.d.ts +6 -0
  84. package/dist/settle.js +34 -0
  85. package/dist/slots.d.ts +13 -0
  86. package/dist/slots.js +160 -0
  87. package/dist/strings.d.ts +13 -0
  88. package/dist/strings.js +208 -0
  89. package/dist/styles.d.ts +14 -0
  90. package/dist/styles.js +181 -0
  91. package/dist/template.d.ts +1 -42
  92. package/dist/template.js +25 -749
  93. package/dist/testing.d.ts +2 -0
  94. package/dist/testing.js +4 -10
  95. package/dist/types.d.ts +2 -0
  96. package/dist/typescript.d.ts +2 -14
  97. package/dist/typescript.js +53 -59
  98. package/dist/vite.d.ts +2 -1
  99. package/dist/vite.js +11 -32
  100. package/package.json +15 -6
  101. package/dist/i18n.d.ts +0 -27
  102. package/dist/router.d.ts +0 -17
  103. package/dist/scoped.d.ts +0 -24
  104. package/dist/scoped.js +0 -177
  105. package/dist/stores.d.ts +0 -6
  106. package/dist/stores.js +0 -229
@@ -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,5 +1,6 @@
1
- import type { I18nSetup } from "./i18n.ts";
2
- import { type Component, type N, type Struct, type Ty } from "./model.ts";
1
+ import type { Plugin } from "./plugin.ts";
2
+ import type { Declared } from "./constants.ts";
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";
5
6
  /** A function a template may call, and the Rust function that is its twin. */
@@ -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,54 +72,18 @@ 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";
64
81
  /** Read the project's configuration. */
65
82
  export declare function loadConfig(root: string, configPath?: string): Config;
66
83
  export declare function tyOfName(name: TypeName): Ty;
67
- /** A route as the routes file lists it: a vue-router path, and the name it may have. */
68
- export interface RouteDef {
69
- /** The path as written: relative to the parent's unless it starts with \`/\`. */
70
- path: string;
71
- name?: string;
72
- /** The routes nested in it. */
73
- children?: RouteDef[];
74
- /** The path with its ancestors', as vue-router normalises it. */
75
- fullPath: string;
76
- }
77
- /** Every route, nested ones included, parents first. */
78
- export declare function allRoutes(routes: RouteDef[]): RouteDef[];
79
- /** A getter: the parameter naming the state (an option store's), and the expression it returns.
80
- * \`setup\` marks a setup store's \`computed\`, which reads the state's refs and the other getters by
81
- * name, through \`.value\`. */
82
- export interface StoreGetter {
83
- param: string | null;
84
- body: N;
85
- file: string;
86
- setup?: true;
87
- }
88
- /** A Pinia option store: `export const usePrefs = defineStore("prefs", { state: (): PrefsState => ... })`. */
89
- export interface Store {
90
- /** `usePrefs` */
91
- hook: string;
92
- /** `prefs`: its key in `pinia.state.value`. */
93
- id: string;
94
- /** `prefs`: its field in the generated `Stores`. */
95
- field: string;
96
- /** `PrefsState`: the interface its state is. */
97
- state: string;
98
- /** The file, without its extension, as an import names it once resolved. */
99
- module: string;
100
- /** Its getters: name → the parameter that names the state, and the expression returned. */
101
- getters: Map<string, StoreGetter>;
102
- }
103
84
  /** Where `TrustedHtml` comes from. Only that import names the type `v-html` will write raw. */
104
85
  export declare const TYPES_MODULE = "ferrovue/types";
105
- /** What the configuration being compiled says, and what has been read so far: set up by
106
- * `generate()` at the start of each run, which is synchronous, so one run never sees another's. */
107
86
  export declare const ctx: {
108
- /** The helpers of the configuration being compiled: name → Rust twin, typed. */
109
87
  helpers: Record<string, {
110
88
  rust: string;
111
89
  params: Ty[];
@@ -114,38 +92,22 @@ export declare const ctx: {
114
92
  }>;
115
93
  helperModule: string | null;
116
94
  trustedHtml: string | null;
117
- /** Custom directives declared to render nothing on the server. */
95
+ builders: boolean;
118
96
  clientDirectives: Set<string>;
119
- /** The configured routes, when there are any. */
120
- routes: RouteDef[] | null;
121
- /** The history's base, and the class names active links take, from the configured router. */
122
- routerBase: string;
123
- linkActive: string;
124
- linkExactActive: string;
125
- /** The configured stores, by hook name, and every interface their files declare. */
126
- stores: Map<string, Store>;
127
- storeStructs: Map<string, Struct>;
128
- /** The store file each of those interfaces is declared in. */
129
- storeFiles: Map<string, string>;
130
- /** The project root, for resolving a component's imports. */
97
+ plugins: readonly Plugin[];
98
+ runs: Map<Plugin<unknown, unknown>, unknown>;
131
99
  rootDir: string;
132
- /** Interfaces and object types declared in shared `.ts` files that components import, and type
133
- * aliases there; the file each comes from. They are written once, to `types.rs`. */
134
100
  typeStructs: Map<string, Struct>;
135
101
  typeAliases: Map<string, any>;
136
102
  typeFiles: Map<string, string>;
137
- /** The shared files read so far. */
138
103
  typeRead: Set<string>;
139
- /** Every component being compiled, by name, for a type that names another one's props. */
104
+ constDecls: Map<string, Map<string, Declared>>;
105
+ typeConsts: Map<string, {
106
+ of: unknown;
107
+ file: string;
108
+ text: string;
109
+ }>;
140
110
  components: Map<string, Component>;
141
- /** Numbers the bindings narrowing introduces, restarted per component so that a file's text
142
- * depends on its own source alone. */
143
111
  narrowCount: number;
144
- /** The directory the components being compiled come from, for the generated headers. */
145
112
  componentsDir: string;
146
- /** The configured locales and their messages, when there are any. */
147
- i18n: I18nSetup | null;
148
- /** How scope ids are computed, and the directory a component's path is hashed from. */
149
- scopeId: ScopeIdMode;
150
- viteRoot: string;
151
113
  };
package/dist/context.js CHANGED
@@ -1,4 +1,3 @@
1
- /* The project's configuration, and the state one `generate()` run builds up. */
2
1
  import { readFileSync } from "node:fs";
3
2
  import { join, resolve } from "node:path";
4
3
  import { BOOL, FLOAT, GenError, INT, opt, STR } from "./model.js";
@@ -13,7 +12,7 @@ export function loadConfig(root, configPath) {
13
12
  }
14
13
  catch (e) {
15
14
  if (e.code === "ENOENT") {
16
- throw new GenError(`cannot find \`${displayName}\` in ${root}`);
15
+ throw new GenError("FV1102", `cannot find \`${displayName}\` in ${root}`, { file: displayName });
17
16
  }
18
17
  throw e;
19
18
  }
@@ -22,13 +21,16 @@ export function loadConfig(root, configPath) {
22
21
  raw = JSON.parse(text);
23
22
  }
24
23
  catch (e) {
25
- throw new GenError(`failed to parse \`${displayName}\`: ${e.message}`);
24
+ throw new GenError("FV1103", `failed to parse \`${displayName}\`: ${e.message}`, { file: displayName });
26
25
  }
27
26
  if (typeof raw.components !== "string" || typeof raw.out !== "string") {
28
- throw new GenError(`${displayName} needs \`components\` and \`out\` directories`);
27
+ throw new GenError("FV1104", `${displayName} needs \`components\` and \`out\` directories`, { file: displayName });
29
28
  }
30
29
  if (raw.scopeId !== undefined && raw.scopeId !== "filepath" && raw.scopeId !== "filepath-source") {
31
- 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 });
32
34
  }
33
35
  return raw;
34
36
  }
@@ -49,54 +51,27 @@ export function tyOfName(name) {
49
51
  case "bool":
50
52
  return BOOL;
51
53
  default:
52
- throw new GenError(`unknown helper type \`${String(name)}\``);
54
+ throw new GenError("FV1107", `unknown helper type \`${String(name)}\``);
53
55
  }
54
56
  }
55
- /** Every route, nested ones included, parents first. */
56
- export function allRoutes(routes) {
57
- return routes.flatMap((r) => [r, ...allRoutes(r.children ?? [])]);
58
- }
59
57
  /** Where `TrustedHtml` comes from. Only that import names the type `v-html` will write raw. */
60
58
  export const TYPES_MODULE = "ferrovue/types";
61
- /** What the configuration being compiled says, and what has been read so far: set up by
62
- * `generate()` at the start of each run, which is synchronous, so one run never sees another's. */
63
59
  export const ctx = {
64
- /** The helpers of the configuration being compiled: name → Rust twin, typed. */
65
60
  helpers: {},
66
61
  helperModule: null,
67
62
  trustedHtml: null,
68
- /** Custom directives declared to render nothing on the server. */
63
+ builders: true,
69
64
  clientDirectives: new Set(),
70
- /** The configured routes, when there are any. */
71
- routes: null,
72
- /** The history's base, and the class names active links take, from the configured router. */
73
- routerBase: "",
74
- linkActive: "router-link-active",
75
- linkExactActive: "router-link-exact-active",
76
- /** The configured stores, by hook name, and every interface their files declare. */
77
- stores: new Map(),
78
- storeStructs: new Map(),
79
- /** The store file each of those interfaces is declared in. */
80
- storeFiles: new Map(),
81
- /** The project root, for resolving a component's imports. */
65
+ plugins: [],
66
+ runs: new Map(),
82
67
  rootDir: "",
83
- /** Interfaces and object types declared in shared `.ts` files that components import, and type
84
- * aliases there; the file each comes from. They are written once, to `types.rs`. */
85
68
  typeStructs: new Map(),
86
69
  typeAliases: new Map(),
87
70
  typeFiles: new Map(),
88
- /** The shared files read so far. */
89
71
  typeRead: new Set(),
90
- /** Every component being compiled, by name, for a type that names another one's props. */
72
+ constDecls: new Map(),
73
+ typeConsts: new Map(),
91
74
  components: new Map(),
92
- /** Numbers the bindings narrowing introduces, restarted per component so that a file's text
93
- * depends on its own source alone. */
94
75
  narrowCount: 0,
95
- /** The directory the components being compiled come from, for the generated headers. */
96
76
  componentsDir: "",
97
- /** The configured locales and their messages, when there are any. */
98
- i18n: null,
99
- /** How scope ids are computed, and the directory a component's path is hashed from. */
100
- scopeId: "filepath-source",
101
- viteRoot: "",
102
77
  };
@@ -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
+ }
package/dist/diff.d.ts CHANGED
@@ -1,4 +1 @@
1
- /** A unified diff of a module, as \`diff -u\` writes it, with three lines of context: what
2
- * \`ferrovue\` would write against what is committed. Lines the two share at the start and the end
3
- * are set aside first, so that a module changed in one place is compared over that place alone. */
4
1
  export declare function unifiedDiff(path: string, before: string, after: string): string;
package/dist/diff.js CHANGED
@@ -1,7 +1,3 @@
1
- /* `ferrovue --check --diff`: what would be written, against what is committed. */
2
- /** A unified diff of a module, as \`diff -u\` writes it, with three lines of context: what
3
- * \`ferrovue\` would write against what is committed. Lines the two share at the start and the end
4
- * are set aside first, so that a module changed in one place is compared over that place alone. */
5
1
  export function unifiedDiff(path, before, after) {
6
2
  const lines = (text) => (text === "" ? [] : text.replace(/\n$/, "").split("\n"));
7
3
  const a = lines(before);
@@ -16,7 +12,6 @@ export function unifiedDiff(path, before, after) {
16
12
  const midB = b.slice(head, b.length - tail);
17
13
  if (!midA.length && !midB.length)
18
14
  return "";
19
- // The longest common subsequence of the middle, by dynamic programming over its lines.
20
15
  const lcs = Array.from({ length: midA.length + 1 }, () => new Uint32Array(midB.length + 1));
21
16
  for (let i = midA.length - 1; i >= 0; i--) {
22
17
  for (let j = midB.length - 1; j >= 0; j--) {
@@ -26,7 +21,6 @@ export function unifiedDiff(path, before, after) {
26
21
  const edits = a.slice(0, head).map((text) => ({ op: " ", text }));
27
22
  let i = 0;
28
23
  let j = 0;
29
- // Of two ways to keep as many lines, a removal comes first, as `diff -u` orders them.
30
24
  while (i < midA.length || j < midB.length) {
31
25
  if (i < midA.length && j < midB.length && midA[i] === midB[j]) {
32
26
  edits.push({ op: " ", text: midA[i++] });
@@ -38,7 +32,6 @@ export function unifiedDiff(path, before, after) {
38
32
  edits.push({ op: "-", text: midA[i++] });
39
33
  }
40
34
  edits.push(...a.slice(a.length - tail).map((text) => ({ op: " ", text })));
41
- // Each changed line with three lines around it, runs that touch joined into one hunk.
42
35
  const CONTEXT = 3;
43
36
  const hunks = [];
44
37
  edits.forEach((e, k) => {
@@ -53,7 +46,6 @@ export function unifiedDiff(path, before, after) {
53
46
  hunks.push([from, to]);
54
47
  });
55
48
  const out = [`--- ${a.length ? `a/${path}` : "/dev/null"}`, `+++ ${b.length ? `b/${path}` : "/dev/null"}`];
56
- // Line numbers in each file where every edit starts.
57
49
  let lineA = 1;
58
50
  let lineB = 1;
59
51
  const at = edits.map((e) => {
@@ -68,7 +60,6 @@ export function unifiedDiff(path, before, after) {
68
60
  const span = edits.slice(from, to + 1);
69
61
  const countA = span.filter((e) => e.op !== "+").length;
70
62
  const countB = span.filter((e) => e.op !== "-").length;
71
- // An empty side is numbered by the line before it, as `diff -u` numbers it.
72
63
  const startA = countA ? at[from][0] : at[from][0] - 1;
73
64
  const startB = countB ? at[from][1] : at[from][1] - 1;
74
65
  out.push(`@@ -${startA},${countA} +${startB},${countB} @@`, ...span.map((e) => `${e.op}${e.text}`));