ferrovue 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +198 -34
  2. package/dist/attrs.d.ts +1 -1
  3. package/dist/attrs.js +24 -22
  4. package/dist/calls.js +17 -15
  5. package/dist/children.d.ts +9 -2
  6. package/dist/children.js +48 -35
  7. package/dist/classes.js +11 -11
  8. package/dist/cli.js +31 -5
  9. package/dist/client.d.ts +35 -2
  10. package/dist/client.js +77 -2
  11. package/dist/compiler.d.ts +1 -1
  12. package/dist/compiler.js +7 -3
  13. package/dist/component.d.ts +3 -1
  14. package/dist/component.js +114 -19
  15. package/dist/constants.d.ts +42 -0
  16. package/dist/constants.js +292 -0
  17. package/dist/context.d.ts +25 -1
  18. package/dist/context.js +11 -5
  19. package/dist/diagnostics.d.ts +23 -0
  20. package/dist/diagnostics.js +30 -0
  21. package/dist/errors.d.ts +867 -0
  22. package/dist/errors.js +330 -0
  23. package/dist/expr.d.ts +1 -0
  24. package/dist/expr.js +120 -49
  25. package/dist/fixture.d.ts +3 -10
  26. package/dist/fixture.js +2 -6
  27. package/dist/hydration.d.ts +15 -0
  28. package/dist/hydration.js +80 -0
  29. package/dist/index.d.ts +2 -1
  30. package/dist/index.js +2 -1
  31. package/dist/link-router.d.ts +24 -0
  32. package/dist/link-router.js +36 -0
  33. package/dist/lists.js +30 -21
  34. package/dist/loops.js +14 -10
  35. package/dist/model.d.ts +32 -1
  36. package/dist/model.js +123 -6
  37. package/dist/narrowing.d.ts +8 -0
  38. package/dist/narrowing.js +40 -9
  39. package/dist/plugins/client-only.d.ts +8 -0
  40. package/dist/plugins/client-only.js +107 -0
  41. package/dist/plugins/i18n.js +14 -14
  42. package/dist/plugins/index.js +3 -1
  43. package/dist/plugins/router-link.js +33 -30
  44. package/dist/plugins/router.js +9 -9
  45. package/dist/plugins/scoped.js +1 -1
  46. package/dist/plugins/shared-types.js +1 -1
  47. package/dist/plugins/stores.js +26 -24
  48. package/dist/plugins/teleport.js +3 -3
  49. package/dist/plugins/twins.d.ts +10 -0
  50. package/dist/plugins/twins.js +164 -0
  51. package/dist/routes.d.ts +10 -0
  52. package/dist/routes.js +6 -0
  53. package/dist/rust.js +18 -14
  54. package/dist/script.d.ts +3 -0
  55. package/dist/script.js +119 -17
  56. package/dist/settle.d.ts +6 -0
  57. package/dist/settle.js +34 -0
  58. package/dist/slots.js +18 -18
  59. package/dist/strings.js +11 -11
  60. package/dist/styles.js +13 -13
  61. package/dist/template.js +11 -11
  62. package/dist/testing.d.ts +1 -0
  63. package/dist/testing.js +3 -2
  64. package/dist/typescript.d.ts +2 -1
  65. package/dist/typescript.js +46 -19
  66. package/dist/vite.d.ts +1 -1
  67. package/dist/vite.js +10 -9
  68. package/package.json +9 -5
package/dist/calls.js CHANGED
@@ -20,7 +20,7 @@ export function call(s, n) {
20
20
  meet(comp, a, b, "`v-model`", n, "equal");
21
21
  return boolOf(stringsEqual(a, b));
22
22
  }
23
- return fail(comp, "`v-model` comparison between these types", n);
23
+ return fail(comp, "FV0413", "`v-model` comparison between these types", n);
24
24
  }
25
25
  case "_ssrIncludeBooleanAttr": {
26
26
  const a = expr(s, args[0]);
@@ -33,7 +33,7 @@ export function call(s, n) {
33
33
  return boolOf(truthy(a));
34
34
  }
35
35
  case "_ssrLooseContain":
36
- return fail(comp, "`v-model` over an array", n);
36
+ return fail(comp, "FV0414", "`v-model` over an array", n);
37
37
  }
38
38
  const own = claim((p) => p.call?.(s, n));
39
39
  if (own)
@@ -58,21 +58,21 @@ export function call(s, n) {
58
58
  return a;
59
59
  if (a.ty.k === "bool")
60
60
  return { code: `i64::from(${bare(a.code)})`, ty: INT };
61
- return fail(comp, "`Number()` takes a string, a number or a boolean that is present", n);
61
+ return fail(comp, "FV0701", "`Number()` takes a string, a number or a boolean that is present", n);
62
62
  }
63
63
  if ((callee.name === "parseInt" && (args.length === 1 || args.length === 2)) || (callee.name === "parseFloat" && args.length === 1)) {
64
64
  const a = expr(s, args[0]);
65
65
  if (a.ty.k !== "str")
66
- fail(comp, `\`${callee.name}()\` takes a string`, args[0]);
66
+ fail(comp, "FV0702", `\`${callee.name}()\` takes a string`, args[0]);
67
67
  if (callee.name === "parseFloat")
68
68
  return { code: `fv::js_parse_float(${strArg(a.code)})`, ty: FLOAT };
69
69
  const radix = args[1];
70
70
  if (radix && !(radix.type === "NumericLiteral" && (radix.value === 10 || radix.value === 16))) {
71
- fail(comp, "`parseInt()` takes a radix of 10 or 16, written as a literal", radix);
71
+ fail(comp, "FV0703", "`parseInt()` takes a radix of 10 or 16, written as a literal", radix);
72
72
  }
73
73
  return { code: `fv::js_parse_int(${strArg(a.code)}, ${radix ? radix.value : 0})`, ty: FLOAT };
74
74
  }
75
- return fail(comp, `\`${callee.name}()\` is not available when rendering on the server`, n);
75
+ return fail(comp, "FV0601", `\`${callee.name}()\` is not available when rendering on the server`, n);
76
76
  }
77
77
  if (callee.type === "MemberExpression" && !callee.computed) {
78
78
  const method = callee.property.name;
@@ -119,7 +119,7 @@ export function call(s, n) {
119
119
  return { code: `${receiver(asF64(x))}.${method}()`, ty: FLOAT };
120
120
  }
121
121
  }
122
- return fail(comp, `\`Math.${method}()\` is supported on numbers as \`max\`, \`min\`, \`abs\`, \`round\`, \`floor\`, \`ceil\` and \`trunc\``, n);
122
+ return fail(comp, "FV0704", `\`Math.${method}()\` is supported on numbers as \`max\`, \`min\`, \`abs\`, \`round\`, \`floor\`, \`ceil\` and \`trunc\``, n);
123
123
  }
124
124
  const target = expr(s, callee.object);
125
125
  if (target.ty.k === "str") {
@@ -145,7 +145,7 @@ export function call(s, n) {
145
145
  if (isNumber(target.ty) && method === "toFixed" && args.length <= 1) {
146
146
  const d = args.length ? args[0] : { type: "NumericLiteral", value: 0 };
147
147
  if (d.type !== "NumericLiteral" || !Number.isInteger(d.value) || d.value < 0 || d.value > 100) {
148
- fail(comp, "`.toFixed()` takes a literal number of digits, from 0 to 100", n);
148
+ fail(comp, "FV0705", "`.toFixed()` takes a literal number of digits, from 0 to 100", n);
149
149
  }
150
150
  return { code: `&*fv::js_to_fixed(${bare(asF64(target))}, ${d.value})`, ty: STR };
151
151
  }
@@ -162,8 +162,10 @@ export function call(s, n) {
162
162
  const of = target.ty.of;
163
163
  if (method === "includes" && args.length === 1) {
164
164
  const v = expr(s, args[0]);
165
+ if (of.k === "int" && v.ty.k === "float")
166
+ return { code: `${atom(target.code)}.iter().any(|v| ${binary("*v as f64", "==", v.code)})`, ty: BOOL };
165
167
  if (!sameTy(v.ty, of))
166
- fail(comp, "`.includes()` looks for a value of the list's own type", args[0]);
168
+ fail(comp, "FV0801", "`.includes()` looks for a value of the list's own type", args[0]);
167
169
  if (of.k === "int")
168
170
  return { code: `${atom(target.code)}.contains(&${operand(v.code, UNARY)})`, ty: BOOL };
169
171
  const test = v.code === '""' ? "v.is_empty()" : binary("&**v", "==", v.code);
@@ -172,14 +174,14 @@ export function call(s, n) {
172
174
  if (method === "join" && args.length <= 1) {
173
175
  const sep = args.length ? expr(s, args[0]) : { code: '","', ty: STR };
174
176
  if (sep.ty.k !== "str")
175
- fail(comp, "`.join()` takes a string", args[0]);
177
+ fail(comp, "FV0802", "`.join()` takes a string", args[0]);
176
178
  const each = of.k === "str" ? `${atom(target.code)}.iter().map(|v| &**v)` : `${atom(target.code)}.iter().map(|v| fv::Js(*v).to_string())`;
177
179
  return { code: `&*${each}.collect::<Vec<_>>().join(${strArg(sep.code)})`, ty: STR, ...loneOf(sep) };
178
180
  }
179
181
  }
180
- return fail(comp, `\`.${method}()\` is not supported`, n);
182
+ return fail(comp, "FV0602", `\`.${method}()\` is not supported`, n);
181
183
  }
182
- return fail(comp, "this call is not supported", n);
184
+ return fail(comp, "FV0603", "this call is not supported", n);
183
185
  }
184
186
  export function isObjectCall(n, method) {
185
187
  return (n?.type === "CallExpression" && n.callee.type === "MemberExpression" && !n.callee.computed &&
@@ -187,7 +189,7 @@ export function isObjectCall(n, method) {
187
189
  }
188
190
  function json(s, v, n) {
189
191
  if (v.lone)
190
- fail(s.comp, lonely("`JSON.stringify()`"), n);
192
+ fail(s.comp, "FV0706", lonely("`JSON.stringify()`"), n);
191
193
  const one = (code, ty) => {
192
194
  switch (ty.k) {
193
195
  case "str":
@@ -199,7 +201,7 @@ function json(s, v, n) {
199
201
  case "bool":
200
202
  return `(if ${condition(code)} { "true" } else { "false" }).to_owned()`;
201
203
  default:
202
- return fail(s.comp, "`JSON.stringify()` of a string, a number, a boolean or a list of those, present", n);
204
+ return fail(s.comp, "FV0707", "`JSON.stringify()` of a string, a number, a boolean or a list of those, present", n);
203
205
  }
204
206
  };
205
207
  if (v.ty.k === "list") {
@@ -212,7 +214,7 @@ function json(s, v, n) {
212
214
  export function helperCall(s, name, args, n) {
213
215
  const h = ctx.helpers[name];
214
216
  if (args.length !== h.params.length)
215
- fail(s.comp, `\`${name}\` takes ${h.params.length} argument(s)`, n);
217
+ fail(s.comp, "FV1101", `\`${name}\` takes ${h.params.length} argument(s)`, n);
216
218
  const code = args.map((a, i) => bare(coerce(s.comp, expr(s, a), h.params[i], a))).join(", ");
217
219
  s.helperBytes.n += h.maxLen;
218
220
  return { code: `${h.rust}(${code})`, ty: h.ret };
@@ -1,9 +1,16 @@
1
- import { type Component, type N, type Scope, type Ty, type Val } from "./model.ts";
1
+ import { type Component, type Field, type N, type Scope, type Ty, type Val } from "./model.ts";
2
2
  import { Emitter } from "./emitter.ts";
3
- export declare function renderChild(s: Scope, e: Emitter, n: N): void;
3
+ /** A component the project renders with a Rust function of its own, in place of a compiled one. */
4
+ export interface TwinCall {
5
+ comp: Component;
6
+ init(s: Scope, f: Field, node: N | undefined, n: N): string;
7
+ call(s: Scope, e: Emitter, inits: string[], slots: N, attrs: string): void;
8
+ }
9
+ export declare function renderChild(s: Scope, e: Emitter, n: N, twin?: TwinCall): void;
4
10
  export declare function array(key: string): boolean;
5
11
  export declare function fieldInit(name: string, value: string): string;
6
12
  export declare function takesSlots(c: Component): boolean;
7
13
  export declare function extraParams(c: Component): string;
8
14
  export declare function callChild(s: Scope, e: Emitter, child: Component, propsCode: string, slots: N, attrs: string | null): void;
15
+ export declare function callWith(s: Scope, e: Emitter, child: Component, m: string, head: string, slotsTy: string, trailing: string, slots: N, vnode?: boolean): void;
9
16
  export declare function ownInto(comp: Component, v: Val, want: Ty, node: N): string;
package/dist/children.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { isSSRSafeAttrName, propsToAttrMap } from "@vue/shared";
2
- import { camelize, declares, fail, GenError, rustStr, sameTy, snake, takesAttrs } from "./model.js";
2
+ import { camelize, declares, fail, nothing, rustStr, sameTy, snake, takesAttrs } from "./model.js";
3
3
  import { ctx } from "./context.js";
4
4
  import { markHome } from "./typescript.js";
5
- import { expr, fieldVal } from "./expr.js";
5
+ import { expr, fieldVal, holdsNothing } from "./expr.js";
6
6
  import { asF64 } from "./numbers.js";
7
7
  import { atom, bare } from "./parens.js";
8
8
  import { Emitter } from "./emitter.js";
@@ -11,7 +11,7 @@ import { passedKey } from "./fallthrough.js";
11
11
  import { claim, paramsOf, slotFieldsOf } from "./plugin.js";
12
12
  import { statements } from "./template.js";
13
13
  import { slotContent, slotFieldBorrows, staticallyFilled } from "./slots.js";
14
- export function renderChild(s, e, n) {
14
+ export function renderChild(s, e, n, twin) {
15
15
  const [target, rawProps, slots, , slotScopeId] = n.arguments;
16
16
  let local = null;
17
17
  if (target.type === "MemberExpression" && target.object.name === "$setup") {
@@ -19,9 +19,9 @@ export function renderChild(s, e, n) {
19
19
  }
20
20
  const isSelf = target.type === "Identifier" && target.name === s.selfAlias.name;
21
21
  const childName = isSelf ? s.comp.name : local ? s.children.get(local) : undefined;
22
- const child = childName ? s.components.get(childName) : undefined;
22
+ const child = twin?.comp ?? (childName ? s.components.get(childName) : undefined);
23
23
  if (!child)
24
- fail(s.comp, "a child component must be an imported island", n);
24
+ fail(s.comp, "FV0501", `a child component must be imported from a \`.vue\` file among the components compiled, in ${ctx.componentsDir.replace(/\/$/, "")}`, n);
25
25
  for (const p of ctx.plugins)
26
26
  p.child?.(s, child, n);
27
27
  const parts = !rawProps || rawProps.type === "NullLiteral" ? [] : mergedParts(rawProps);
@@ -33,30 +33,32 @@ export function renderChild(s, e, n) {
33
33
  if (parts.some(passed)) {
34
34
  for (const name of s.comp.attrNames) {
35
35
  if (declares(child, name)) {
36
- fail(s.comp, `\`${name}\`, an attribute ${s.comp.name} may be passed, would reach ${child.name} as its prop \`${camelize(name)}\`, which an attribute has no type for`, n);
36
+ fail(s.comp, "FV0502", `\`${name}\`, an attribute ${s.comp.name} may be passed, would reach ${child.name} as its prop \`${camelize(name)}\`, which an attribute has no type for`, n);
37
37
  }
38
38
  }
39
39
  }
40
40
  const ids = claim((p) => p.childIds?.(s, child, passesAttrs, !!slotScopeId, n)) ?? null;
41
41
  if (objects.length === 1 && objects[0].type !== "ObjectExpression") {
42
+ if (twin)
43
+ fail(s.comp, "FV1501", `the props of ${child.name}, a Rust twin, are attributes or an object literal`, objects[0]);
42
44
  const v = expr(s, objects[0]);
43
45
  const own = child.name === s.comp.name && v.ty.k === "struct" && v.ty.name === "Props";
44
46
  if (own || (v.ty.k === "child" && v.ty.name === child.name)) {
45
- callChild(s, e, child, v.code, slots, childAttrsArg(s, child, parts, merges, null, ids));
47
+ callChild(s, e, child, v.code, slots, childAttrsArg(s, child, parts, merges, null, ids, n));
46
48
  return;
47
49
  }
48
- fail(s.comp, `child props must be an object literal, or \`v-bind\` of ${child.name}'s own \`Props\``, n);
50
+ fail(s.comp, "FV0503", `child props must be an object literal, or \`v-bind\` of ${child.name}'s own \`Props\``, n);
49
51
  }
50
52
  for (const p of objects) {
51
53
  if (p.type !== "ObjectExpression")
52
- fail(s.comp, `child props must be an object literal, or \`v-bind\` of ${child.name}'s own \`Props\``, p);
54
+ fail(s.comp, "FV0503", `child props must be an object literal, or \`v-bind\` of ${child.name}'s own \`Props\``, p);
53
55
  }
54
56
  const given = new Map();
55
57
  const fallthrough = new Set();
56
58
  for (const obj of objects) {
57
59
  for (const p of obj.properties) {
58
60
  if (p.type !== "ObjectProperty" || p.computed)
59
- fail(s.comp, "child props hold plain keys", p);
61
+ fail(s.comp, "FV0504", "child props hold plain keys", p);
60
62
  const key = p.key.type === "Identifier" ? p.key.name : String(p.key.value);
61
63
  if (!passedKey(child, key))
62
64
  continue;
@@ -66,29 +68,36 @@ export function renderChild(s, e, n) {
66
68
  continue;
67
69
  }
68
70
  if (array(key))
69
- fail(s.comp, `an attribute named \`${key}\`, which JavaScript would put before the others`, p);
71
+ fail(s.comp, "FV0505", `an attribute named \`${key}\`, which JavaScript would put before the others`, p);
70
72
  const name = propsToAttrMap[key] ?? key.toLowerCase();
71
73
  if (key !== "class" && key !== "style" && !isSSRSafeAttrName(name))
72
- fail(s.comp, `unsafe attribute name \`${name}\``, p);
74
+ fail(s.comp, "FV0404", `unsafe attribute name \`${name}\``, p);
73
75
  fallthrough.add(p);
74
76
  }
75
77
  }
78
+ if (twin) {
79
+ const inits = child.props.fields.map((f) => twin.init(s, f, given.get(f.js), n));
80
+ twin.call(s, e, inits, slots, childAttrsArg(s, child, parts, merges, fallthrough, ids, n));
81
+ return;
82
+ }
76
83
  const inits = child.props.fields.map((f) => {
77
84
  const node = given.get(f.js);
78
85
  if (!node) {
79
86
  if (f.ty.k !== "opt")
80
- fail(s.comp, `${child.name} requires \`${f.js}\``, n);
87
+ fail(s.comp, "FV0506", `${child.name} requires \`${f.js}\``, n);
88
+ if (f.ty.none !== undefined)
89
+ fail(s.comp, "FV0507", `${child.name} requires \`${f.js}\`, which is \`T | null\`: Vue would hand it \`undefined\`; pass \`null\` for none`, n);
81
90
  return `${f.rust}: None`;
82
91
  }
83
92
  const v = expr(s, node);
84
93
  return fieldInit(f.rust, ownInto(s.comp, { ...v, ty: markHome(v.ty, s.comp.name) }, markHome(f.ty, child.name), node));
85
94
  });
86
- callChild(s, e, child, `&super::${child.module}::Props { ${inits.join(", ")} }`, slots, childAttrsArg(s, child, parts, merges, fallthrough, ids));
95
+ callChild(s, e, child, `&super::${child.module}::Props { ${inits.join(", ")} }`, slots, childAttrsArg(s, child, parts, merges, fallthrough, ids, n));
87
96
  }
88
97
  export function array(key) {
89
98
  return /^(0|[1-9]\d*)$/.test(key) && Number(key) < 2 ** 32 - 1;
90
99
  }
91
- function childAttrsArg(s, child, parts, merges, fallthrough, ids) {
100
+ function childAttrsArg(s, child, parts, merges, fallthrough, ids, n) {
92
101
  const sources = [];
93
102
  for (const p of parts) {
94
103
  if (s.fallthrough !== null && ((isAttrs(p) && s.comp.inheritAttrs) || dollarAttrs(s, p))) {
@@ -105,7 +114,7 @@ function childAttrsArg(s, child, parts, merges, fallthrough, ids) {
105
114
  }
106
115
  if (!takesAttrs(child)) {
107
116
  if (sources.length)
108
- throw new GenError(`${s.comp.file}: ${child.name} is passed attributes it does not take`);
117
+ fail(s.comp, "FV0508", `${child.name} is passed attributes it does not take`, n);
109
118
  return ids;
110
119
  }
111
120
  if (!sources.length)
@@ -124,30 +133,33 @@ export function extraParams(c) {
124
133
  return (takesSlots(c) ? ", fv_slots: Slots<'_>" : "") + paramsOf(c).map((p) => `, ${p.name}: ${p.ty}`).join("");
125
134
  }
126
135
  export function callChild(s, e, child, propsCode, slots, attrs) {
136
+ const m = `super::${child.module}`;
137
+ const scoped = child.inherits || takesAttrs(child);
138
+ const trailing = paramsOf(child).map((p) => `, ${p.name}`).join("") + (scoped ? `, ${attrs ?? (takesAttrs(child) ? "&fv::Attrs::NONE" : '""')}` : "");
139
+ const render = scoped ? "render_scoped" : "render";
140
+ callWith(s, e, child, m, `${m}::${render}(out, ${propsCode}`, `${m}::Slots`, trailing, slots);
141
+ }
142
+ export function callWith(s, e, child, m, head, slotsTy, trailing, slots, vnode = false) {
127
143
  const given = new Map();
128
144
  if (slots && slots.type !== "NullLiteral") {
129
145
  if (slots.type !== "ObjectExpression")
130
- fail(s.comp, "slots must be an object literal", slots);
146
+ fail(s.comp, "FV0901", "slots must be an object literal", slots);
131
147
  for (const p of slots.properties) {
132
148
  const key = p.key?.type === "Identifier" ? p.key.name : p.key?.value;
133
149
  if (key === "_")
134
150
  continue;
135
151
  if (p.type !== "ObjectProperty" || p.computed)
136
- fail(s.comp, "slots hold plain keys", p);
152
+ fail(s.comp, "FV0902", "slots hold plain keys", p);
137
153
  if (!child.slotNames.includes(key))
138
- fail(s.comp, `${child.name} has no slot \`${key}\``, p);
154
+ fail(s.comp, "FV0903", `${child.name} has no slot \`${key}\``, p);
139
155
  given.set(key, p.value);
140
156
  }
141
157
  }
142
- const m = `super::${child.module}`;
143
- const scoped = child.inherits || takesAttrs(child);
144
- const trailing = paramsOf(child).map((p) => `, ${p.name}`).join("") + (scoped ? `, ${attrs ?? (takesAttrs(child) ? "&fv::Attrs::NONE" : '""')}` : "");
145
- const render = scoped ? "render_scoped" : "render";
146
158
  if (!takesSlots(child)) {
147
- e.stmt(`${m}::${render}(out, ${propsCode}${trailing});`);
159
+ e.stmt(`${head}${trailing});`);
148
160
  return;
149
161
  }
150
- e.open(`${m}::${render}(out, ${propsCode}, ${m}::Slots`);
162
+ e.open(`${head}, ${slotsTy}`);
151
163
  for (const name of child.slotNames) {
152
164
  const field = snake(name);
153
165
  const value = given.get(name);
@@ -182,7 +194,7 @@ export function callChild(s, e, child, propsCode, slots, attrs) {
182
194
  if (param?.type === "ObjectPattern") {
183
195
  for (const p of param.properties) {
184
196
  if (p.type !== "ObjectProperty" || p.computed || p.value.type !== "Identifier") {
185
- fail(s.comp, "slot props are destructured into plain names, without defaults", p);
197
+ fail(s.comp, "FV0904", "slot props are destructured into plain names, without defaults", p);
186
198
  }
187
199
  locals.set(p.value.name, fieldVal(s.comp, sp, ty, p.key.name ?? p.key.value, p));
188
200
  }
@@ -191,19 +203,19 @@ export function callChild(s, e, child, propsCode, slots, attrs) {
191
203
  locals.set(param.name, { code: sp, ty });
192
204
  }
193
205
  else if (!takesNone)
194
- fail(s.comp, "slot props are a name or an object pattern", param);
206
+ fail(s.comp, "FV0905", "slot props are a name or an object pattern", param);
195
207
  const life = shape.fields.some((f) => slotFieldBorrows(f.ty)) ? "<'_>" : "";
196
208
  e.open(`${field}: Some(&|out: &mut String, ${sp}: &${m}::${shape.name}${life}${sidParam}| -> bool`);
197
209
  const opened = e.lines.length - 1;
198
- content({ ...s, locals, sid });
210
+ content({ ...s, locals, sid, vnode });
199
211
  unread(opened, sp);
200
212
  unread(opened, sid);
201
213
  e.close("),");
202
214
  continue;
203
215
  }
204
216
  if (!takesNone)
205
- fail(s.comp, `\`<slot${name === "default" ? "" : ` name="${name}"`}>\` in ${child.name} passes no props`, param);
206
- const inner = { ...s, sid };
217
+ fail(s.comp, "FV0906", `\`<slot${name === "default" ? "" : ` name="${name}"`}>\` in ${child.name} passes no props`, param);
218
+ const inner = { ...s, sid, vnode };
207
219
  if (sid !== null) {
208
220
  e.open(`${field}: Some(fv::Slot::slotted(&|out: &mut String${sidParam}| -> bool`);
209
221
  const opened = e.lines.length - 1;
@@ -227,14 +239,15 @@ export function callChild(s, e, child, propsCode, slots, attrs) {
227
239
  e.close(`${trailing});`);
228
240
  }
229
241
  export function ownInto(comp, v, want, node) {
242
+ holdsNothing(comp, v, want, node, "the prop");
243
+ if (want.k === "opt" && nothing(v.ty))
244
+ return "None";
230
245
  if (want.k === "str") {
231
246
  if (v.ty.k !== "str")
232
- fail(comp, "a string prop needs a string", node);
247
+ fail(comp, "FV0509", "a string prop needs a string", node);
233
248
  return `std::borrow::Cow::Borrowed(${bare(v.code)})`;
234
249
  }
235
250
  if (want.k === "opt" && want.of.k === "str") {
236
- if (v.ty.k === "undef")
237
- return "None";
238
251
  if (v.ty.k === "str")
239
252
  return `Some(std::borrow::Cow::Borrowed(${bare(v.code)}))`;
240
253
  if (v.ty.k === "opt" && v.ty.of.k === "str")
@@ -257,7 +270,7 @@ export function ownInto(comp, v, want, node) {
257
270
  if (want.k === "opt" && objecty(want.of) && sameTy(v.ty, want.of))
258
271
  return `Some(${atom(v.code)}.to_owned())`;
259
272
  if (objecty(want) && v.ty.k === want.k && JSON.stringify(v.ty).includes('"struct"')) {
260
- fail(comp, `a ${JSON.stringify(v.ty)} where the child takes a ${JSON.stringify(want)}: two components share a type only when both import it from one \`.ts\` file`, node);
273
+ fail(comp, "FV0510", `a ${JSON.stringify(v.ty)} where the child takes a ${JSON.stringify(want)}: two components share a type only when both import it from one \`.ts\` file`, node);
261
274
  }
262
275
  if (want.k === "list" && v.ty.k === "list" && v.ty.of.k === "undef")
263
276
  return "Vec::new()";
@@ -269,5 +282,5 @@ export function ownInto(comp, v, want, node) {
269
282
  if (want.of.k === "int" || want.of.k === "float" || want.of.k === "bool")
270
283
  return `${atom(v.code)}.to_vec()`;
271
284
  }
272
- return fail(comp, `a ${JSON.stringify(v.ty)} into a ${JSON.stringify(want)} prop`, node);
285
+ return fail(comp, "FV0511", `a ${JSON.stringify(v.ty)} into a ${JSON.stringify(want)} prop`, node);
273
286
  }
package/dist/classes.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { escapeHtml } from "@vue/shared";
2
- import { fail, rustStr, STR } from "./model.js";
2
+ import { fail, nothing, rustStr, STR } from "./model.js";
3
3
  import { expr } from "./expr.js";
4
4
  import { cond, known, truthy } from "./narrowing.js";
5
5
  import { meet } from "./strings.js";
@@ -25,10 +25,10 @@ export function classItems(s, n) {
25
25
  const keys = [];
26
26
  const entries = n.properties.map((p) => {
27
27
  if (p.type !== "ObjectProperty")
28
- fail(s.comp, "a class object holds `name: condition` pairs", p);
28
+ fail(s.comp, "FV1001", "a class object holds `name: condition` pairs", p);
29
29
  const key = p.computed ? expr(s, p.key) : { code: rustStr(p.key.type === "Identifier" ? p.key.name : String(p.key.value)), ty: STR };
30
30
  if (key.ty.k !== "str")
31
- fail(s.comp, "a computed class name is a string", p.key);
31
+ fail(s.comp, "FV1002", "a computed class name is a string", p.key);
32
32
  for (const other of keys)
33
33
  meet(s.comp, other, key, "a class object's names", p.key, "equal");
34
34
  keys.push(key);
@@ -38,18 +38,18 @@ export function classItems(s, n) {
38
38
  }
39
39
  return n.properties.flatMap((p) => {
40
40
  if (p.type !== "ObjectProperty")
41
- fail(s.comp, "a class object holds `name: condition` pairs", p);
41
+ fail(s.comp, "FV1001", "a class object holds `name: condition` pairs", p);
42
42
  let name;
43
43
  if (!p.computed) {
44
44
  const key = p.key.type === "Identifier" ? p.key.name : String(p.key.value);
45
45
  if (key !== key.trim() || !key)
46
- fail(s.comp, `class name \`${key}\` has spaces around it`, p);
46
+ fail(s.comp, "FV1003", `class name \`${key}\` has spaces around it`, p);
47
47
  name = { lit: key };
48
48
  }
49
49
  else {
50
50
  const k = expr(s, p.key);
51
51
  if (k.ty.k !== "str")
52
- fail(s.comp, "a computed class name is a string", p.key);
52
+ fail(s.comp, "FV1002", "a computed class name is a string", p.key);
53
53
  name = { code: `fv::js_trim(${strArg(k.code)})` };
54
54
  }
55
55
  const v = expr(s, p.value);
@@ -77,9 +77,9 @@ export function classItems(s, n) {
77
77
  return [{ code: v.code }];
78
78
  if (v.ty.k === "opt" && v.ty.of.k === "str")
79
79
  return [{ code: `${atom(v.code)}.unwrap_or("")` }];
80
- if (v.ty.k === "undef")
80
+ if (nothing(v.ty))
81
81
  return [];
82
- return fail(s.comp, "a class is a string, an array, or an object of conditions", n);
82
+ return fail(s.comp, "FV1004", "a class is a string, an array, or an object of conditions", n);
83
83
  }
84
84
  function conditional(test, items) {
85
85
  if (test === "true" || test === "false")
@@ -123,7 +123,7 @@ export function classPresent(s, n) {
123
123
  break;
124
124
  }
125
125
  const v = expr(s, n);
126
- if (v.ty.k === "undef")
126
+ if (nothing(v.ty))
127
127
  return "false";
128
128
  if (v.ty.k === "opt")
129
129
  return `${atom(v.code)}.is_some()`;
@@ -170,7 +170,7 @@ export function classAttr(s, n, side) {
170
170
  return `if ${condition(cond(s, n.test))} { ${classAttr(s, n.consequent, side)} } else { ${classAttr(s, n.alternate, side)} }`;
171
171
  }
172
172
  const v = expr(s, n);
173
- if (v.ty.k === "str" || v.ty.k === "undef" || (v.ty.k === "opt" && v.ty.of.k === "str"))
173
+ if (v.ty.k === "str" || nothing(v.ty) || (v.ty.k === "opt" && v.ty.of.k === "str"))
174
174
  return valueAttr(s, v, n);
175
- return fail(s.comp, "a class is a string, an array, or an object of conditions", n);
175
+ return fail(s.comp, "FV1004", "a class is a string, an array, or an object of conditions", n);
176
176
  }
package/dist/cli.js CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, watch, writeFileSync } from "node:fs";
3
3
  import { dirname, join, relative, resolve, sep } from "node:path";
4
- import { CONFIG_FILE, GenError, generate, loadConfig, VERSION, write } from "./compiler.js";
4
+ import { CONFIG_FILE, generate, loadConfig, VERSION, write } from "./compiler.js";
5
5
  import { unifiedDiff } from "./diff.js";
6
+ import { diagnose, formatRefusal, isRefusal } from "./diagnostics.js";
6
7
  const root = process.cwd();
7
8
  const HELP = `Usage: ferrovue [command] [options]
8
9
 
@@ -14,16 +15,23 @@ Commands:
14
15
  Options:
15
16
  --check check that generated files match committed files without writing
16
17
  -d, --diff show unified diff of changes when checking (use with --check)
18
+ --format <format> how to write errors and results: human (default) or json
17
19
  --watch watch components, stores, routes and config for changes and regenerate
18
20
  -c, --config <path> path to configuration file (default: ferrovue.config.json)
19
21
  -v, --version print the version and exit
20
22
  -h, --help print this help and exit
21
23
  `;
24
+ let json = false;
25
+ function emit(result) {
26
+ console.log(JSON.stringify({ diagnostics: [], ...result }));
27
+ }
22
28
  function report(e) {
23
- if (e instanceof GenError || e instanceof SyntaxError)
24
- console.error(`error: ${e.message}`);
25
- else
29
+ if (!isRefusal(e))
26
30
  throw e;
31
+ if (json)
32
+ emit({ diagnostics: [diagnose(e)] });
33
+ else
34
+ console.error(formatRefusal(e));
27
35
  }
28
36
  function initProject(targetRoot, configPath) {
29
37
  const configFile = configPath ? resolve(targetRoot, configPath) : join(targetRoot, CONFIG_FILE);
@@ -77,6 +85,10 @@ function check(config, showDiff = false) {
77
85
  }
78
86
  });
79
87
  const extra = have.filter((name) => !want.has(name));
88
+ if (json) {
89
+ emit({ out: config.out, stale: stale.map((name) => `${config.out}/${name}`), notGenerated: extra.map((name) => `${config.out}/${name}`) });
90
+ return stale.length || extra.length ? 1 : 0;
91
+ }
80
92
  if (stale.length || extra.length) {
81
93
  const committed = (name) => {
82
94
  try {
@@ -104,6 +116,8 @@ function check(config, showDiff = false) {
104
116
  }
105
117
  function once(config) {
106
118
  const { files, changed, removed } = write(root, config);
119
+ if (json)
120
+ return emit({ out: config.out, files, changed, removed });
107
121
  const what = changed.length || removed.length ? `${changed.length} changed, ${removed.length} removed` : "nothing changed";
108
122
  console.log(`${config.out}: ${files.length} files, ${what}`);
109
123
  }
@@ -137,7 +151,7 @@ function watchProject(configPath) {
137
151
  clearTimeout(timer);
138
152
  timer = setTimeout(run, 50);
139
153
  });
140
- console.log("watching for changes (Ctrl-C to stop)");
154
+ (json ? console.error : console.log)("watching for changes (Ctrl-C to stop)");
141
155
  }
142
156
  try {
143
157
  const rawArgs = process.argv.slice(2);
@@ -155,6 +169,14 @@ try {
155
169
  else if (arg.startsWith("--config=")) {
156
170
  configPath = arg.slice("--config=".length);
157
171
  }
172
+ else if (arg === "--format" || arg.startsWith("--format=")) {
173
+ const format = arg === "--format" ? rawArgs[++i] : arg.slice("--format=".length);
174
+ if (format !== "human" && format !== "json") {
175
+ console.error(`error: '--format' is 'human' or 'json'`);
176
+ process.exit(1);
177
+ }
178
+ json = format === "json";
179
+ }
158
180
  else {
159
181
  args.push(arg);
160
182
  }
@@ -169,6 +191,10 @@ try {
169
191
  console.error("error: '--diff' requires '--check'");
170
192
  process.exit(1);
171
193
  }
194
+ if ((args.includes("--diff") || args.includes("-d")) && json) {
195
+ console.error("error: '--diff' writes text, and does not combine with '--format json'");
196
+ process.exit(1);
197
+ }
172
198
  if (args.includes("--version") || args.includes("-v")) {
173
199
  console.log(VERSION);
174
200
  }
package/dist/client.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type App, type Component, type Plugin } from "vue";
2
2
  import type { Pinia } from "pinia";
3
3
  import type { Router } from "vue-router";
4
- /** Give Pinia the state the server rendered with — what `ferrovue::state_script_into` wrote — before
4
+ /** Give Pinia the state the server rendered with (what `ferrovue::state_script_into` wrote) before
5
5
  * the app mounts, so every store starts from it and the hydrated markup agrees with the server's. */
6
6
  export declare function hydrateState(pinia: Pinia, id?: string, doc?: Document): void;
7
7
  export interface MountOptions {
@@ -18,7 +18,7 @@ export interface MountOptions {
18
18
  * those given or its props are malformed. Warns on the console by default. */
19
19
  onError?: (element: Element, problem: string) => void;
20
20
  }
21
- /** A component, or a function that loads one — `() => import("./Counter.vue")` — so that the code
21
+ /** A component, or a function that loads one (`() => import("./Counter.vue")`), so that the code
22
22
  * of an island not on the page is never fetched. A function is taken for a loader as vue-router
23
23
  * takes one for a lazy route: unless it has `props` or `displayName`, which mark a functional
24
24
  * component, or `__vccOpts`, a class component. `ferrovue/islands` maps the name of every island
@@ -38,3 +38,36 @@ export interface Islands {
38
38
  * rendered it with, mounted where it is. A loader is called only for an island the page holds, all
39
39
  * of them at once, and the islands mount in document order once every one has loaded. */
40
40
  export declare function mountIslands(components: Record<string, IslandComponent>, options?: MountOptions): Promise<Islands>;
41
+ /** Content only the browser renders. The server writes the `#fallback` slot, or nothing; the
42
+ * browser hydrates that fallback, then swaps in the default slot once mounted. ferrovue does not
43
+ * compile the default slot, so anything may go there: a component library's components, code that
44
+ * reads `window`. The swap happens only where Vue runs on the page, as in an island. */
45
+ export declare const ClientOnly: Component;
46
+ /** One component the server rendered into a slot of a page's layout, as `ferrovue::Part` records
47
+ * it: its name (`c`) and the props it was rendered from (`p`). */
48
+ export interface PagePart {
49
+ c: string;
50
+ p: Record<string, unknown>;
51
+ }
52
+ /** What `ferrovue::PageRecord` writes: the layout's props, and each slot's parts in order. */
53
+ export interface PageRecord {
54
+ props: Record<string, unknown>;
55
+ slots: Record<string, PagePart[]>;
56
+ }
57
+ export interface PageOptions extends Pick<MountOptions, "pinia" | "router" | "plugins"> {
58
+ /** The element holding the layout's root, or a selector for it: `#app` by default. */
59
+ container?: Element | string;
60
+ /** The `id` of the record's script, as given to `PageRecord::script_into`: `__fv_page` by default. */
61
+ record?: string;
62
+ /** The document to read the page from. */
63
+ doc?: Document;
64
+ }
65
+ /** The app of a page: its root renders `layout` with the record's props, and each slot as the
66
+ * components the record names, in order, as a plain array, which hydrates the markup the server
67
+ * wrote with no fragment of its own. `components` must hold every component the record names. */
68
+ export declare function createPageApp(layout: Component, record: PageRecord, components: Record<string, Component>, options?: PageOptions): App;
69
+ /** Hydrate a page `ferrovue::Page` rendered: read its record, load the components it names (and
70
+ * no others), wait for the router, and mount the layout on the container. Rejects, leaving the page
71
+ * as the server rendered it, when the container or the record is missing or a component is not
72
+ * given or does not load. */
73
+ export declare function mountPage(layout: IslandComponent, components: Record<string, IslandComponent>, options?: PageOptions): Promise<App>;