ferrovue 0.5.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 (108) hide show
  1. package/README.md +306 -62
  2. package/dist/args.d.ts +28 -0
  3. package/dist/args.js +69 -0
  4. package/dist/attrs.d.ts +2 -1
  5. package/dist/attrs.js +10 -4
  6. package/dist/calls.js +5 -2
  7. package/dist/children.d.ts +2 -2
  8. package/dist/children.js +84 -16
  9. package/dist/classes.d.ts +4 -0
  10. package/dist/classes.js +23 -1
  11. package/dist/cli.js +80 -81
  12. package/dist/client-render.d.ts +23 -0
  13. package/dist/client-render.js +143 -0
  14. package/dist/client.d.ts +74 -10
  15. package/dist/client.js +373 -42
  16. package/dist/compiler.d.ts +9 -5
  17. package/dist/compiler.js +80 -18
  18. package/dist/component.d.ts +7 -3
  19. package/dist/component.js +38 -27
  20. package/dist/conformance.d.ts +85 -0
  21. package/dist/conformance.js +327 -0
  22. package/dist/constants.js +4 -2
  23. package/dist/context.d.ts +66 -10
  24. package/dist/context.js +176 -5
  25. package/dist/diagnostics.d.ts +6 -4
  26. package/dist/diagnostics.js +7 -3
  27. package/dist/dynamic.d.ts +18 -0
  28. package/dist/dynamic.js +576 -0
  29. package/dist/editor.d.ts +18 -0
  30. package/dist/editor.js +59 -0
  31. package/dist/errors.d.ts +213 -4
  32. package/dist/errors.js +129 -4
  33. package/dist/expr.d.ts +2 -0
  34. package/dist/expr.js +91 -12
  35. package/dist/fallthrough.d.ts +1 -0
  36. package/dist/fallthrough.js +10 -2
  37. package/dist/file-routes.d.ts +33 -0
  38. package/dist/file-routes.js +224 -0
  39. package/dist/files.d.ts +13 -0
  40. package/dist/files.js +49 -0
  41. package/dist/fixture.d.ts +16 -0
  42. package/dist/fixture.js +22 -7
  43. package/dist/hydration.d.ts +12 -1
  44. package/dist/hydration.js +47 -1
  45. package/dist/index.d.ts +3 -3
  46. package/dist/index.js +1 -1
  47. package/dist/inputs.d.ts +24 -0
  48. package/dist/inputs.js +40 -0
  49. package/dist/link-router.d.ts +7 -3
  50. package/dist/link-router.js +8 -4
  51. package/dist/lists.d.ts +16 -1
  52. package/dist/lists.js +78 -4
  53. package/dist/loops.js +4 -4
  54. package/dist/model.d.ts +20 -1
  55. package/dist/model.js +46 -1
  56. package/dist/numbers.js +3 -2
  57. package/dist/page-routes.d.ts +10 -0
  58. package/dist/page-routes.js +10 -0
  59. package/dist/paths.d.ts +5 -0
  60. package/dist/paths.js +7 -0
  61. package/dist/plugin.d.ts +15 -1
  62. package/dist/plugin.js +3 -0
  63. package/dist/plugins/client-only.js +3 -0
  64. package/dist/plugins/css-modules.d.ts +39 -0
  65. package/dist/plugins/css-modules.js +135 -0
  66. package/dist/plugins/head.d.ts +15 -0
  67. package/dist/plugins/head.js +385 -0
  68. package/dist/plugins/i18n.js +5 -12
  69. package/dist/plugins/index.js +4 -1
  70. package/dist/plugins/provide.d.ts +44 -0
  71. package/dist/plugins/provide.js +530 -0
  72. package/dist/plugins/router-link.js +7 -3
  73. package/dist/plugins/router.d.ts +4 -1
  74. package/dist/plugins/router.js +58 -11
  75. package/dist/plugins/scoped.d.ts +10 -0
  76. package/dist/plugins/scoped.js +17 -4
  77. package/dist/plugins/shared-types.js +2 -1
  78. package/dist/plugins/stores.js +26 -15
  79. package/dist/routes.d.ts +5 -2
  80. package/dist/routes.js +8 -2
  81. package/dist/rust.d.ts +9 -1
  82. package/dist/rust.js +83 -32
  83. package/dist/schema.d.ts +25 -0
  84. package/dist/schema.js +73 -0
  85. package/dist/script.js +31 -8
  86. package/dist/settle.d.ts +2 -0
  87. package/dist/settle.js +5 -0
  88. package/dist/slots.js +20 -4
  89. package/dist/ssr.d.ts +3 -0
  90. package/dist/ssr.js +67 -0
  91. package/dist/strings.d.ts +3 -0
  92. package/dist/strings.js +16 -2
  93. package/dist/styles.d.ts +5 -1
  94. package/dist/styles.js +131 -4
  95. package/dist/template.js +92 -11
  96. package/dist/testing.d.ts +4 -4
  97. package/dist/testing.js +3 -30
  98. package/dist/types.d.ts +5 -0
  99. package/dist/typescript.d.ts +11 -1
  100. package/dist/typescript.js +131 -36
  101. package/dist/vhtml.d.ts +4 -0
  102. package/dist/vhtml.js +63 -0
  103. package/dist/vite.d.ts +25 -11
  104. package/dist/vite.js +166 -51
  105. package/dist/volar.d.ts +15 -0
  106. package/dist/volar.js +87 -0
  107. package/package.json +41 -7
  108. package/schema.json +333 -0
package/dist/args.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ import { GenError } from "./model.ts";
2
+ import type { Deprecation } from "./schema.ts";
3
+ /** A command-line option: its names, short one first, the value it takes, and its line in `--help`. */
4
+ export interface Flag {
5
+ names: readonly string[];
6
+ value?: string;
7
+ help: string;
8
+ deprecated?: Deprecation;
9
+ }
10
+ /** The commands `ferrovue` takes, with their lines in `--help`. */
11
+ export declare const COMMANDS: Readonly<Record<string, string>>;
12
+ /** The options `ferrovue` takes. */
13
+ export declare const FLAGS: readonly Flag[];
14
+ /** What the command line asks for: the command, and each option given by its long name, with its
15
+ * value or `true`. */
16
+ export interface Args {
17
+ command: string | null;
18
+ options: Map<string, string | true>;
19
+ /** The name each option was given by, as an error about it quotes it. */
20
+ spelt: Map<string, string>;
21
+ warnings: GenError[];
22
+ }
23
+ /** `ferrovue --help`. */
24
+ export declare function helpText(flags?: readonly Flag[], commands?: Readonly<Record<string, string>>): string;
25
+ /** Read the command line, or say what is wrong with it. A deprecated option gives a warning. */
26
+ export declare function parseArgs(argv: readonly string[], flags?: readonly Flag[], commands?: Readonly<Record<string, string>>): Args | {
27
+ error: string;
28
+ };
package/dist/args.js ADDED
@@ -0,0 +1,69 @@
1
+ import { GenError } from "./model.js";
2
+ /** The commands `ferrovue` takes, with their lines in `--help`. */
3
+ export const COMMANDS = {
4
+ init: "scaffold starter ferrovue.config.json and components",
5
+ };
6
+ /** The options `ferrovue` takes. */
7
+ export const FLAGS = [
8
+ { names: ["--check"], help: "check that generated files match committed files without writing" },
9
+ { names: ["-d", "--diff"], help: "show unified diff of changes when checking (use with --check)" },
10
+ { names: ["--format"], value: "<format>", help: "how to write errors and results: human (default) or json" },
11
+ { names: ["--watch"], help: "watch components, stores, routes and config for changes and regenerate" },
12
+ { names: ["-c", "--config"], value: "<path>", help: "path to configuration file (default: ferrovue.config.json)" },
13
+ { names: ["-v", "--version"], help: "print the version and exit" },
14
+ { names: ["-h", "--help"], help: "print this help and exit" },
15
+ ];
16
+ const longName = (flag) => flag.names.at(-1);
17
+ /** `ferrovue --help`. */
18
+ export function helpText(flags = FLAGS, commands = COMMANDS) {
19
+ const line = (left, help) => ` ${left.padEnd(23)} ${help}`;
20
+ const option = (flag) => {
21
+ const [short, long] = flag.names.length > 1 ? [`${flag.names[0]}, `, longName(flag)] : [" ", longName(flag)];
22
+ const help = flag.deprecated ? `${flag.help} (deprecated: use ${flag.deprecated.use})` : flag.help;
23
+ return line(`${short}${long}${flag.value ? ` ${flag.value}` : ""}`, help);
24
+ };
25
+ return `Usage: ferrovue [command] [options]
26
+
27
+ Compile Vue components to Rust render functions, run from the project root.
28
+
29
+ Commands:
30
+ ${Object.entries(commands).map(([name, help]) => line(name, help)).join("\n")}
31
+
32
+ Options:
33
+ ${flags.map(option).join("\n")}`;
34
+ }
35
+ /** Read the command line, or say what is wrong with it. A deprecated option gives a warning. */
36
+ export function parseArgs(argv, flags = FLAGS, commands = COMMANDS) {
37
+ const args = { command: null, options: new Map(), spelt: new Map(), warnings: [] };
38
+ for (let i = 0; i < argv.length; i++) {
39
+ const arg = argv[i];
40
+ const eq = arg.startsWith("--") ? arg.indexOf("=") : -1;
41
+ const name = eq === -1 ? arg : arg.slice(0, eq);
42
+ const flag = flags.find((f) => f.names.includes(name));
43
+ if (!flag) {
44
+ if (arg in commands && args.command === null) {
45
+ args.command = arg;
46
+ continue;
47
+ }
48
+ return { error: `unknown option or command '${arg}'\n\nRun \`ferrovue --help\` for usage.` };
49
+ }
50
+ let value = true;
51
+ if (flag.value) {
52
+ if (eq !== -1)
53
+ value = arg.slice(eq + 1);
54
+ else if (i + 1 < argv.length && !argv[i + 1].startsWith("-"))
55
+ value = argv[++i];
56
+ else
57
+ return { error: `option '${name}' requires an argument` };
58
+ }
59
+ else if (eq !== -1) {
60
+ return { error: `option '${name}' takes no value` };
61
+ }
62
+ if (flag.deprecated) {
63
+ args.warnings.push(new GenError("FV1118", `'${name}' is deprecated since ${flag.deprecated.since}, and goes in the next major release: use ${flag.deprecated.use}`));
64
+ }
65
+ args.options.set(longName(flag), value);
66
+ args.spelt.set(longName(flag), name);
67
+ }
68
+ return args;
69
+ }
package/dist/attrs.d.ts CHANGED
@@ -4,7 +4,8 @@ export declare function display(e: Emitter, v: Val): boolean;
4
4
  export declare function interpolate(s: Scope, e: Emitter, v: Val, n: N): void;
5
5
  export declare function attrValue(e: Emitter, v: Val): void;
6
6
  export declare function renderAttr(s: Scope, e: Emitter, key: string, v: Val, n: N): void;
7
- export declare function renderDynamicAttr(s: Scope, e: Emitter, key: string, v: Val, n: N): void;
7
+ export declare function keepsAttrCase(tag: string): boolean;
8
+ export declare function renderDynamicAttr(s: Scope, e: Emitter, key: string, v: Val, n: N, tag?: string | null): void;
8
9
  export declare const IGNORED_PROPS: Set<string>;
9
10
  export declare function directiveProps(s: Scope, n: N): boolean;
10
11
  export declare function isAttrs(n: N): boolean;
package/dist/attrs.js CHANGED
@@ -1,4 +1,4 @@
1
- import { escapeHtml, hyphenate, isBooleanAttr, isSSRSafeAttrName, propsToAttrMap } from "@vue/shared";
1
+ import { escapeHtml, hyphenate, isBooleanAttr, isSSRSafeAttrName, isSVGTag, propsToAttrMap } from "@vue/shared";
2
2
  import { fail, nothing, GenError, rustStr } from "./model.js";
3
3
  import { CONFIG_FILE, ctx } from "./context.js";
4
4
  import { describeTy, expr } from "./expr.js";
@@ -75,8 +75,11 @@ export function renderAttr(s, e, key, v, n) {
75
75
  attrValue(e, v);
76
76
  e.lit(`"`);
77
77
  }
78
- export function renderDynamicAttr(s, e, key, v, n) {
79
- const name = propsToAttrMap[key] ?? key.toLowerCase();
78
+ export function keepsAttrCase(tag) {
79
+ return tag.indexOf("-") > 0 || isSVGTag(tag);
80
+ }
81
+ export function renderDynamicAttr(s, e, key, v, n, tag = null) {
82
+ const name = tag !== null && keepsAttrCase(tag) ? key : (propsToAttrMap[key] ?? key.toLowerCase());
80
83
  if (!isSSRSafeAttrName(name))
81
84
  fail(s.comp, "FV0404", `unsafe attribute name \`${name}\``, n);
82
85
  renderable(s, key, v, n);
@@ -91,7 +94,7 @@ export function renderDynamicAttr(s, e, key, v, n) {
91
94
  }
92
95
  if (v.ty.k === "opt") {
93
96
  e.open(`if let Some(v) = ${v.code}`);
94
- renderDynamicAttr(s, e, key, { code: "v", ty: v.ty.of }, n);
97
+ renderDynamicAttr(s, e, key, { code: "v", ty: v.ty.of }, n, tag);
95
98
  e.close();
96
99
  return;
97
100
  }
@@ -335,7 +338,10 @@ export function valueAttr(s, v, n) {
335
338
  const make = of === "str" ? "fv::Attr::str" : of === "int" ? "fv::Attr::Int" : of === "float" ? "fv::Attr::Float" : of === "bool" ? "fv::Attr::Bool" : null;
336
339
  if (make !== null)
337
340
  return `${atom(v.code)}.map_or(fv::Attr::Undefined, ${make})`;
341
+ break;
338
342
  }
343
+ default:
344
+ break;
339
345
  }
340
346
  return fail(s.comp, "FV0412", "an attribute that may fall through is a string, a number or a boolean", n);
341
347
  }
package/dist/calls.js CHANGED
@@ -2,7 +2,7 @@ import { BOOL, fail, FLOAT, INT, sameTy, STR } from "./model.js";
2
2
  import { ctx } from "./context.js";
3
3
  import { claim } from "./plugin.js";
4
4
  import { atom, bare, binary, condition, operand, receiver, strArg, UNARY } from "./parens.js";
5
- import { computedListMethod, items, listMethod, objectCall } from "./lists.js";
5
+ import { computedListMethod, isBoolean, items, listMethod, objectCall, presentLiteral } from "./lists.js";
6
6
  import { lonely, loneOf, meet, stringMethod, stringsEqual } from "./strings.js";
7
7
  import { asF64, intFromF64, isNumber } from "./numbers.js";
8
8
  import { boolOf, truthy } from "./narrowing.js";
@@ -41,6 +41,8 @@ export function call(s, n) {
41
41
  const helper = s.helpers.get(callee.name);
42
42
  if (helper)
43
43
  return helperCall(s, helper, args, n);
44
+ if (isBoolean(s, callee) && args.length === 1)
45
+ return boolOf(truthy(expr(s, args[0])));
44
46
  if (callee.name === "String" && args.length === 1) {
45
47
  const a = expr(s, args[0]);
46
48
  if (a.ty.k === "str")
@@ -121,7 +123,8 @@ export function call(s, n) {
121
123
  }
122
124
  return fail(comp, "FV0704", `\`Math.${method}()\` is supported on numbers as \`max\`, \`min\`, \`abs\`, \`round\`, \`floor\`, \`ceil\` and \`trunc\``, n);
123
125
  }
124
- const target = expr(s, callee.object);
126
+ const literal = method === "filter" && args.length === 1 && isBoolean(s, args[0]) && callee.object.type === "ArrayExpression";
127
+ const target = (literal ? presentLiteral(s, callee.object) : null) ?? expr(s, callee.object);
125
128
  if (target.ty.k === "str") {
126
129
  const lone = loneOf(target);
127
130
  switch (args.length === 0 ? method : "") {
@@ -10,7 +10,7 @@ export declare function renderChild(s: Scope, e: Emitter, n: N, twin?: TwinCall)
10
10
  export declare function array(key: string): boolean;
11
11
  export declare function fieldInit(name: string, value: string): string;
12
12
  export declare function takesSlots(c: Component): boolean;
13
- export declare function extraParams(c: Component): string;
14
- export declare function callChild(s: Scope, e: Emitter, child: Component, propsCode: string, slots: N, attrs: string | null): void;
13
+ export declare function extraParams(c: Component, unread?: (name: string) => boolean): string;
14
+ export declare function callChild(s: Scope, e: Emitter, child: Component, propsCode: string, slots: N, attrs: string | null, later?: boolean): void;
15
15
  export declare function callWith(s: Scope, e: Emitter, child: Component, m: string, head: string, slotsTy: string, trailing: string, slots: N, vnode?: boolean): void;
16
16
  export declare function ownInto(comp: Component, v: Val, want: Ty, node: N): string;
package/dist/children.js CHANGED
@@ -6,11 +6,12 @@ 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";
9
- import { attrOf, dollarAttrs, IGNORED_PROPS, isAttrs, mergedParts } from "./attrs.js";
10
- import { passedKey } from "./fallthrough.js";
11
- import { claim, paramsOf, slotFieldsOf } from "./plugin.js";
9
+ import { attrOf, dollarAttrs, isAttrs, mergedParts } from "./attrs.js";
10
+ import { isListener, passedKey } from "./fallthrough.js";
11
+ import { claim, paramsOf, slotContextOf, slotFieldsOf } from "./plugin.js";
12
12
  import { statements } from "./template.js";
13
13
  import { slotContent, slotFieldBorrows, staticallyFilled } from "./slots.js";
14
+ import { slotAsVnodes } from "./dynamic.js";
14
15
  export function renderChild(s, e, n, twin) {
15
16
  const [target, rawProps, slots, , slotScopeId] = n.arguments;
16
17
  let local = null;
@@ -22,13 +23,24 @@ export function renderChild(s, e, n, twin) {
22
23
  const child = twin?.comp ?? (childName ? s.components.get(childName) : undefined);
23
24
  if (!child)
24
25
  fail(s.comp, "FV0501", `a child component must be imported from a \`.vue\` file among the components compiled, in ${ctx.componentsDir.replace(/\/$/, "")}`, n);
26
+ const later = !twin && local !== null && s.loadedLater.has(local);
25
27
  for (const p of ctx.plugins)
26
28
  p.child?.(s, child, n);
27
29
  const parts = !rawProps || rawProps.type === "NullLiteral" ? [] : mergedParts(rawProps);
28
30
  const merges = parts.length > 0 && parts[0] !== rawProps;
29
31
  const passesAttrs = parts.some(isAttrs);
30
32
  const passed = (p) => s.fallthrough !== null && ((isAttrs(p) && s.comp.inheritAttrs) || dollarAttrs(s, p));
31
- const marker = (p) => p.type === "ObjectExpression" && p.properties.length > 0 && p.properties.every((q) => q.type === "ObjectProperty" && IGNORED_PROPS.has(q.key.name ?? q.key.value));
33
+ const handlers = (p) => {
34
+ const dynamic = p.type === "ObjectProperty" && p.computed && isCall(p.key, "_toHandlerKey");
35
+ if (!isCall(p, "_toHandlers") && !dynamic)
36
+ return false;
37
+ const declared = child.props.fields.find((f) => isListener(f.js));
38
+ if (declared)
39
+ fail(s.comp, "FV0514", `a listener whose name is not known before rendering, given to ${child.name}, which declares \`${declared.js}\` as a prop`, p);
40
+ return true;
41
+ };
42
+ const silent = (p) => p.type === "ObjectProperty" && !p.computed && !passedKey(child, p.key.name ?? String(p.key.value));
43
+ const marker = (p) => handlers(p) || (p.type === "ObjectExpression" && p.properties.length > 0 && p.properties.every((q) => silent(q) || handlers(q)));
32
44
  const objects = parts.filter((p) => !isAttrs(p) && !dollarAttrs(s, p) && !marker(p));
33
45
  if (parts.some(passed)) {
34
46
  for (const name of s.comp.attrNames) {
@@ -44,7 +56,7 @@ export function renderChild(s, e, n, twin) {
44
56
  const v = expr(s, objects[0]);
45
57
  const own = child.name === s.comp.name && v.ty.k === "struct" && v.ty.name === "Props";
46
58
  if (own || (v.ty.k === "child" && v.ty.name === child.name)) {
47
- callChild(s, e, child, v.code, slots, childAttrsArg(s, child, parts, merges, null, ids, n));
59
+ callChild(s, e, child, v.code, slots, childAttrsArg(s, child, parts, merges, null, ids, n), later);
48
60
  return;
49
61
  }
50
62
  fail(s.comp, "FV0503", `child props must be an object literal, or \`v-bind\` of ${child.name}'s own \`Props\``, n);
@@ -54,9 +66,12 @@ export function renderChild(s, e, n, twin) {
54
66
  fail(s.comp, "FV0503", `child props must be an object literal, or \`v-bind\` of ${child.name}'s own \`Props\``, p);
55
67
  }
56
68
  const given = new Map();
57
- const fallthrough = new Set();
69
+ const fallthrough = new Map();
70
+ const loaded = !merges && local !== null && s.loadedLater.has(local);
58
71
  for (const obj of objects) {
59
72
  for (const p of obj.properties) {
73
+ if (handlers(p))
74
+ continue;
60
75
  if (p.type !== "ObjectProperty" || p.computed)
61
76
  fail(s.comp, "FV0504", "child props hold plain keys", p);
62
77
  const key = p.key.type === "Identifier" ? p.key.name : String(p.key.value);
@@ -72,7 +87,7 @@ export function renderChild(s, e, n, twin) {
72
87
  const name = propsToAttrMap[key] ?? key.toLowerCase();
73
88
  if (key !== "class" && key !== "style" && !isSSRSafeAttrName(name))
74
89
  fail(s.comp, "FV0404", `unsafe attribute name \`${name}\``, p);
75
- fallthrough.add(p);
90
+ fallthrough.set(p, loaded && key === "class" ? loadedClass(s, p.value) : p.value);
76
91
  }
77
92
  }
78
93
  if (twin) {
@@ -92,11 +107,41 @@ export function renderChild(s, e, n, twin) {
92
107
  const v = expr(s, node);
93
108
  return fieldInit(f.rust, ownInto(s.comp, { ...v, ty: markHome(v.ty, s.comp.name) }, markHome(f.ty, child.name), node));
94
109
  });
95
- callChild(s, e, child, `&super::${child.module}::Props { ${inits.join(", ")} }`, slots, childAttrsArg(s, child, parts, merges, fallthrough, ids, n));
110
+ callChild(s, e, child, `&super::${child.module}::Props { ${inits.join(", ")} }`, slots, childAttrsArg(s, child, parts, merges, fallthrough, ids, n), later);
111
+ }
112
+ function isCall(n, callee) {
113
+ return n?.type === "CallExpression" && n.callee.type === "Identifier" && n.callee.name === callee;
96
114
  }
97
115
  export function array(key) {
98
116
  return /^(0|[1-9]\d*)$/.test(key) && Number(key) < 2 ** 32 - 1;
99
117
  }
118
+ /** A string `:class` given to a component `defineAsyncComponent` loads, in props no `mergeProps`
119
+ * normalised: Vue's wrapper hands on the class once as written and once trimmed, so a class with
120
+ * whitespace around it is written twice. */
121
+ const lengthOf = (n) => ({ type: "MemberExpression", object: n, property: { type: "Identifier", name: "length" }, computed: false });
122
+ function holdsClassLiteral(n) {
123
+ if (n.type === "ObjectExpression" || n.type === "ArrayExpression")
124
+ return true;
125
+ if (n.type === "ConditionalExpression")
126
+ return holdsClassLiteral(n.consequent) || holdsClassLiteral(n.alternate);
127
+ if (n.type === "LogicalExpression")
128
+ return holdsClassLiteral(n.left) || holdsClassLiteral(n.right);
129
+ return false;
130
+ }
131
+ function loadedClass(s, value) {
132
+ if (value.type === "ConditionalExpression" && (holdsClassLiteral(value.consequent) || holdsClassLiteral(value.alternate))) {
133
+ return { ...value, consequent: loadedClass(s, value.consequent), alternate: loadedClass(s, value.alternate) };
134
+ }
135
+ if (holdsClassLiteral(value) || expr(s, value).ty.k !== "str")
136
+ return value;
137
+ const trimmed = { type: "CallExpression", callee: { type: "MemberExpression", object: value, property: { type: "Identifier", name: "trim" }, computed: false }, arguments: [] };
138
+ return {
139
+ type: "ConditionalExpression",
140
+ test: { type: "BinaryExpression", operator: "===", left: lengthOf(trimmed), right: lengthOf(value) },
141
+ consequent: value,
142
+ alternate: { type: "ArrayExpression", elements: [trimmed, value] },
143
+ };
144
+ }
100
145
  function childAttrsArg(s, child, parts, merges, fallthrough, ids, n) {
101
146
  const sources = [];
102
147
  for (const p of parts) {
@@ -106,7 +151,7 @@ function childAttrsArg(s, child, parts, merges, fallthrough, ids, n) {
106
151
  else if (p.type === "ObjectExpression" && fallthrough !== null) {
107
152
  const entries = p.properties.filter((q) => fallthrough.has(q)).map((q) => {
108
153
  const key = q.key.type === "Identifier" ? q.key.name : String(q.key.value);
109
- return `(${rustStr(key)}, ${attrOf(s, key, q.value, "vnode")})`;
154
+ return `(${rustStr(key)}, ${attrOf(s, key, fallthrough.get(q), "vnode")})`;
110
155
  });
111
156
  if (entries.length)
112
157
  sources.push(`&[${entries.join(", ")}]`);
@@ -129,15 +174,23 @@ export function fieldInit(name, value) {
129
174
  export function takesSlots(c) {
130
175
  return c.slotNames.length > 0 || slotFieldsOf(c).length > 0;
131
176
  }
132
- export function extraParams(c) {
133
- return (takesSlots(c) ? ", fv_slots: Slots<'_>" : "") + paramsOf(c).map((p) => `, ${p.name}: ${p.ty}`).join("");
177
+ export function extraParams(c, unread = () => false) {
178
+ return (takesSlots(c) ? ", fv_slots: Slots<'_>" : "") + paramsOf(c).map((p) => `, ${unread(p.name) ? "_" : ""}${p.name}: ${p.ty}`).join("");
134
179
  }
135
- export function callChild(s, e, child, propsCode, slots, attrs) {
180
+ export function callChild(s, e, child, propsCode, slots, attrs, later = false) {
136
181
  const m = `super::${child.module}`;
137
182
  const scoped = child.inherits || takesAttrs(child);
138
183
  const trailing = paramsOf(child).map((p) => `, ${p.name}`).join("") + (scoped ? `, ${attrs ?? (takesAttrs(child) ? "&fv::Attrs::NONE" : '""')}` : "");
139
184
  const render = scoped ? "render_scoped" : "render";
185
+ const around = later ? ctx.plugins.flatMap((p) => p.loadedLater?.(child) ?? []) : [];
186
+ if (around.length) {
187
+ e.open("");
188
+ for (const line of around)
189
+ e.stmt(line);
190
+ }
140
191
  callWith(s, e, child, m, `${m}::${render}(out, ${propsCode}`, `${m}::Slots`, trailing, slots);
192
+ if (around.length)
193
+ e.close();
141
194
  }
142
195
  export function callWith(s, e, child, m, head, slotsTy, trailing, slots, vnode = false) {
143
196
  const given = new Map();
@@ -160,6 +213,9 @@ export function callWith(s, e, child, m, head, slotsTy, trailing, slots, vnode =
160
213
  return;
161
214
  }
162
215
  e.open(`${head}, ${slotsTy}`);
216
+ const context = slotContextOf(child);
217
+ const contextParams = context.map((p) => `, ${p.name}: ${p.ty}`).join("");
218
+ const opaque = vnode ? child.name : s.opaque;
163
219
  for (const name of child.slotNames) {
164
220
  const field = snake(name);
165
221
  const value = given.get(name);
@@ -168,6 +224,7 @@ export function callWith(s, e, child, m, head, slotsTy, trailing, slots, vnode =
168
224
  continue;
169
225
  }
170
226
  const { body, param } = slotContent(s, value);
227
+ const asVnodes = vnode || s.vnode || slotAsVnodes(child, name);
171
228
  const shape = child.slotShapes.get(name);
172
229
  const takesNone = param?.type === "Identifier" && param.name === "_";
173
230
  const sid = child.passesSlotIds ? `fv_sid${++ctx.narrowCount}` : null;
@@ -205,18 +262,29 @@ export function callWith(s, e, child, m, head, slotsTy, trailing, slots, vnode =
205
262
  else if (!takesNone)
206
263
  fail(s.comp, "FV0905", "slot props are a name or an object pattern", param);
207
264
  const life = shape.fields.some((f) => slotFieldBorrows(f.ty)) ? "<'_>" : "";
208
- e.open(`${field}: Some(&|out: &mut String, ${sp}: &${m}::${shape.name}${life}${sidParam}| -> bool`);
265
+ e.open(`${field}: Some(&|out: &mut String, ${sp}: &${m}::${shape.name}${life}${sidParam}${contextParams}| -> bool`);
209
266
  const opened = e.lines.length - 1;
210
- content({ ...s, locals, sid, vnode });
267
+ content({ ...s, locals, sid, vnode: asVnodes, opaque });
211
268
  unread(opened, sp);
212
269
  unread(opened, sid);
270
+ for (const p of context)
271
+ unread(opened, p.name);
213
272
  e.close("),");
214
273
  continue;
215
274
  }
216
275
  if (!takesNone)
217
276
  fail(s.comp, "FV0906", `\`<slot${name === "default" ? "" : ` name="${name}"`}>\` in ${child.name} passes no props`, param);
218
- const inner = { ...s, sid, vnode };
219
- if (sid !== null) {
277
+ const inner = { ...s, sid, vnode: asVnodes, opaque };
278
+ if (context.length) {
279
+ e.open(`${field}: Some(&|out: &mut String${sidParam}${contextParams}| -> bool`);
280
+ const opened = e.lines.length - 1;
281
+ content(inner);
282
+ unread(opened, sid);
283
+ for (const p of context)
284
+ unread(opened, p.name);
285
+ e.close("),");
286
+ }
287
+ else if (sid !== null) {
220
288
  e.open(`${field}: Some(fv::Slot::slotted(&|out: &mut String${sidParam}| -> bool`);
221
289
  const opened = e.lines.length - 1;
222
290
  content(inner);
package/dist/classes.d.ts CHANGED
@@ -7,6 +7,10 @@ export type ClassItem = {
7
7
  };
8
8
  export declare function classItems(s: Scope, n: N): ClassItem[];
9
9
  export declare function renderClass(s: Scope, e: Emitter, n: N, after?: boolean): void;
10
+ /** The class list of an element in content Vue renders from virtual nodes, in the order Vue's
11
+ * client compiler merges a static `class` written before `:class`: the static names first, at the
12
+ * place of the static `class`. */
13
+ export declare function vnodeClass(s: Scope, n: N): N;
10
14
  export declare function classPresent(s: Scope, n: N): string;
11
15
  export declare function maybeEqual(s: Scope, n: N): boolean;
12
16
  export declare function literalClass(n: N): string | null;
package/dist/classes.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { escapeHtml } from "@vue/shared";
2
- import { fail, nothing, rustStr, STR } from "./model.js";
2
+ import { directiveOwner, 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";
@@ -102,6 +102,28 @@ export function renderClass(s, e, n, after = false) {
102
102
  e.stmt(`fv::class_into(out, ${wrote}, &[${rest.join(", ")}]);`);
103
103
  }
104
104
  }
105
+ const UNWRITTEN = new Set(["on", "if", "else-if", "else", "for", "slot", "once", "memo", "cloak", "pre"]);
106
+ /** The class list of an element in content Vue renders from virtual nodes, in the order Vue's
107
+ * client compiler merges a static `class` written before `:class`: the static names first, at the
108
+ * place of the static `class`. */
109
+ export function vnodeClass(s, n) {
110
+ const last = n.type === "ArrayExpression" ? n.elements.at(-1) : null;
111
+ if (n.fvMerged || last?.type !== "StringLiteral" || n.elements.length < 2)
112
+ return n;
113
+ const el = directiveOwner(s.comp, n.elements[0]);
114
+ if (!el)
115
+ return n;
116
+ const own = el.props.findIndex((p) => p.type === 6 && p.name === "class");
117
+ const bound = el.props.findIndex((p) => p.type === 7 && p.name === "bind" && p.arg?.content === "class");
118
+ if (own < 0 || bound < own)
119
+ return n;
120
+ const between = el.props.slice(own + 1, bound).find((p) => p.type === 6 || !UNWRITTEN.has(p.name));
121
+ if (between) {
122
+ const at = { type: "VueTemplate", loc: { start: { line: between.loc.start.line, column: between.loc.start.column - 1 } }, __fv: "source" };
123
+ fail(s.comp, "FV1013", "in content Vue renders from virtual nodes, a `class` written before `:class` is written where the `class` stands, and Vue's server compiler moves it to where `:class` stands; write the two next to each other, or `:class` first", at);
124
+ }
125
+ return { ...n, elements: [last, ...n.elements.slice(0, -1)] };
126
+ }
105
127
  export function classPresent(s, n) {
106
128
  if (n.fvMerged)
107
129
  return n.elements.reduce((acc, el) => logical(acc, "||", classPresent(s, el)), "false");