ferrovue 0.4.0 → 0.6.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 (92) hide show
  1. package/README.md +378 -63
  2. package/dist/attrs.d.ts +3 -2
  3. package/dist/attrs.js +31 -26
  4. package/dist/calls.js +17 -15
  5. package/dist/children.d.ts +11 -4
  6. package/dist/children.js +110 -44
  7. package/dist/classes.d.ts +4 -0
  8. package/dist/classes.js +33 -11
  9. package/dist/cli.js +31 -5
  10. package/dist/client.d.ts +44 -6
  11. package/dist/client.js +182 -18
  12. package/dist/compiler.d.ts +1 -1
  13. package/dist/compiler.js +42 -10
  14. package/dist/component.d.ts +3 -1
  15. package/dist/component.js +114 -20
  16. package/dist/conformance.d.ts +64 -0
  17. package/dist/conformance.js +276 -0
  18. package/dist/constants.d.ts +42 -0
  19. package/dist/constants.js +292 -0
  20. package/dist/context.d.ts +40 -7
  21. package/dist/context.js +14 -5
  22. package/dist/diagnostics.d.ts +23 -0
  23. package/dist/diagnostics.js +30 -0
  24. package/dist/dynamic.d.ts +18 -0
  25. package/dist/dynamic.js +576 -0
  26. package/dist/errors.d.ts +1034 -0
  27. package/dist/errors.js +425 -0
  28. package/dist/expr.d.ts +1 -0
  29. package/dist/expr.js +120 -49
  30. package/dist/file-routes.d.ts +33 -0
  31. package/dist/file-routes.js +224 -0
  32. package/dist/fixture.d.ts +19 -10
  33. package/dist/fixture.js +23 -12
  34. package/dist/hydration.d.ts +15 -0
  35. package/dist/hydration.js +80 -0
  36. package/dist/index.d.ts +3 -2
  37. package/dist/index.js +2 -1
  38. package/dist/link-router.d.ts +24 -0
  39. package/dist/link-router.js +37 -0
  40. package/dist/lists.d.ts +4 -0
  41. package/dist/lists.js +32 -23
  42. package/dist/loops.js +18 -14
  43. package/dist/model.d.ts +43 -1
  44. package/dist/model.js +167 -7
  45. package/dist/narrowing.d.ts +8 -0
  46. package/dist/narrowing.js +40 -9
  47. package/dist/numbers.js +3 -2
  48. package/dist/page-routes.d.ts +9 -0
  49. package/dist/page-routes.js +10 -0
  50. package/dist/plugin.d.ts +13 -1
  51. package/dist/plugin.js +3 -0
  52. package/dist/plugins/client-only.d.ts +8 -0
  53. package/dist/plugins/client-only.js +110 -0
  54. package/dist/plugins/head.d.ts +15 -0
  55. package/dist/plugins/head.js +385 -0
  56. package/dist/plugins/i18n.js +14 -14
  57. package/dist/plugins/index.js +5 -1
  58. package/dist/plugins/provide.d.ts +44 -0
  59. package/dist/plugins/provide.js +528 -0
  60. package/dist/plugins/router-link.js +39 -32
  61. package/dist/plugins/router.d.ts +4 -1
  62. package/dist/plugins/router.js +65 -19
  63. package/dist/plugins/scoped.js +1 -1
  64. package/dist/plugins/shared-types.js +1 -1
  65. package/dist/plugins/stores.js +45 -33
  66. package/dist/plugins/teleport.js +3 -3
  67. package/dist/plugins/twins.d.ts +10 -0
  68. package/dist/plugins/twins.js +164 -0
  69. package/dist/routes.d.ts +13 -0
  70. package/dist/routes.js +12 -0
  71. package/dist/rust.js +64 -32
  72. package/dist/script.d.ts +3 -0
  73. package/dist/script.js +138 -20
  74. package/dist/settle.d.ts +8 -0
  75. package/dist/settle.js +39 -0
  76. package/dist/slots.js +36 -22
  77. package/dist/ssr.d.ts +3 -0
  78. package/dist/ssr.js +29 -0
  79. package/dist/strings.d.ts +3 -0
  80. package/dist/strings.js +27 -13
  81. package/dist/styles.js +13 -13
  82. package/dist/template.js +102 -21
  83. package/dist/testing.d.ts +3 -3
  84. package/dist/testing.js +3 -29
  85. package/dist/types.d.ts +5 -0
  86. package/dist/typescript.d.ts +2 -1
  87. package/dist/typescript.js +52 -21
  88. package/dist/vhtml.d.ts +4 -0
  89. package/dist/vhtml.js +63 -0
  90. package/dist/vite.d.ts +8 -2
  91. package/dist/vite.js +75 -14
  92. package/package.json +23 -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,17 +33,22 @@ 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
- * `crate::render::Trusted<'a>`. Without it, a component cannot use `v-html` at all. */
41
+ * `crate::render::Trusted<'a>`. `ferrovue::InlineHtml` makes every `TrustedHtml` prop inline HTML.
42
+ * Without it, only an `InlineHtml` prop may reach `v-html`. */
28
43
  trustedHtml?: string;
29
44
  /** A JSON file listing the app's routes as vue-router paths (`/users/:id`), which `<RouterLink>`
30
- * resolves against. Without it, a component cannot use `<RouterLink>`. */
31
- routes?: string;
32
- /** The router, in full: its routes file, the history's base, and the class names
33
- * `createRouter` gives active links. `routes` alone is shorthand for `{ routes }`. */
45
+ * resolves against, or `{ pages }`: a folder of pages whose file names give the routes, as
46
+ * vue-router's file-based routing reads them. Without it, a component cannot use `<RouterLink>`. */
47
+ routes?: RoutesSource;
48
+ /** The router, in full: its routes, the history's base, and the class names `createRouter`
49
+ * gives active links. `routes` alone is shorthand for `{ routes }`. */
34
50
  router?: {
35
- routes: string;
51
+ routes: RoutesSource;
36
52
  base?: string;
37
53
  linkActiveClass?: string;
38
54
  linkExactActiveClass?: string;
@@ -40,7 +56,7 @@ export interface Config {
40
56
  /** The directory of Pinia stores (`.ts` files), whose state a component may read while it renders
41
57
  * on the server. */
42
58
  stores?: string;
43
- /** Custom directives that render nothing on the server — no `getSSRProps` — by name, without
59
+ /** Custom directives that render nothing on the server (no `getSSRProps`), by name, without
44
60
  * `v-`: `["focus", "click-outside"]`. Any other custom directive is refused, since the server
45
61
  * cannot know what its `getSSRProps` would add. */
46
62
  clientDirectives?: string[];
@@ -58,12 +74,21 @@ export interface Config {
58
74
  scopeId?: ScopeIdMode;
59
75
  /** Vite's root, from which a scope id hashes a component's path: the project root by default. */
60
76
  viteRoot?: string;
77
+ /** Whether each props struct and shared type gets `new()` and a chainable setter per optional
78
+ * field: `true` by default. With `false`, props are built as struct literals. */
79
+ builders?: boolean;
61
80
  }
62
81
  export type ScopeIdMode = "filepath" | "filepath-source";
82
+ /** Where the routes come from: a JSON file of routes, or a folder of pages. */
83
+ export type RoutesSource = string | {
84
+ pages: string;
85
+ };
63
86
  export declare const CONFIG_FILE = "ferrovue.config.json";
64
87
  /** Read the project's configuration. */
65
88
  export declare function loadConfig(root: string, configPath?: string): Config;
66
89
  export declare function tyOfName(name: TypeName): Ty;
90
+ /** The Rust type of an `InlineHtml` prop, and of a `TrustedHtml` one when `trustedHtml` names it. */
91
+ export declare const INLINE_HTML = "ferrovue::InlineHtml";
67
92
  /** Where `TrustedHtml` comes from. Only that import names the type `v-html` will write raw. */
68
93
  export declare const TYPES_MODULE = "ferrovue/types";
69
94
  export declare const ctx: {
@@ -75,6 +100,7 @@ export declare const ctx: {
75
100
  }>;
76
101
  helperModule: string | null;
77
102
  trustedHtml: string | null;
103
+ builders: boolean;
78
104
  clientDirectives: Set<string>;
79
105
  plugins: readonly Plugin[];
80
106
  runs: Map<Plugin<unknown, unknown>, unknown>;
@@ -83,7 +109,14 @@ export declare const ctx: {
83
109
  typeAliases: Map<string, any>;
84
110
  typeFiles: Map<string, string>;
85
111
  typeRead: Set<string>;
112
+ constDecls: Map<string, Map<string, Declared>>;
113
+ typeConsts: Map<string, {
114
+ of: unknown;
115
+ file: string;
116
+ text: string;
117
+ }>;
86
118
  components: Map<string, Component>;
87
119
  narrowCount: number;
88
120
  componentsDir: string;
121
+ vnodeTag: string | null;
89
122
  };
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,15 +51,18 @@ 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
  }
57
+ /** The Rust type of an `InlineHtml` prop, and of a `TrustedHtml` one when `trustedHtml` names it. */
58
+ export const INLINE_HTML = "ferrovue::InlineHtml";
54
59
  /** Where `TrustedHtml` comes from. Only that import names the type `v-html` will write raw. */
55
60
  export const TYPES_MODULE = "ferrovue/types";
56
61
  export const ctx = {
57
62
  helpers: {},
58
63
  helperModule: null,
59
64
  trustedHtml: null,
65
+ builders: true,
60
66
  clientDirectives: new Set(),
61
67
  plugins: [],
62
68
  runs: new Map(),
@@ -65,7 +71,10 @@ export const ctx = {
65
71
  typeAliases: new Map(),
66
72
  typeFiles: new Map(),
67
73
  typeRead: new Set(),
74
+ constDecls: new Map(),
75
+ typeConsts: new Map(),
68
76
  components: new Map(),
69
77
  narrowCount: 0,
70
78
  componentsDir: "",
79
+ vnodeTag: null,
71
80
  };
@@ -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
+ }
@@ -0,0 +1,18 @@
1
+ import { type Component, type N, type Scope } from "./model.ts";
2
+ import { Emitter } from "./emitter.ts";
3
+ /** Whether the content a parent gives `slot` of `child` is rendered as virtual nodes, as it is
4
+ * inside an element a `<component :is>` chooses. */
5
+ export declare function slotAsVnodes(child: Component, slot: string): boolean;
6
+ /** Whether a statement of a compiled template renders a `<component :is>`. */
7
+ export declare function isDynamicComponent(st: N): boolean;
8
+ /** Render `_ssrRenderVNode` of a `<component :is>`, or return false for a statement that is not one. */
9
+ export declare function dynamicComponent(s: Scope, e: Emitter, st: N): boolean;
10
+ /** Resolve every `<component :is>` of the components before any is generated: the slots whose
11
+ * content renders as virtual nodes, and the compiled templates as the analyses read them, with
12
+ * each `<component :is>` written out as its choices. */
13
+ export declare function prepareDynamic(read: {
14
+ comp: Component;
15
+ ast: N[];
16
+ ssr: string;
17
+ scope: Scope;
18
+ }[]): string[];