ferrovue 0.6.0 → 0.7.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 (79) hide show
  1. package/README.md +106 -13
  2. package/dist/args.d.ts +28 -0
  3. package/dist/args.js +69 -0
  4. package/dist/attrs.js +3 -0
  5. package/dist/calls.js +5 -2
  6. package/dist/children.js +18 -3
  7. package/dist/cli.js +80 -81
  8. package/dist/client-render.d.ts +23 -0
  9. package/dist/client-render.js +143 -0
  10. package/dist/client.d.ts +65 -6
  11. package/dist/client.js +269 -27
  12. package/dist/compiler.d.ts +9 -5
  13. package/dist/compiler.js +48 -14
  14. package/dist/component.d.ts +7 -3
  15. package/dist/component.js +37 -25
  16. package/dist/conformance.d.ts +24 -3
  17. package/dist/conformance.js +58 -7
  18. package/dist/constants.js +4 -2
  19. package/dist/context.d.ts +51 -4
  20. package/dist/context.js +173 -5
  21. package/dist/diagnostics.d.ts +6 -4
  22. package/dist/diagnostics.js +7 -3
  23. package/dist/editor.d.ts +18 -0
  24. package/dist/editor.js +59 -0
  25. package/dist/errors.d.ts +45 -3
  26. package/dist/errors.js +33 -3
  27. package/dist/expr.d.ts +2 -0
  28. package/dist/expr.js +91 -12
  29. package/dist/fallthrough.d.ts +1 -0
  30. package/dist/fallthrough.js +10 -2
  31. package/dist/files.d.ts +13 -0
  32. package/dist/files.js +49 -0
  33. package/dist/fixture.d.ts +3 -3
  34. package/dist/hydration.d.ts +12 -1
  35. package/dist/hydration.js +47 -1
  36. package/dist/index.d.ts +2 -2
  37. package/dist/index.js +1 -1
  38. package/dist/inputs.d.ts +24 -0
  39. package/dist/inputs.js +40 -0
  40. package/dist/link-router.d.ts +6 -2
  41. package/dist/link-router.js +7 -4
  42. package/dist/lists.d.ts +12 -1
  43. package/dist/lists.js +76 -2
  44. package/dist/model.d.ts +9 -1
  45. package/dist/model.js +2 -0
  46. package/dist/page-routes.d.ts +1 -0
  47. package/dist/paths.d.ts +5 -0
  48. package/dist/paths.js +7 -0
  49. package/dist/plugin.d.ts +2 -0
  50. package/dist/plugins/css-modules.d.ts +39 -0
  51. package/dist/plugins/css-modules.js +135 -0
  52. package/dist/plugins/head.js +4 -4
  53. package/dist/plugins/i18n.js +5 -12
  54. package/dist/plugins/index.js +2 -1
  55. package/dist/plugins/provide.js +4 -2
  56. package/dist/plugins/router.js +3 -2
  57. package/dist/plugins/scoped.d.ts +10 -0
  58. package/dist/plugins/scoped.js +17 -4
  59. package/dist/plugins/shared-types.js +2 -1
  60. package/dist/plugins/stores.js +7 -6
  61. package/dist/rust.d.ts +9 -1
  62. package/dist/rust.js +38 -15
  63. package/dist/schema.d.ts +25 -0
  64. package/dist/schema.js +73 -0
  65. package/dist/script.js +12 -5
  66. package/dist/slots.js +2 -0
  67. package/dist/ssr.js +42 -4
  68. package/dist/styles.d.ts +5 -1
  69. package/dist/styles.js +131 -4
  70. package/dist/testing.d.ts +2 -1
  71. package/dist/testing.js +1 -1
  72. package/dist/typescript.d.ts +11 -1
  73. package/dist/typescript.js +124 -33
  74. package/dist/vite.d.ts +18 -10
  75. package/dist/vite.js +106 -51
  76. package/dist/volar.d.ts +15 -0
  77. package/dist/volar.js +87 -0
  78. package/package.json +27 -7
  79. package/schema.json +333 -0
package/dist/styles.js CHANGED
@@ -1,10 +1,133 @@
1
1
  import { escapeHtml, hyphenate, parseStringStyle } from "@vue/shared";
2
2
  import { fail, nothing, rustStr } from "./model.js";
3
- import { expr } from "./expr.js";
4
- import { cond } from "./narrowing.js";
5
- import { condition, logical, not, strArg } from "./parens.js";
3
+ import { describeTy, expr } from "./expr.js";
4
+ import { cond, known } from "./narrowing.js";
5
+ import { condition, logical, not, receiver, strArg } from "./parens.js";
6
6
  import { Emitter } from "./emitter.js";
7
7
  import { display, valueAttr } from "./attrs.js";
8
+ import { isTemporary } from "./strings.js";
9
+ /** Put the object Vue's server compiler declares as `_cssVars`, the `v-bind()` variables in
10
+ * `<style>`, in place of each use of it in the compiled template's statements, with each value
11
+ * marked to be written as `ssrRenderStyle` writes a variable. */
12
+ export function inlineCssVars(comp, body) {
13
+ const at = body.findIndex((st) => st.type === "VariableDeclaration" && st.declarations[0]?.id?.name === "_cssVars");
14
+ if (at < 0)
15
+ return body;
16
+ const vars = body[at].declarations[0].init.properties[0].value.properties;
17
+ const place = (n, where) => {
18
+ if (Array.isArray(n))
19
+ return n.map((x) => place(x, where));
20
+ if (!n || typeof n !== "object")
21
+ return n;
22
+ const copy = {};
23
+ for (const [k, v] of Object.entries(n))
24
+ copy[k] = k === "loc" ? where.loc : place(v, where);
25
+ if (typeof n.type === "string")
26
+ copy.__fv = "source";
27
+ return copy;
28
+ };
29
+ const object = () => ({
30
+ type: "ObjectExpression",
31
+ properties: [{
32
+ type: "ObjectProperty",
33
+ computed: false,
34
+ key: { type: "Identifier", name: "style" },
35
+ value: {
36
+ type: "ObjectExpression",
37
+ properties: vars.map((p, i) => {
38
+ const where = comp.cssVarsAt?.[i] ?? p;
39
+ return {
40
+ type: "ObjectProperty",
41
+ computed: false,
42
+ key: { type: "StringLiteral", value: String(p.key.value).replace(/^:/, "") },
43
+ value: { type: "FvCssVar", value: place(p.value, where), loc: where.loc, __fv: where.__fv },
44
+ };
45
+ }),
46
+ },
47
+ }],
48
+ });
49
+ const swap = (n) => {
50
+ if (!n || typeof n !== "object")
51
+ return;
52
+ for (const [k, v] of Object.entries(n)) {
53
+ if (k === "loc")
54
+ continue;
55
+ if (Array.isArray(v)) {
56
+ v.forEach((x, i) => {
57
+ if (x?.type === "Identifier" && x.name === "_cssVars")
58
+ v[i] = object();
59
+ else
60
+ swap(x);
61
+ });
62
+ }
63
+ else if (v && typeof v === "object") {
64
+ if (v.type === "Identifier" && v.name === "_cssVars")
65
+ n[k] = object();
66
+ else
67
+ swap(v);
68
+ }
69
+ }
70
+ };
71
+ const rest = body.filter((_, i) => i !== at);
72
+ swap(rest);
73
+ return rest;
74
+ }
75
+ function cssVarOf(s, n) {
76
+ const v = expr(s, n.value);
77
+ const of = v.ty.k === "opt" ? v.ty.of : v.ty;
78
+ if (!nothing(v.ty) && of.k !== "str" && of.k !== "int" && of.k !== "float") {
79
+ fail(s.comp, "FV1006", `\`v-bind()\` in \`<style>\` of ${describeTy(of)}: Vue writes a string or a number, and warns of anything else`, n);
80
+ }
81
+ return v;
82
+ }
83
+ function writeCssVar(s, e, css, n) {
84
+ const one = (v) => {
85
+ if (nothing(v.ty)) {
86
+ e.lit(`${escapeHtml(css)}:initial;`);
87
+ return;
88
+ }
89
+ if (v.ty.k === "opt") {
90
+ e.open(`if let Some(v) = ${v.code}`);
91
+ one({ code: "v", ty: v.ty.of });
92
+ e.close(" else {");
93
+ e.lit(`${escapeHtml(css)}:initial;`);
94
+ e.close();
95
+ return;
96
+ }
97
+ e.lit(`${escapeHtml(css)}:`);
98
+ const k = v.ty.k === "str" ? known(v) : true;
99
+ if (k === false)
100
+ e.lit(" ");
101
+ else if (k === undefined && isTemporary(v))
102
+ e.stmt(`match AsRef::<str>::as_ref(${strArg(v.code)}) { "" => out.push(' '), v => fv::escape_into(out, v) }`);
103
+ else if (k === undefined) {
104
+ e.open(`if ${receiver(v.code)}.is_empty()`);
105
+ e.lit(" ");
106
+ e.close(" else {");
107
+ display(e, v);
108
+ e.close();
109
+ }
110
+ else
111
+ display(e, v);
112
+ e.lit(";");
113
+ };
114
+ one(cssVarOf(s, n));
115
+ }
116
+ function cssVarAttr(s, n) {
117
+ const one = (v) => {
118
+ if (nothing(v.ty))
119
+ return 'fv::Attr::str("initial")';
120
+ if (v.ty.k === "opt")
121
+ return `match ${v.code} { Some(v) => ${one({ code: "v", ty: v.ty.of })}, None => fv::Attr::str("initial") }`;
122
+ const k = v.ty.k === "str" ? known(v) : true;
123
+ if (k === false)
124
+ return 'fv::Attr::str(" ")';
125
+ if (k === undefined)
126
+ return `if ${receiver(v.code)}.is_empty() { fv::Attr::str(" ") } else { ${valueAttr(s, v, n)} }`;
127
+ return valueAttr(s, v, n);
128
+ };
129
+ return one(cssVarOf(s, n));
130
+ }
8
131
  export function mergedStyle(s, e, n) {
9
132
  if (n.type === "StringLiteral") {
10
133
  renderStyle(s, e, { type: "ArrayExpression", elements: [n] });
@@ -38,7 +161,7 @@ export function styleAttr(s, n) {
38
161
  const key = p.key.type === "Identifier" ? p.key.name : String(p.key.value);
39
162
  if (/^\d+$/.test(key) || key.startsWith(":"))
40
163
  fail(s.comp, "FV1009", `style property \`${key}\``, p);
41
- return `(${rustStr(key)}, ${valueAttr(s, expr(s, p.value), p.value)})`;
164
+ return `(${rustStr(key)}, ${p.value.type === "FvCssVar" ? cssVarAttr(s, p.value) : valueAttr(s, expr(s, p.value), p.value)})`;
42
165
  });
43
166
  return `fv::Attr::style([${entries.join(", ")}])`;
44
167
  }
@@ -123,6 +246,10 @@ export function renderStyle(s, e, n) {
123
246
  }
124
247
  }
125
248
  const write = (css, value) => {
249
+ if (value.type === "FvCssVar") {
250
+ writeCssVar(s, e, css, value);
251
+ return;
252
+ }
126
253
  if (value.type === "StringLiteral" || value.type === "NumericLiteral") {
127
254
  e.lit(escapeHtml(`${css}:${String(value.value)};`));
128
255
  return;
package/dist/testing.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { attachSsrRender } from "./ssr.ts";
2
2
  export { conformanceSuite, type ConformanceOptions } from "./conformance.ts";
3
3
  export { fixtureApp, readFixture, routeRecords, type Fixture, type RouteEntry, type RouterOptions } from "./fixture.ts";
4
- export { hydrateRecordedPage, type RecordedPage } from "./hydration.ts";
4
+ export { hydrateRecordedPage, renderRecordedPage, type RecordedPage } from "./hydration.ts";
5
+ export type { IslandComponent, MountOptions, PageOptions, PagePart, PageRecord, RenderPageOptions } from "./client.ts";
package/dist/testing.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { attachSsrRender } from "./ssr.js";
2
2
  export { conformanceSuite } from "./conformance.js";
3
3
  export { fixtureApp, readFixture, routeRecords } from "./fixture.js";
4
- export { hydrateRecordedPage } from "./hydration.js";
4
+ export { hydrateRecordedPage, renderRecordedPage } from "./hydration.js";
@@ -3,8 +3,18 @@ export declare function typesImports(comp: Component, body: N[]): void;
3
3
  export declare const ONE_NOTHING = "its Rust type is an `Option`, whose `None` cannot be both, as `=== null` and the props sent to the client would need";
4
4
  export declare function tyOfTs(comp: Component, t: N, structs: Map<string, Struct>, seen?: Set<string>): Ty;
5
5
  export declare function declareTypes(comp: Component, body: N[], structs: Map<string, Struct>, aliases: Map<string, N>): N[];
6
+ export declare function refusePrelude(comp: Component, d: N): void;
6
7
  export declare function resolveImport(fromFile: string, spec: string): string | null;
7
8
  export declare function readTypeFile(file: string): void;
9
+ /** Translate a type a `.ts` file declares, the first time something the server renders reaches it. */
10
+ export declare function reachType(name: string): void;
11
+ /** Translate an interface a component declares, the first time something the server renders reaches it. */
12
+ export declare function reachLocal(comp: Component, name: string): void;
13
+ /** Refuse a type whose every field a value of it must carry, as props and store state do, where one
14
+ * of its fields, or of the types it holds, has no Rust type. */
15
+ export declare function requireWhole(comp: Component, ty: Ty, seen?: Set<Struct>): void;
16
+ /** Whether `types.rs` holds the type: one built from a constant list, or a declared one something reached. */
17
+ export declare function typeWritten(name: string): boolean;
8
18
  export declare function markHome(ty: Ty, home: string): Ty;
9
19
  export declare function lookupStruct(comp: Component, ty: Ty & {
10
20
  k: "struct";
@@ -13,7 +23,7 @@ export declare function lookupStruct(comp: Component, ty: Ty & {
13
23
  owner: Component;
14
24
  path: string;
15
25
  };
16
- export declare function structOf(comp: Component, name: string, members: N[], structs: Map<string, Struct>): Struct;
26
+ export declare function structOf(comp: Component, name: string, members: N[], structs: Map<string, Struct>, lenient?: boolean): Struct;
17
27
  export declare function definePropsType(call: N): N | null;
18
28
  export declare function runtimeDefaults(comp: Component, content: string): Map<string, N>;
19
29
  export declare function defaultValue(comp: Component, f: Field, node: N, written?: N): string;
@@ -1,11 +1,13 @@
1
1
  import { parse as parseJs } from "@babel/parser";
2
2
  import { readFileSync, statSync } from "node:fs";
3
- import { basename, dirname, join, relative, resolve } from "node:path";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { relativePath } from "./paths.js";
4
5
  import { absence, blankComponent, BOOL, fail, FLOAT, GenError, INT, joinAbsence, opt, RUST_PRELUDE, rustStr, sameTy, snake, STR, tagAst, withAbsence } from "./model.js";
5
6
  import { CONFIG_FILE, ctx, INLINE_HTML, TYPES_MODULE } from "./context.js";
6
7
  import { childOf } from "./expr.js";
7
8
  import { claim } from "./plugin.js";
8
9
  import { declareConsts, enumType } from "./constants.js";
10
+ import { parseTs } from "./files.js";
9
11
  export function typesImports(comp, body) {
10
12
  for (const s of body) {
11
13
  if (s.type !== "ImportDeclaration" || s.source.value !== TYPES_MODULE)
@@ -92,8 +94,13 @@ export function tyOfTs(comp, t, structs, seen = new Set()) {
92
94
  fail(comp, "FV0313", "a `Record` is keyed by `string`", key);
93
95
  return recordOf(comp, value, structs, seen);
94
96
  }
95
- if (structs.has(name))
97
+ if (structs.has(name)) {
98
+ if (structs === ctx.typeStructs)
99
+ reachType(name);
100
+ else if (structs === comp.structs)
101
+ reachLocal(comp, name);
96
102
  return { k: "struct", name };
103
+ }
97
104
  if (comp.childProps.has(name))
98
105
  return { k: "child", name: comp.childProps.get(name) };
99
106
  if (comp.floatName !== null && name === comp.floatName)
@@ -107,8 +114,11 @@ export function tyOfTs(comp, t, structs, seen = new Set()) {
107
114
  return ctx.trustedHtml === INLINE_HTML ? { k: "html", inline: true } : { k: "html" };
108
115
  }
109
116
  const imported = comp.importedTypes.get(name);
110
- if (imported)
117
+ if (imported) {
118
+ if (imported.k === "struct" && imported.home === "types")
119
+ reachType(imported.name);
111
120
  return imported;
121
+ }
112
122
  const alias = comp.aliases.get(name);
113
123
  if (alias) {
114
124
  if (seen.has(name))
@@ -150,14 +160,15 @@ export function declareTypes(comp, body, structs, aliases) {
150
160
  }
151
161
  }
152
162
  }
153
- for (const d of decls) {
154
- if (RUST_PRELUDE.has(d.name)) {
155
- fail(comp, "FV0318", `an interface called \`${d.name}\` would hide Rust's own \`${d.name}\` in the generated code; rename it`, d.node);
156
- }
163
+ for (const d of decls)
157
164
  structs.set(d.name, { name: d.name, fields: [] });
158
- }
159
165
  return decls;
160
166
  }
167
+ export function refusePrelude(comp, d) {
168
+ if (RUST_PRELUDE.has(d.name)) {
169
+ fail(comp, "FV0318", `an interface called \`${d.name}\` would hide Rust's own \`${d.name}\` in the generated code; rename it`, d.node);
170
+ }
171
+ }
161
172
  export function resolveImport(fromFile, spec) {
162
173
  if (!spec.startsWith("."))
163
174
  return null;
@@ -176,30 +187,88 @@ export function readTypeFile(file) {
176
187
  if (ctx.typeRead.has(file))
177
188
  return;
178
189
  ctx.typeRead.add(file);
179
- const rel = relative(ctx.rootDir, file);
190
+ const rel = relativePath(ctx.rootDir, file);
180
191
  const home = blankComponent(basename(rel), "types", rel, ctx.typeStructs);
181
192
  home.aliases = ctx.typeAliases;
182
193
  home.source = readFileSync(file, "utf8");
183
- const body = parseJs(home.source, { sourceType: "module", plugins: ["typescript"] }).program.body;
194
+ const body = parseTs(home);
184
195
  tagAst(body, "source");
185
196
  typesImports(home, body);
186
197
  ctx.constDecls.set(file, declareConsts(home, body));
187
- const before = new Set(ctx.typeStructs.keys());
188
- const local = new Map();
189
- const decls = declareTypes(home, body, local, ctx.typeAliases);
190
- for (const d of decls) {
191
- if (before.has(d.name))
198
+ for (const d of declareTypes(home, body, new Map(), ctx.typeAliases)) {
199
+ const known = ctx.typeDecls.get(d.name);
200
+ if (known === undefined && ctx.typeStructs.has(d.name))
192
201
  fail(home, "FV0319", `\`${d.name}\` is declared by ${ctx.typeFiles.get(d.name)} too`, d.node);
193
- ctx.typeStructs.set(d.name, local.get(d.name));
202
+ if (known !== undefined && ctx.typeReached.has(d.name))
203
+ fail(home, "FV0319", `\`${d.name}\` is declared by ${known[0].home.file} too`, d.node);
204
+ if (known !== undefined) {
205
+ if (known.at(-1).home.file === rel)
206
+ known.pop();
207
+ known.push({ home, members: d.members, node: d.node });
208
+ continue;
209
+ }
210
+ ctx.typeDecls.set(d.name, [{ home, members: d.members, node: d.node }]);
211
+ ctx.typeStructs.set(d.name, { name: d.name, fields: [] });
194
212
  ctx.typeFiles.set(d.name, rel);
195
213
  }
196
- for (const d of decls) {
197
- const st = structOf(home, d.name, d.members, ctx.typeStructs);
214
+ }
215
+ /** Translate a type a `.ts` file declares, the first time something the server renders reaches it. */
216
+ export function reachType(name) {
217
+ const decls = ctx.typeDecls.get(name);
218
+ if (decls === undefined || ctx.typeReached.has(name))
219
+ return;
220
+ ctx.typeReached.add(name);
221
+ try {
222
+ const [first, again] = decls;
223
+ if (again !== undefined)
224
+ fail(again.home, "FV0319", `\`${name}\` is declared by ${first.home.file} too`, again.node);
225
+ refusePrelude(first.home, { name, node: first.node });
226
+ const st = structOf(first.home, name, first.members, ctx.typeStructs, true);
198
227
  for (const f of st.fields)
199
228
  f.ty = markHome(f.ty, "types");
200
- ctx.typeStructs.set(d.name, st);
229
+ ctx.typeStructs.set(name, st);
230
+ }
231
+ catch (e) {
232
+ ctx.typeReached.delete(name);
233
+ throw e;
234
+ }
235
+ }
236
+ /** Translate an interface a component declares, the first time something the server renders reaches it. */
237
+ export function reachLocal(comp, name) {
238
+ const decl = comp.typeDecls.get(name);
239
+ if (decl === undefined || comp.reached.has(name))
240
+ return;
241
+ comp.reached.add(name);
242
+ try {
243
+ refusePrelude(comp, { name, node: decl.node });
244
+ comp.structs.set(name, structOf(comp, name, decl.members, comp.structs, true));
245
+ }
246
+ catch (e) {
247
+ comp.reached.delete(name);
248
+ throw e;
201
249
  }
202
250
  }
251
+ /** Refuse a type whose every field a value of it must carry, as props and store state do, where one
252
+ * of its fields, or of the types it holds, has no Rust type. */
253
+ export function requireWhole(comp, ty, seen = new Set()) {
254
+ if (ty.k === "opt" || ty.k === "list" || ty.k === "record")
255
+ return requireWhole(comp, ty.of, seen);
256
+ if (ty.k !== "struct")
257
+ return;
258
+ const { st, owner } = lookupStruct(comp, ty);
259
+ if (st === undefined || seen.has(st))
260
+ return;
261
+ seen.add(st);
262
+ const refused = st.refused?.values().next().value;
263
+ if (refused !== undefined)
264
+ throw refused;
265
+ for (const f of st.fields)
266
+ requireWhole(owner, f.ty, seen);
267
+ }
268
+ /** Whether `types.rs` holds the type: one built from a constant list, or a declared one something reached. */
269
+ export function typeWritten(name) {
270
+ return !ctx.typeDecls.has(name) || ctx.typeReached.has(name);
271
+ }
203
272
  export function markHome(ty, home) {
204
273
  if (ty.k === "struct" && !claim((p) => p.struct?.(ty)) && !ty.home && ty.name !== "Props")
205
274
  return { ...ty, home };
@@ -211,31 +280,53 @@ export function lookupStruct(comp, ty) {
211
280
  const own = claim((p) => p.struct?.(ty));
212
281
  if (own)
213
282
  return { st: own.st, owner: comp, path: comp.module === own.module ? "" : `super::${own.module}::` };
214
- if (ty.home === "types")
283
+ if (ty.home === "types") {
284
+ reachType(ty.name);
215
285
  return { st: ctx.typeStructs.get(ty.name), owner: comp, path: comp.module === "types" ? "" : "super::types::" };
286
+ }
216
287
  if (ty.home !== undefined && ty.home !== comp.name) {
217
288
  const owner = childOf(ty.home);
289
+ reachLocal(owner, ty.name);
218
290
  return { st: owner.structs.get(ty.name), owner, path: `super::${owner.module}::` };
219
291
  }
220
- return { st: ty.name === "Props" ? comp.props : comp.structs.get(ty.name), owner: comp, path: "" };
292
+ if (ty.name === "Props")
293
+ return { st: comp.props, owner: comp, path: "" };
294
+ reachLocal(comp, ty.name);
295
+ return { st: comp.structs.get(ty.name), owner: comp, path: "" };
221
296
  }
222
- export function structOf(comp, name, members, structs) {
297
+ export function structOf(comp, name, members, structs, lenient = false) {
223
298
  const fields = [];
299
+ const refused = new Map();
224
300
  for (const m of members) {
225
- if (m.type !== "TSPropertySignature" || m.key.type !== "Identifier") {
226
- fail(comp, "FV0320", `\`${name}\` may only hold plain named fields`, m);
227
- }
228
- const base = tyOfTs(comp, m.typeAnnotation.typeAnnotation, structs);
229
- if (m.optional && absence(base) === "null") {
230
- fail(comp, "FV0321", `\`${m.key.name}?: T | null\` may be absent, which is \`undefined\`, or \`null\`: ${ONE_NOTHING}; declare it \`${m.key.name}: T | null\` or \`${m.key.name}?: T\``, m);
301
+ try {
302
+ fields.push(fieldOf(comp, name, m, structs));
231
303
  }
232
- if (m.optional && base.k === "opt") {
233
- fields.push({ js: m.key.name, rust: snake(m.key.name), ty: base });
234
- continue;
304
+ catch (e) {
305
+ if (!lenient || !(e instanceof GenError))
306
+ throw e;
307
+ const key = memberKey(m);
308
+ if (!refused.has(key))
309
+ refused.set(key, e);
235
310
  }
236
- fields.push({ js: m.key.name, rust: snake(m.key.name), ty: m.optional ? opt(base) : base });
237
311
  }
238
- return { name, fields };
312
+ return refused.size ? { name, fields, refused } : { name, fields };
313
+ }
314
+ function memberKey(m) {
315
+ if (m.type !== "TSPropertySignature" && m.type !== "TSMethodSignature")
316
+ return "";
317
+ if (m.computed)
318
+ return "";
319
+ return m.key.type === "Identifier" ? m.key.name : m.key.type === "StringLiteral" ? m.key.value : "";
320
+ }
321
+ function fieldOf(comp, name, m, structs) {
322
+ if (m.type !== "TSPropertySignature" || m.key.type !== "Identifier") {
323
+ fail(comp, "FV0320", `\`${name}\` may only hold plain named fields`, m);
324
+ }
325
+ const base = tyOfTs(comp, m.typeAnnotation.typeAnnotation, structs);
326
+ if (m.optional && absence(base) === "null") {
327
+ fail(comp, "FV0321", `\`${m.key.name}?: T | null\` may be absent, which is \`undefined\`, or \`null\`: ${ONE_NOTHING}; declare it \`${m.key.name}: T | null\` or \`${m.key.name}?: T\``, m);
328
+ }
329
+ return { js: m.key.name, rust: snake(m.key.name), ty: m.optional && base.k !== "opt" ? opt(base) : base };
239
330
  }
240
331
  export function definePropsType(call) {
241
332
  if (call?.type === "CallExpression" && call.callee.type === "Identifier" && call.callee.name === "withDefaults") {
package/dist/vite.d.ts CHANGED
@@ -1,26 +1,34 @@
1
1
  import type { Plugin, ResolvedConfig } from "vite";
2
2
  import { type FileRoute } from "./file-routes.ts";
3
+ export type { FileRoute } from "./file-routes.ts";
3
4
  export interface FerrovueOptions {
4
- /** The directory holding `ferrovue.config.json`: Vite's working directory by default. */
5
+ /** The project root, which the configuration's paths are relative to: Vite's working directory
6
+ * by default. */
5
7
  root?: string;
8
+ /** The configuration file, relative to `root`, as the CLI's `--config` takes it:
9
+ * `ferrovue.config.json` by default. */
10
+ config?: string;
6
11
  }
7
- /** Whether a changed file can change what is generated: a component, a store or type file, the
8
- * routes or the configuration: anything but the output itself, dependencies and build output. */
9
- export declare function affects(root: string, file: string): boolean;
10
12
  /** How `@vitejs/plugin-vue`, as this build configures it, computes a `<style scoped>` id: from the
11
- * path alone or the path and source, hashed from Vite's root. `null` without the plugin, or with an
12
- * id generator of its own. */
13
+ * path alone or the path and source, hashed from Vite's root; and whether it compiles for production.
14
+ * `null` without the plugin, or with an id generator of its own. */
13
15
  export declare function vueScopeIds(config: ResolvedConfig): {
14
16
  mode: "filepath" | "filepath-source";
15
17
  root: string;
18
+ production: boolean;
16
19
  } | null;
17
- /** Why the server's scope ids would differ from the client's, when a component has `<style scoped>`
18
- * and `ferrovue.config.json` computes them otherwise than plugin-vue: a page would hydrate cleanly
19
- * and show unstyled. */
20
+ /** Why the server's scope ids, or the CSS variables `v-bind()` sets, would differ from the client's,
21
+ * when a component has `<style scoped>` or `v-bind()` in `<style>` and the configuration,
22
+ * `configFile`, computes them otherwise than plugin-vue: a page would hydrate and show unstyled. */
20
23
  export declare function scopeIdMismatch(root: string, vue: {
21
24
  mode: string;
22
25
  root: string;
23
- } | null): string | null;
26
+ production?: boolean;
27
+ } | null, configFile?: string): string | null;
28
+ /** Why the class names of the server's CSS modules would differ from those Vite gives the client,
29
+ * when a component has `<style module>` and Vite's `css`, run from `cwd`, names them otherwise than
30
+ * `cssModules` in the configuration, `configFile`. */
31
+ export declare function cssModulesMismatch(root: string, css: ResolvedConfig["css"] | undefined, cwd: string, configFile?: string): string | null;
24
32
  /** The `ferrovue/routes` module: the routes vue-router's file-based routing builds from the pages,
25
33
  * as vue-router's records with each page loaded lazily (`routes`), and as a routes file lists them
26
34
  * (the default export), for `routeRecords` and `linkRouter`. */