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
@@ -4,9 +4,19 @@ import { type Plugin } from "../plugin.ts";
4
4
  interface ScopedRun {
5
5
  mode: ScopeIdMode;
6
6
  viteRoot: string;
7
+ production: boolean;
7
8
  ids: Map<Component, string>;
9
+ cssVars: Map<Component, {
10
+ id: string;
11
+ vars: string[];
12
+ }>;
8
13
  slotted: Set<Component>;
9
14
  }
10
15
  export declare function scopeIdOf(comp: Component): string | null;
16
+ /** The id `@vitejs/plugin-vue` gives a component, `descriptor.id`: a hash of its path from Vite's
17
+ * root, followed by its source unless `mode` hashes the path alone. */
18
+ export declare function componentId(file: string, source: string, mode: ScopeIdMode, viteRoot: string): string;
19
+ /** The component's id, `componentId`, as the configuration computes it. */
20
+ export declare function scopeHash(file: string, source: string): string;
11
21
  export declare const scoped: Plugin<ScopedRun>;
12
22
  export {};
@@ -7,14 +7,20 @@ import { runOf } from "../plugin.js";
7
7
  export function scopeIdOf(comp) {
8
8
  return runOf(scoped).ids.get(comp) ?? null;
9
9
  }
10
- function scopeHash(file, source) {
11
- const { mode, viteRoot } = runOf(scoped);
10
+ /** The id `@vitejs/plugin-vue` gives a component, `descriptor.id`: a hash of its path from Vite's
11
+ * root, followed by its source unless `mode` hashes the path alone. */
12
+ export function componentId(file, source, mode, viteRoot) {
12
13
  const path = relative(viteRoot, resolvePath(file)).split(sep).join("/");
13
14
  return createHash("sha256")
14
15
  .update(mode === "filepath" ? path : path + source)
15
16
  .digest("hex")
16
17
  .slice(0, 8);
17
18
  }
19
+ /** The component's id, `componentId`, as the configuration computes it. */
20
+ export function scopeHash(file, source) {
21
+ const { mode, viteRoot } = runOf(scoped);
22
+ return componentId(file, source, mode, viteRoot);
23
+ }
18
24
  function flowOf(comp, ssr, children) {
19
25
  const calls = [];
20
26
  const outlets = [];
@@ -106,19 +112,26 @@ export const scoped = {
106
112
  configure: (config, root) => ({
107
113
  mode: config.scopeId ?? "filepath-source",
108
114
  viteRoot: resolvePath(root, config.viteRoot ?? "."),
115
+ production: config.isProduction ?? true,
109
116
  ids: new Map(),
117
+ cssVars: new Map(),
110
118
  slotted: new Set(),
111
119
  }),
112
120
  sfc(comp, descriptor, file, source) {
113
121
  const run = runOf(scoped);
122
+ const hash = descriptor.styles.some((st) => st.scoped) || descriptor.cssVars.length ? scopeHash(file, source) : null;
114
123
  if (descriptor.styles.some((st) => st.scoped))
115
- run.ids.set(comp, `data-v-${scopeHash(file, source)}`);
124
+ run.ids.set(comp, `data-v-${hash}`);
125
+ if (descriptor.cssVars.length)
126
+ run.cssVars.set(comp, { id: `data-v-${hash}`, vars: descriptor.cssVars });
116
127
  if (descriptor.slotted)
117
128
  run.slotted.add(comp);
118
129
  },
119
130
  templateOptions(comp) {
131
+ const run = runOf(scoped);
120
132
  const id = scopeIdOf(comp);
121
- return { id: id ?? comp.name, scoped: id !== null, slotted: runOf(scoped).slotted.has(comp) };
133
+ const vars = run.cssVars.get(comp);
134
+ return { id: id ?? vars?.id ?? comp.name, scoped: id !== null, slotted: run.slotted.has(comp), ssrCssVars: vars?.vars ?? [], isProd: run.production };
122
135
  },
123
136
  analyse: (read) => scopeFlow(read),
124
137
  childIds,
@@ -1,7 +1,8 @@
1
1
  import { ctx } from "../context.js";
2
2
  import { typesSource } from "../rust.js";
3
+ import { typeWritten } from "../typescript.js";
3
4
  import {} from "../plugin.js";
4
5
  export const sharedTypes = {
5
6
  name: "types",
6
- modules: () => (ctx.typeStructs.size || ctx.typeConsts.size ? [["types.rs", typesSource()]] : []),
7
+ modules: () => ([...ctx.typeStructs.keys()].some(typeWritten) || ctx.typeConsts.size ? [["types.rs", typesSource()]] : []),
7
8
  };
@@ -1,8 +1,9 @@
1
- import { parse as parseJs } from "@babel/parser";
2
- import { readdirSync, readFileSync } from "node:fs";
3
- import { basename, dirname, join, relative, resolve } from "node:path";
1
+ import { readFileSync } from "node:fs";
2
+ import { basename, dirname, join, resolve } from "node:path";
3
+ import { relativePath } from "../paths.js";
4
4
  import { absence, blankComponent, BOOL, fail, failIn, FLOAT, GenError, INT, opt, sameTy, snake, STR, tagAst, UNDEF } from "../model.js";
5
5
  import { ctx } from "../context.js";
6
+ import { listDir, parseTs } from "../files.js";
6
7
  import { ONE_NOTHING, structOf, tyOfTs, typesImports } from "../typescript.js";
7
8
  import { patternNames, setupStatement } from "../script.js";
8
9
  import { expr, fieldVal } from "../expr.js";
@@ -17,12 +18,12 @@ function storeImport(comp, from) {
17
18
  }
18
19
  function readStores(root, dir) {
19
20
  const run = runOf(piniaStores);
20
- const files = readdirSync(join(root, dir)).filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts")).toSorted();
21
+ const files = listDir(root, dir, "stores").filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts")).toSorted();
21
22
  for (const f of files) {
22
23
  const path = join(root, dir, f);
23
- const comp = storeHome(relative(root, path));
24
+ const comp = storeHome(relativePath(root, path));
24
25
  comp.source = readFileSync(path, "utf8");
25
- const ast = parseJs(comp.source, { sourceType: "module", plugins: ["typescript"] }).program.body;
26
+ const ast = parseTs(comp);
26
27
  tagAst(ast, "source");
27
28
  typesImports(comp, ast);
28
29
  const decls = ast.map((st) => (st.type === "ExportNamedDeclaration" ? st.declaration : st)).filter(Boolean);
package/dist/rust.d.ts CHANGED
@@ -1,11 +1,19 @@
1
1
  import { type Component, type N, type Struct, type Ty } from "./model.ts";
2
2
  export declare function needsLifetime(ty: Ty, comp: Component, seen?: Set<string>): boolean;
3
3
  export declare function structLifetime(st: Struct, comp: Component): boolean;
4
+ export declare function localTy(ty: Ty, comp: Component): string;
4
5
  export declare function rustTy(ty: Ty, comp: Component): string;
5
6
  export declare function structSource(st: Struct, comp: Component, doc: string): string;
7
+ /** The version of generated code this compiler writes, which `mod.rs` hands `ferrovue::__compat!`:
8
+ * the highest version the `ferrovue` crate of the same release supports. RELEASING.md says when
9
+ * it changes. */
10
+ export declare const GENERATED_VERSION = 1;
11
+ /** How every file ferrovue writes begins. */
12
+ export declare const GENERATED = "// @generated by ferrovue";
6
13
  export declare function header(source: string, edit?: string): string;
7
14
  export declare function textLen(comp: Component, place: string, ty: Ty, seen?: Set<string>): string[];
8
15
  export declare function isIsland(comp: Component): boolean;
9
- export declare function componentSource(comp: Component, ast: N[], ssr: string, components: Map<string, Component>): string;
16
+ /** The component's module, built once nothing the server renders reaches more of its interfaces. */
17
+ export declare function componentSource(comp: Component, ast: N[], ssr: string, components: Map<string, Component>): () => string;
10
18
  export declare function modSource(comps: Component[], modules: string[]): string;
11
19
  export declare function typesSource(): string;
package/dist/rust.js CHANGED
@@ -2,11 +2,12 @@ import { parse as parseJs } from "@babel/parser";
2
2
  import { basename } from "node:path";
3
3
  import { blankComponent, fail, GenError, rustStr, snake, tagAst, takesAttrs } from "./model.js";
4
4
  import { ctx, INLINE_HTML } from "./context.js";
5
- import { lookupStruct } from "./typescript.js";
5
+ import { lookupStruct, typeWritten } from "./typescript.js";
6
6
  import { childOf } from "./expr.js";
7
7
  import { Emitter } from "./emitter.js";
8
8
  import { occurrences } from "./parens.js";
9
9
  import { statements } from "./template.js";
10
+ import { inlineCssVars } from "./styles.js";
10
11
  import { slotFieldBorrows, slotFieldTy, slotTypeName } from "./slots.js";
11
12
  import { extraParams, fieldInit, takesSlots } from "./children.js";
12
13
  import { paramsOf, renderParams, slotContextOf, slotFieldsOf } from "./plugin.js";
@@ -39,6 +40,9 @@ export function needsLifetime(ty, comp, seen = new Set()) {
39
40
  export function structLifetime(st, comp) {
40
41
  return st.fields.some((f) => needsLifetime(f.ty, comp, new Set([st.name])));
41
42
  }
43
+ export function localTy(ty, comp) {
44
+ return rustTy(ty, comp).replace(/\bCow<'a, /g, "std::borrow::Cow<").replace(/'a\b/g, "'_");
45
+ }
42
46
  export function rustTy(ty, comp) {
43
47
  switch (ty.k) {
44
48
  case "str":
@@ -134,8 +138,14 @@ ${fields}
134
138
  }
135
139
  ${ctx.builders ? `\n${builderSource(st, comp, life)}\n` : ""}`;
136
140
  }
141
+ /** The version of generated code this compiler writes, which `mod.rs` hands `ferrovue::__compat!`:
142
+ * the highest version the `ferrovue` crate of the same release supports. RELEASING.md says when
143
+ * it changes. */
144
+ export const GENERATED_VERSION = 1;
145
+ /** How every file ferrovue writes begins. */
146
+ export const GENERATED = "// @generated by ferrovue";
137
147
  export function header(source, edit = "the `.vue` file") {
138
- return `// @generated by ferrovue from ${source}. Do not edit: change ${edit} and run
148
+ return `${GENERATED} from ${source}. Do not edit: change ${edit} and run
139
149
  // \`ferrovue\`.
140
150
  `;
141
151
  }
@@ -174,8 +184,14 @@ export function textLen(comp, place, ty, seen = new Set()) {
174
184
  return [];
175
185
  }
176
186
  }
187
+ function manyArgs(count) {
188
+ return count > 7 ? "#[allow(clippy::too_many_arguments)]\n" : "";
189
+ }
190
+ function renderParamCount(comp) {
191
+ return (takesSlots(comp) ? 1 : 0) + paramsOf(comp).length;
192
+ }
177
193
  function renderSource(comp, life, args, e) {
178
- const doc = "/// Write the component's server render into `out`.\n";
194
+ const doc = `/// Write the component's server render into \`out\`.\n${manyArgs(2 + renderParamCount(comp))}`;
179
195
  const props = e.reads("props", 0) ? "props" : "_props";
180
196
  const unread = (name) => {
181
197
  const shadow = e.lines.findIndex((l) => l.trimStart().startsWith(`let ${name} = `));
@@ -194,13 +210,14 @@ function renderSource(comp, life, args, e) {
194
210
 
195
211
  /// [\`render\`], with ${what}.
196
212
  #[doc(hidden)]
197
- pub fn render_scoped(out: &mut String, ${props}: &Props${life}${extraParams(comp, unread)}, ${attrs}: ${ty}) {
213
+ ${manyArgs(3 + renderParamCount(comp))}pub fn render_scoped(out: &mut String, ${props}: &Props${life}${extraParams(comp, unread)}, ${attrs}: ${ty}) {
198
214
  ${e.lines.join("\n")}
199
215
  }`;
200
216
  }
201
217
  export function isIsland(comp) {
202
218
  return !takesSlots(comp) && comp.takes.size === 0;
203
219
  }
220
+ /** The component's module, built once nothing the server renders reaches more of its interfaces. */
204
221
  export function componentSource(comp, ast, ssr, components) {
205
222
  if (takesAttrs(comp) && comp.inheritAttrs && comp.attrsDropped) {
206
223
  fail(comp, "FV0416", `a root \`<Transition>\` or \`<KeepAlive>\` around a \`v-if\` in ${comp.name}, which a parent passes attributes: Vue's server drops them, where its client puts them on the element; set \`inheritAttrs: false\` and bind \`$attrs\` on the element`, comp.attrsDropped);
@@ -216,7 +233,7 @@ export function componentSource(comp, ast, ssr, components) {
216
233
  const opening = [...preludes.flatMap((p) => p.before), ...lets, ...preludes.flatMap((p) => p.after)];
217
234
  for (const l of opening)
218
235
  e.stmt(l);
219
- statements(scope, e, fn.body.body);
236
+ statements(scope, e, inlineCssVars(comp, fn.body.body));
220
237
  for (let i = opening.length - 1; i >= 0; i--) {
221
238
  const name = /^let (\w+)/.exec(opening[i])?.[1];
222
239
  if (name !== undefined && !e.reads(name, i + 1))
@@ -232,11 +249,6 @@ export function componentSource(comp, ast, ssr, components) {
232
249
  const named = life ? "<'a>" : "";
233
250
  const owned = life ? "<'a>" : "";
234
251
  const ownedLife = life ? "'a" : "'static";
235
- const structs = [...comp.structs.values()]
236
- .filter((st) => st.name !== "Props" && !st.slot)
237
- .map((st) => structSource(st, comp, `/// \`${st.name}\` in \`${basename(comp.file)}\`.\n`))
238
- .join("\n");
239
- const usesCow = /Cow</.test(structs + structSource(comp.props, comp, ""));
240
252
  const plain = isIsland(comp);
241
253
  const context = slotContextOf(comp);
242
254
  const contextTys = context.map((p) => `, ${p.ty}`).join("");
@@ -310,20 +322,28 @@ pub fn into_island${owned}(props: Props${named}) -> fv::Html<${ownedLife}, Props
310
322
  }
311
323
  `
312
324
  : `/// The component's markup, for a maud page that shows it.
313
- pub fn html${gen}(props: &'p Props${named}${params}) -> fv::Html<'p, Props${named}, impl Fn(&mut String, &Props${named}) + 'p> {
325
+ ${manyArgs(1 + renderParamCount(comp))}pub fn html${gen}(props: &'p Props${named}${params}) -> fv::Html<'p, Props${named}, impl Fn(&mut String, &Props${named}) + 'p> {
314
326
  fv::Html::markup(props, move |out: &mut String, props: &Props${named}| render(out, props${args}))
315
327
  }
316
328
  `;
317
- return `${header(comp.file)}
329
+ const render = renderSource(comp, life, args, e);
330
+ return () => {
331
+ const structs = [...comp.structs.values()]
332
+ .filter((st) => st.name !== "Props" && !st.slot && (!comp.typeDecls.has(st.name) || comp.reached.has(st.name)))
333
+ .map((st) => structSource(st, comp, `/// \`${st.name}\` in \`${basename(comp.file)}\`.\n`))
334
+ .join("\n");
335
+ const usesCow = /Cow</.test(structs + structSource(comp.props, comp, ""));
336
+ return `${header(comp.file)}
318
337
  ${usesCow ? "use std::borrow::Cow;\n\n" : ""}use ferrovue as fv;
319
338
 
320
339
  /// The component's name, as \`data-island\` carries it.
321
340
  pub const NAME: &str = ${rustStr(comp.name)};
322
341
 
323
342
  ${structs ? structs + "\n" : ""}${structSource(comp.props, comp, `/// The props \`${basename(comp.file)}\` declares.\n`)}
324
- ${slotsStruct}${renderSource(comp, life, args, e)}
343
+ ${slotsStruct}${render}
325
344
 
326
345
  ${wrappers}`;
346
+ };
327
347
  }
328
348
  function readsFixture(c) {
329
349
  return takesSlots(c) || paramsOf(c).some((p) => p.test.fixture);
@@ -401,6 +421,8 @@ ${defaults.map((d) => `${d}\n\n`).join("")} fn slot(&self, name: &str) -> Opt
401
421
  ${ctx.builders ? "// `into_` forms, `NAME`, a constructor and a setter per optional prop) and an app calls only what\n// it needs." : "// `into_` forms and `NAME`) and an app calls only what it needs."}
402
422
  #![allow(dead_code)]
403
423
 
424
+ ferrovue::__compat!(${GENERATED_VERSION});
425
+
404
426
  ${comps.map((c) => `pub mod ${c.module};`).join("\n")}${modules.map((m) => `\npub mod ${m.replace(/\.rs$/, "")};`).join("")}
405
427
  ${fixture}
406
428
  ${renderParams()
@@ -420,8 +442,9 @@ ${arms}
420
442
  }
421
443
  export function typesSource() {
422
444
  const home = blankComponent("types", "types", "types", ctx.typeStructs);
423
- const files = [...new Set([...ctx.typeFiles.values(), ...[...ctx.typeConsts.values()].map((c) => c.file)])].toSorted();
424
- const structs = [...ctx.typeStructs.values()]
445
+ const written = [...ctx.typeStructs.values()].filter((st) => typeWritten(st.name));
446
+ const files = [...new Set([...written.map((st) => ctx.typeFiles.get(st.name)), ...[...ctx.typeConsts.values()].map((c) => c.file)])].toSorted();
447
+ const structs = written
425
448
  .map((st) => structSource(st, home, `/// \`${st.name}\` in \`${ctx.typeFiles.get(st.name)}\`.\n`))
426
449
  .join("\n");
427
450
  const consts = [...ctx.typeConsts.values()].map((c) => `${c.text}\n`).join("");
@@ -0,0 +1,25 @@
1
+ /** A key or option that still works and goes in the next major release. */
2
+ export interface Deprecation {
3
+ /** The version that deprecated it. */
4
+ since: string;
5
+ /** What to write instead, as the warning names it: `` `router.routes` ``. */
6
+ use: string;
7
+ }
8
+ /** The part of JSON Schema (draft-07) that describes `ferrovue.config.json`, with `deprecated` naming
9
+ * what replaces a key. Descriptions come from the TSDoc of `Config` when `schema.json` is written. */
10
+ export interface Schema {
11
+ type?: "string" | "boolean" | "integer" | "object" | "array";
12
+ enum?: readonly string[];
13
+ default?: unknown;
14
+ minimum?: number;
15
+ items?: Schema;
16
+ properties?: Record<string, Schema>;
17
+ required?: readonly string[];
18
+ additionalProperties?: false | Schema;
19
+ oneOf?: readonly Schema[];
20
+ deprecated?: Deprecation;
21
+ }
22
+ /** The types a helper or twin takes, as the configuration spells them. */
23
+ export declare const TYPE_NAMES: readonly string[];
24
+ /** What `ferrovue.config.json` may hold: the keys the compiler reads, their types and defaults. */
25
+ export declare const CONFIG_SCHEMA: Schema;
package/dist/schema.js ADDED
@@ -0,0 +1,73 @@
1
+ const STRING = { type: "string" };
2
+ /** The types a helper or twin takes, as the configuration spells them. */
3
+ export const TYPE_NAMES = ["string", "string?", "int", "int?", "float", "float?", "bool"];
4
+ const TYPE_NAME = { type: "string", enum: TYPE_NAMES };
5
+ const ROUTES = { oneOf: [STRING, { type: "object", properties: { pages: STRING }, required: ["pages"], additionalProperties: false }] };
6
+ /** What `ferrovue.config.json` may hold: the keys the compiler reads, their types and defaults. */
7
+ export const CONFIG_SCHEMA = {
8
+ type: "object",
9
+ properties: {
10
+ $schema: STRING,
11
+ components: STRING,
12
+ out: STRING,
13
+ helpers: {
14
+ type: "object",
15
+ properties: {
16
+ module: STRING,
17
+ functions: {
18
+ type: "object",
19
+ additionalProperties: {
20
+ type: "object",
21
+ properties: { rust: STRING, params: { type: "array", items: TYPE_NAME }, returns: TYPE_NAME, maxLen: { type: "integer", minimum: 0, default: 0 } },
22
+ required: ["rust", "params", "returns"],
23
+ additionalProperties: false,
24
+ },
25
+ },
26
+ },
27
+ required: ["module", "functions"],
28
+ additionalProperties: false,
29
+ },
30
+ twins: {
31
+ type: "object",
32
+ additionalProperties: {
33
+ type: "object",
34
+ properties: { rust: STRING, props: { type: "object", additionalProperties: TYPE_NAME }, slots: { type: "array", items: STRING } },
35
+ required: ["rust"],
36
+ additionalProperties: false,
37
+ },
38
+ },
39
+ trustedHtml: STRING,
40
+ routes: ROUTES,
41
+ router: {
42
+ type: "object",
43
+ properties: {
44
+ routes: ROUTES,
45
+ base: { type: "string", default: "" },
46
+ linkActiveClass: { type: "string", default: "router-link-active" },
47
+ linkExactActiveClass: { type: "string", default: "router-link-exact-active" },
48
+ },
49
+ required: ["routes"],
50
+ additionalProperties: false,
51
+ },
52
+ stores: STRING,
53
+ clientDirectives: { type: "array", items: STRING },
54
+ i18n: {
55
+ type: "object",
56
+ properties: { messages: STRING, locale: { type: "string", default: "en" }, fallbackLocale: { oneOf: [STRING, { type: "array", items: STRING }] } },
57
+ required: ["messages"],
58
+ additionalProperties: false,
59
+ },
60
+ scopeId: { type: "string", enum: ["filepath", "filepath-source"], default: "filepath-source" },
61
+ viteRoot: { type: "string", default: "." },
62
+ isProduction: { type: "boolean", default: true },
63
+ cssModules: {
64
+ type: "object",
65
+ properties: { generateScopedName: STRING, hashPrefix: { type: "string", default: "" }, context: STRING },
66
+ required: ["generateScopedName"],
67
+ additionalProperties: false,
68
+ },
69
+ builders: { type: "boolean", default: true },
70
+ },
71
+ required: ["components", "out"],
72
+ additionalProperties: false,
73
+ };
package/dist/script.js CHANGED
@@ -3,8 +3,8 @@ import { fail, nothing, GenError, snake, takesAttrs, UNDEF } from "./model.js";
3
3
  import { CONFIG_FILE, ctx } from "./context.js";
4
4
  import { definePropsType, resolveImport, tyOfTs } from "./typescript.js";
5
5
  import { constOfDecl, declareConsts } from "./constants.js";
6
- import { rustTy } from "./rust.js";
7
- import { expr, fieldVal } from "./expr.js";
6
+ import { localTy } from "./rust.js";
7
+ import { expr, fieldVal, typedBy } from "./expr.js";
8
8
  import { collected, heldList } from "./lists.js";
9
9
  import { bare, operand, UNARY } from "./parens.js";
10
10
  import { known } from "./narrowing.js";
@@ -53,8 +53,8 @@ export function setupSource(init) {
53
53
  return setupSource(init.expression);
54
54
  return init;
55
55
  }
56
- function localTy(ty, comp) {
57
- return rustTy(ty, comp).replace(/\bCow<'a, /g, "std::borrow::Cow<").replace(/'a\b/g, "'_");
56
+ function statedTy(init) {
57
+ return init.type === "TSAsExpression" || init.type === "TSSatisfiesExpression" ? init.typeAnnotation : undefined;
58
58
  }
59
59
  function emptyRef(comp, init) {
60
60
  if (init?.type !== "CallExpression" || init.callee.type !== "Identifier" || !["ref", "shallowRef"].includes(init.callee.name))
@@ -281,10 +281,12 @@ export function scopeFor(comp, ast, components) {
281
281
  scope.propsIdent = local;
282
282
  continue;
283
283
  }
284
- const source = setupSource(init0);
285
284
  const isRef = init0.type === "CallExpression" && init0.callee.type === "Identifier" && ["ref", "shallowRef", "computed"].includes(init0.callee.name);
286
285
  if (isRef)
287
286
  scope.refs.add(local);
287
+ const typed = isRef ? init0.typeParameters?.params?.[0] : (d.id.typeAnnotation?.typeAnnotation ?? statedTy(init0));
288
+ const untyped = setupSource(init0);
289
+ const source = untyped && typed ? typedBy(untyped, typed) : untyped;
288
290
  let v;
289
291
  try {
290
292
  const empty = emptyRef(comp, init0);
@@ -306,6 +308,11 @@ export function scopeFor(comp, ast, components) {
306
308
  }
307
309
  const name = `s_${snake(local).replace(/^r#/, "")}`;
308
310
  const lone = v.lone ? { lone: true } : {};
311
+ if (v.slice) {
312
+ lets.push(`let ${name} = ${bare(v.code)};`);
313
+ scope.setup.set(local, { code: name, ty: v.ty, slice: true, ...lone });
314
+ continue;
315
+ }
309
316
  if (v.ty.k === "list" && v.iter !== undefined) {
310
317
  lets.push(`let ${name} = ${collected(v)};`);
311
318
  scope.setup.set(local, heldList(name, v.ty.of, v.lone));
package/dist/slots.js CHANGED
@@ -87,6 +87,8 @@ export function slotFieldValue(s, v, n) {
87
87
  if (v.ty.of.k === "opt" || nothing(v.ty.of))
88
88
  break;
89
89
  return v.code;
90
+ default:
91
+ break;
90
92
  }
91
93
  return fail(s.comp, "FV0911", "a slot prop is a string, a number, a boolean, an object or a list", n);
92
94
  }
package/dist/ssr.js CHANGED
@@ -1,7 +1,27 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { compileScript, compileTemplate, parse as parseSfc } from "@vue/compiler-sfc";
3
+ import { getEscapedCssVarName } from "@vue/shared";
3
4
  import * as vue from "vue";
4
5
  import * as serverRenderer from "vue/server-renderer";
6
+ const KEY = /(?:^|[{,])\s*"((?:[^"\\]|\\.)*)"\s*:/g;
7
+ function cssVarNames(file, component, count) {
8
+ const text = ["setup", "ssrRender"]
9
+ .map((k) => component[k])
10
+ .map((f) => (typeof f === "function" ? f.toString() : ""))
11
+ .join("\n");
12
+ const keys = (start) => {
13
+ const at = start.exec(text);
14
+ return at ? [...text.slice(at.index + at[0].length - 1).matchAll(KEY)].slice(0, count).map((m) => m[1]) : [];
15
+ };
16
+ const server = keys(/_cssVars = \{\s*style:\s*\{/);
17
+ if (server.length === count && server.every((n) => n.startsWith(":--")))
18
+ return server.map((n) => n.slice(3));
19
+ const client = keys(/useCssVars\)?\(\s*\(?_ctx\)?\s*=>\s*\(\{/);
20
+ if (client.length !== count) {
21
+ throw new Error(`${file}: \`v-bind()\` in \`<style>\`: the component holds none of the variables \`@vitejs/plugin-vue\` names; load it through the plugin`);
22
+ }
23
+ return client.map((k) => getEscapedCssVarName(JSON.parse(`"${k}"`), true));
24
+ }
5
25
  /** Give a component compiled for the browser the `ssrRender` its SSR build would have. */
6
26
  export function attachSsrRender(file, name, component) {
7
27
  const { descriptor } = parseSfc(readFileSync(file, "utf8"), { filename: file });
@@ -10,20 +30,38 @@ export function attachSsrRender(file, name, component) {
10
30
  if (!scopeId && descriptor.styles.some((st) => st.scoped)) {
11
31
  throw new Error(`${file}: a \`<style scoped>\` component without \`__scopeId\`: load it through \`@vitejs/plugin-vue\``);
12
32
  }
13
- const { code } = compileTemplate({
33
+ const varNames = descriptor.cssVars.length ? cssVarNames(file, component, descriptor.cssVars.length) : [];
34
+ let named = 0;
35
+ const code = compileTemplate({
14
36
  source: descriptor.template.content,
15
37
  filename: file,
16
38
  id: scopeId ?? name,
17
39
  scoped: !!scopeId,
18
40
  slotted: descriptor.slotted,
19
41
  ssr: true,
20
- ssrCssVars: [],
21
- compilerOptions: script ? { bindingMetadata: script.bindings } : {},
22
- });
42
+ ssrCssVars: descriptor.cssVars,
43
+ isProd: true,
44
+ compilerOptions: script?.bindings ? { bindingMetadata: script.bindings } : {},
45
+ }).code.replace(/^(\s*)":--[^"]*":/gm, (_, indent) => `${indent}":--${varNames[named++]}":`);
23
46
  const body = code
24
47
  .replace(/import \{([^}]*)\} from "vue"/g, (_, names) => `const {${names.replace(/ as /g, ": ")}} = __vue;`)
25
48
  .replace(/import \{([^}]*)\} from "vue\/server-renderer"/g, (_, names) => `const {${names.replace(/ as /g, ": ")}} = __sr;`)
26
49
  .replace("export function ssrRender", "return function ssrRender");
27
50
  // eslint-disable-next-line @typescript-eslint/no-implied-eval
28
51
  component.ssrRender = new Function("__vue", "__sr", body)(vue, serverRenderer);
52
+ const declared = /\n {2}const _cssVars = (\{ style: \{[^]*?\n\}\})\n/.exec(code)?.[1];
53
+ if (declared !== undefined)
54
+ exposeCssVars(component, body.slice(0, body.indexOf("return function ssrRender")), declared);
55
+ }
56
+ function exposeCssVars(component, imports, declared) {
57
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
58
+ const vars = new Function("__vue", "__sr", `${imports}return (_ctx, $props, $setup) => (${declared}).style;`)(vue, serverRenderer);
59
+ const setup = component.setup;
60
+ component.setup = (props, ctx) => {
61
+ const instance = vue.getCurrentInstance();
62
+ if (instance && !instance.getCssVars) {
63
+ instance.getCssVars = () => Object.fromEntries(Object.entries(vars(instance.proxy, instance.props, instance.setupState)).map(([k, v]) => [k.slice(3).replace(/\\(.)/g, "$1"), v]));
64
+ }
65
+ return setup?.(props, ctx);
66
+ };
29
67
  }
package/dist/styles.d.ts CHANGED
@@ -1,5 +1,9 @@
1
- import { type N, type Scope } from "./model.ts";
1
+ import { type Component, type N, type Scope } from "./model.ts";
2
2
  import { Emitter } from "./emitter.ts";
3
+ /** Put the object Vue's server compiler declares as `_cssVars`, the `v-bind()` variables in
4
+ * `<style>`, in place of each use of it in the compiled template's statements, with each value
5
+ * marked to be written as `ssrRenderStyle` writes a variable. */
6
+ export declare function inlineCssVars(comp: Component, body: N[]): N[];
3
7
  export declare function mergedStyle(s: Scope, e: Emitter, n: N): void;
4
8
  export declare function styleAttr(s: Scope, n: N): string;
5
9
  export interface StyleItem {