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
@@ -0,0 +1,576 @@
1
+ import { parse as parseJs } from "@babel/parser";
2
+ import { isHTMLTag, isVoidTag } from "@vue/shared";
3
+ import { basename } from "node:path";
4
+ import { camelize, fail, GenError, rustStr, tagAst } from "./model.js";
5
+ import { ctx } from "./context.js";
6
+ import { expr } from "./expr.js";
7
+ import { cond, known } from "./narrowing.js";
8
+ import { condition } from "./parens.js";
9
+ import { Emitter } from "./emitter.js";
10
+ import { isAttrs, mergedParts, renderAttrs } from "./attrs.js";
11
+ import { claim } from "./plugin.js";
12
+ import { definePropsType, resolveImport } from "./typescript.js";
13
+ import { setupSource } from "./script.js";
14
+ import { slotBody } from "./slots.js";
15
+ import { push, statements } from "./template.js";
16
+ const setupAsts = new WeakMap();
17
+ const vnodeSlots = new WeakMap();
18
+ const RESERVED_TAGS = new Set(["template", "slot", "component", "textarea"]);
19
+ const OPEN = "`<component :is>` renders a closed set of choices: imported components, element names, a `computed` or `?:` choosing among them, a prop typed as a union of string literals, or an object of imported components read by such a prop";
20
+ /** Whether the content a parent gives `slot` of `child` is rendered as virtual nodes, as it is
21
+ * inside an element a `<component :is>` chooses. */
22
+ export function slotAsVnodes(child, slot) {
23
+ return vnodeSlots.get(child)?.has(slot) ?? false;
24
+ }
25
+ function dynamicOf(st) {
26
+ const c = st?.type === "ExpressionStatement" ? st.expression : null;
27
+ if (c?.type !== "CallExpression" || c.callee.type !== "Identifier" || c.callee.name !== "_ssrRenderVNode")
28
+ return null;
29
+ const vnode = c.arguments[1];
30
+ const resolved = vnode?.type === "CallExpression" && vnode.callee.type === "Identifier" && vnode.callee.name === "_createVNode" ? vnode.arguments[0] : null;
31
+ if (resolved?.type !== "CallExpression" || resolved.callee.type !== "Identifier" || resolved.callee.name !== "_resolveDynamicComponent")
32
+ return null;
33
+ return { is: resolved.arguments[0], props: vnode.arguments[1], slots: vnode.arguments[2], parent: c.arguments[2], sid: c.arguments[3], at: vnode };
34
+ }
35
+ /** Whether a statement of a compiled template renders a `<component :is>`. */
36
+ export function isDynamicComponent(st) {
37
+ return dynamicOf(st) !== null;
38
+ }
39
+ function unwrap(n) {
40
+ while (n && ["TSAsExpression", "TSSatisfiesExpression", "TSNonNullExpression", "ParenthesizedExpression"].includes(n.type))
41
+ n = n.expression;
42
+ return n;
43
+ }
44
+ function setupName(n) {
45
+ if (n?.type === "MemberExpression" && n.object.type === "Identifier" && (n.object.name === "$setup" || n.object.name === "_ctx")) {
46
+ return n.computed ? (n.property.type === "StringLiteral" ? n.property.value : null) : n.property.name;
47
+ }
48
+ return n?.type === "Identifier" ? n.name : null;
49
+ }
50
+ function declared(s, name) {
51
+ for (const st of setupAsts.get(s.comp) ?? []) {
52
+ if (st.type !== "VariableDeclaration")
53
+ continue;
54
+ for (const d of st.declarations)
55
+ if (d.id.type === "Identifier" && d.id.name === name)
56
+ return d;
57
+ }
58
+ return null;
59
+ }
60
+ function destructuredProp(s, local) {
61
+ for (const st of setupAsts.get(s.comp) ?? []) {
62
+ if (st.type !== "VariableDeclaration")
63
+ continue;
64
+ for (const d of st.declarations) {
65
+ if (d.id.type !== "ObjectPattern" || !definePropsType(d.init))
66
+ continue;
67
+ for (const p of d.id.properties) {
68
+ if (p.type !== "ObjectProperty")
69
+ continue;
70
+ const target = p.value.type === "AssignmentPattern" ? p.value.left : p.value;
71
+ if (target.type === "Identifier" && target.name === local)
72
+ return p.key.name ?? p.key.value;
73
+ }
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+ function propOf(s, n) {
79
+ const own = (name) => (s.comp.props.fields.some((f) => f.js === name) ? name : null);
80
+ if (n?.type === "MemberExpression" && !n.computed && n.property.type === "Identifier") {
81
+ const o = n.object;
82
+ if (o.type === "Identifier" && (o.name === "$props" || o.name === "_ctx" || (o.name === s.propsIdent && !s.locals.has(o.name))))
83
+ return own(n.property.name);
84
+ if (s.propsIdent !== null && setupName(o) === s.propsIdent && o.type === "MemberExpression")
85
+ return own(n.property.name);
86
+ }
87
+ const name = n?.type === "Identifier" && s.locals.has(n.name) ? null : setupName(n);
88
+ return name === null ? null : destructuredProp(s, name);
89
+ }
90
+ function literalUnion(comp, t, seen = new Set()) {
91
+ switch (t?.type) {
92
+ case "TSParenthesizedType":
93
+ return literalUnion(comp, t.typeAnnotation, seen);
94
+ case "TSLiteralType":
95
+ if (t.literal.type === "StringLiteral")
96
+ return [t.literal.value];
97
+ if (t.literal.type === "TemplateLiteral" && !t.literal.expressions.length)
98
+ return [t.literal.quasis[0].value.cooked];
99
+ return null;
100
+ case "TSUnionType": {
101
+ const all = [];
102
+ for (const u of t.types) {
103
+ const lits = literalUnion(comp, u, seen);
104
+ if (lits === null)
105
+ return null;
106
+ for (const l of lits)
107
+ if (!all.includes(l))
108
+ all.push(l);
109
+ }
110
+ return all;
111
+ }
112
+ case "TSTypeReference": {
113
+ const name = t.typeName.type === "Identifier" ? t.typeName.name : undefined;
114
+ const alias = name === undefined || seen.has(name) ? undefined : comp.aliases.get(name);
115
+ return alias ? literalUnion(comp, alias, new Set(seen).add(name)) : null;
116
+ }
117
+ }
118
+ return null;
119
+ }
120
+ function propType(s, name) {
121
+ const ast = setupAsts.get(s.comp) ?? [];
122
+ let t = null;
123
+ for (const st of ast) {
124
+ const call = st.type === "ExpressionStatement" ? st.expression : st.type === "VariableDeclaration" ? st.declarations[0]?.init : null;
125
+ t = definePropsType(call) ?? t;
126
+ }
127
+ let members = t?.type === "TSTypeLiteral" ? t.members : [];
128
+ if (t?.type === "TSTypeReference") {
129
+ for (const st of ast) {
130
+ const d = st.type === "ExportNamedDeclaration" ? st.declaration : st;
131
+ if (d?.type === "TSInterfaceDeclaration" && d.id.name === t.typeName.name)
132
+ members = d.body.body;
133
+ if (d?.type === "TSTypeAliasDeclaration" && d.id.name === t.typeName.name && d.typeAnnotation.type === "TSTypeLiteral")
134
+ members = d.typeAnnotation.members;
135
+ }
136
+ }
137
+ return members.find((m) => m.type === "TSPropertySignature" && (m.key.name ?? m.key.value) === name)?.typeAnnotation?.typeAnnotation ?? null;
138
+ }
139
+ function keysOf(s, n, at) {
140
+ const name = propOf(s, n);
141
+ if (name === null)
142
+ fail(s.comp, "FV0418", `${OPEN}; this value is not one of those`, n);
143
+ const field = s.comp.props.fields.find((f) => f.js === name);
144
+ const keys = literalUnion(s.comp, propType(s, name));
145
+ if (keys === null)
146
+ fail(s.comp, "FV0418", `${OPEN}; \`${name}\` is not typed as a union of string literals, as \`"h1" | "h2"\``, n);
147
+ if (field.ty.k === "opt" && field.dflt === undefined) {
148
+ fail(s.comp, "FV0420", `\`${name}\` may be absent, where Vue renders \`<!---->\` or fails: make it required, or give it a default`, at);
149
+ }
150
+ return { on: n, keys };
151
+ }
152
+ function element(s, tag, n) {
153
+ const self = s.comp.name;
154
+ if (!isHTMLTag(tag) || RESERVED_TAGS.has(tag) || self === tag || self === camelize(tag) || self === camelize(tag).replace(/^./, (c) => c.toUpperCase())) {
155
+ fail(s.comp, "FV0419", `\`${tag}\` is not an HTML element \`<component :is>\` renders: name an HTML element other than \`<textarea>\`, \`<template>\` and \`<slot>\`, or bind an imported component`, n);
156
+ }
157
+ return { tag };
158
+ }
159
+ function component(s, name) {
160
+ const child = s.children.get(name);
161
+ return child === undefined ? null : { local: name, child };
162
+ }
163
+ function vueImports(body) {
164
+ const found = new Map();
165
+ for (const st of body) {
166
+ if (st.type !== "ImportDeclaration" || !st.source.value.endsWith(".vue"))
167
+ continue;
168
+ for (const sp of st.specifiers)
169
+ if (sp.type === "ImportDefaultSpecifier")
170
+ found.set(sp.local.name, basename(st.source.value, ".vue"));
171
+ }
172
+ return found;
173
+ }
174
+ function objectEntries(home, name, init) {
175
+ const obj = unwrap(init);
176
+ if (obj?.type !== "ObjectExpression")
177
+ return null;
178
+ const entries = new Map();
179
+ for (const p of obj.properties) {
180
+ if (p.type !== "ObjectProperty" || p.computed)
181
+ fail(home, "FV0418", `${OPEN}; \`${name}\` holds plain keys`, p);
182
+ entries.set(p.key.type === "Identifier" ? p.key.name : String(p.key.value), p.value);
183
+ }
184
+ return entries;
185
+ }
186
+ function tableOf(s, n) {
187
+ const name = setupName(n);
188
+ if (name === null || s.locals.has(name))
189
+ return null;
190
+ const local = declared(s, name);
191
+ if (local) {
192
+ const entries = objectEntries(s.comp, name, local.init);
193
+ return entries && { entries, home: s.comp, name, imports: new Map() };
194
+ }
195
+ for (const st of setupAsts.get(s.comp) ?? []) {
196
+ if (st.type !== "ImportDeclaration" || st.importKind === "type")
197
+ continue;
198
+ const sp = st.specifiers.find((x) => x.type === "ImportSpecifier" && x.local.name === name && x.importKind !== "type");
199
+ if (!sp)
200
+ continue;
201
+ const file = resolveImport(s.comp.file, st.source.value);
202
+ const d = file === null ? undefined : ctx.constDecls.get(file)?.get(sp.imported.name ?? sp.imported.value);
203
+ if (!d?.exported || d.node.type !== "VariableDeclarator")
204
+ return null;
205
+ const entries = objectEntries(d.home, name, d.node.init);
206
+ if (!entries)
207
+ return null;
208
+ const body = parseJs(d.home.source ?? "", { sourceType: "module", plugins: ["typescript"] }).program.body;
209
+ return { entries, home: d.home, name, imports: vueImports(body) };
210
+ }
211
+ return null;
212
+ }
213
+ function entry(s, table, key, at) {
214
+ const value = unwrap(table.entries.get(key));
215
+ if (!value)
216
+ fail(s.comp, "FV0421", `\`${key}\` is not a key of \`${table.name}\`, where Vue finds no component and renders \`<!---->\``, at);
217
+ if (value.type === "StringLiteral")
218
+ return element(s, value.value, value);
219
+ if (value.type === "Identifier") {
220
+ if (table.home === s.comp) {
221
+ const found = component(s, value.name);
222
+ if (found)
223
+ return found;
224
+ }
225
+ else {
226
+ const child = table.imports.get(value.name);
227
+ if (child !== undefined && s.components.has(child)) {
228
+ const local = `${child}.vue`;
229
+ s.children.set(local, child);
230
+ return { local, child };
231
+ }
232
+ }
233
+ }
234
+ return fail(table.home, "FV0418", `${OPEN}; \`${table.name}.${key}\` is not a component imported from a \`.vue\` file among the components compiled, or an element name`, value);
235
+ }
236
+ function pickOf(s, raw, seen = new Set()) {
237
+ const n = unwrap(raw);
238
+ if (n?.type === "StringLiteral")
239
+ return { k: "one", choice: element(s, n.value, n) };
240
+ if (n?.type === "TemplateLiteral" && !n.expressions.length)
241
+ return { k: "one", choice: element(s, n.quasis[0].value.cooked, n) };
242
+ if (n?.type === "ConditionalExpression")
243
+ return { k: "if", test: n.test, yes: pickOf(s, n.consequent, seen), no: pickOf(s, n.alternate, seen) };
244
+ const prop = propOf(s, n);
245
+ if (prop !== null) {
246
+ const { on, keys } = keysOf(s, n, n);
247
+ return { k: "match", on, arms: keys.map((key) => [key, { k: "one", choice: element(s, key, n) }]) };
248
+ }
249
+ if (n?.type === "MemberExpression" && !(n.object.type === "Identifier" && ["$setup", "_ctx", "$props"].includes(n.object.name))) {
250
+ const table = tableOf(s, n.object);
251
+ if (table) {
252
+ const key = n.computed ? unwrap(n.property) : { type: "StringLiteral", value: n.property.name };
253
+ if (key.type === "StringLiteral")
254
+ return { k: "one", choice: entry(s, table, key.value, n) };
255
+ const { on, keys } = keysOf(s, key, n);
256
+ return { k: "match", on, arms: keys.map((k) => [k, { k: "one", choice: entry(s, table, k, n) }]) };
257
+ }
258
+ }
259
+ const name = n?.type === "Identifier" && s.locals.has(n.name) ? null : setupName(n);
260
+ if (name !== null) {
261
+ const found = component(s, name);
262
+ if (found)
263
+ return { k: "one", choice: found };
264
+ const d = seen.has(name) ? null : declared(s, name);
265
+ const source = d?.init ? setupSource(d.init) : null;
266
+ if (source)
267
+ return pickOf(s, source, new Set(seen).add(name));
268
+ if (tableOf(s, n))
269
+ fail(s.comp, "FV0418", `${OPEN}; \`${name}\` is an object: read one of its components by key, as \`${name}[key]\``, raw);
270
+ }
271
+ return fail(s.comp, "FV0418", `${OPEN}; this value is not one of those`, raw);
272
+ }
273
+ function choicesOf(p) {
274
+ if (p.k === "one")
275
+ return [p.choice];
276
+ if (p.k === "if")
277
+ return [...choicesOf(p.yes), ...choicesOf(p.no)];
278
+ return p.arms.flatMap(([, a]) => choicesOf(a));
279
+ }
280
+ function ownScopeId(comp) {
281
+ const options = claim((p) => p.templateOptions?.(comp));
282
+ return options?.scoped ? options.id : null;
283
+ }
284
+ function defaultSlot(slots) {
285
+ if (!slots || slots.type !== "ObjectExpression")
286
+ return null;
287
+ return slots.properties.find((p) => p.type === "ObjectProperty" && (p.key.name ?? p.key.value) === "default")?.value ?? null;
288
+ }
289
+ function renderElement(s, e, tag, d) {
290
+ if (s.fill)
291
+ e.stmt("filled = true;");
292
+ const parts = !d.props || d.props.type === "NullLiteral" ? [] : mergedParts(d.props);
293
+ const inherited = parts.some(isAttrs) ? s.attrs : null;
294
+ const slotted = d.sid?.type === "Identifier" && d.sid.name === "_scopeId" ? s.sid : null;
295
+ const own = ownScopeId(s.comp);
296
+ const name = () => (typeof tag === "string" ? e.lit(tag) : e.stmt(`out.push_str(${tag.code});`));
297
+ e.lit("<");
298
+ name();
299
+ if (parts.length)
300
+ renderAttrs({ ...s, attrs: null }, e, d.props);
301
+ if (inherited === null && slotted === null) {
302
+ if (own !== null)
303
+ e.lit(` ${own}`);
304
+ }
305
+ else {
306
+ e.stmt(`out.push_str(&fv::scope_attrs(${inherited ?? '""'}, ${own === null ? '""' : rustStr(own)}, ${slotted ?? '""'}));`);
307
+ }
308
+ e.lit(">");
309
+ if (typeof tag === "string" && isVoidTag(tag))
310
+ return;
311
+ const content = defaultSlot(d.slots);
312
+ if (content)
313
+ statements({ ...s, vnode: true, fill: false, sid: slotted }, e, slotBody(s, content));
314
+ e.lit("</");
315
+ name();
316
+ e.lit(">");
317
+ }
318
+ function renderChoice(s, e, choice, d) {
319
+ if ("tag" in choice) {
320
+ renderElement(s, e, choice.tag, d);
321
+ return;
322
+ }
323
+ const at = { loc: d.at.loc, __fv: d.at.__fv };
324
+ const target = { type: "MemberExpression", computed: true, object: { type: "Identifier", name: "$setup", ...at }, property: { type: "StringLiteral", value: choice.local, ...at }, ...at };
325
+ const takes = s.components.get(choice.child)?.slotNames;
326
+ const slots = d.slots?.type === "ObjectExpression" && takes
327
+ ? { ...d.slots, properties: d.slots.properties.filter((p) => ["_", ...takes].includes(p.key?.name ?? p.key?.value)) }
328
+ : (d.slots ?? { type: "NullLiteral", ...at });
329
+ const args = [target, d.props ?? { type: "NullLiteral", ...at }, slots, d.parent, ...(d.sid ? [d.sid] : [])];
330
+ push(s, e, { type: "CallExpression", callee: { type: "Identifier", name: "_ssrRenderComponent", ...at }, arguments: args, ...at });
331
+ }
332
+ function same(a, b) {
333
+ return a.k === "one" && b.k === "one" && JSON.stringify(a.choice) === JSON.stringify(b.choice);
334
+ }
335
+ function renderPick(s, e, p, d) {
336
+ if (p.k === "one") {
337
+ renderChoice(s, e, p.choice, d);
338
+ return;
339
+ }
340
+ if (p.k === "if") {
341
+ const t = cond(s, p.test);
342
+ if (t === "true" || t === "false") {
343
+ renderPick(s, e, t === "true" ? p.yes : p.no, d);
344
+ return;
345
+ }
346
+ e.open(`if ${condition(t)}`);
347
+ renderPick(s, e, p.yes, d);
348
+ e.close(" else {");
349
+ renderPick(s, e, p.no, d);
350
+ e.close();
351
+ return;
352
+ }
353
+ const v = expr(s, p.on);
354
+ const k = known(v);
355
+ if (v.ty.k === "str" && k !== undefined) {
356
+ const value = JSON.parse(v.code);
357
+ const arm = p.arms.find(([key]) => key === value) ?? p.arms.at(-1);
358
+ renderPick(s, e, arm[1], d);
359
+ return;
360
+ }
361
+ const groups = [];
362
+ for (const [key, arm] of p.arms) {
363
+ const g = groups.find(([, q]) => same(q, arm));
364
+ if (g)
365
+ g[0].push(key);
366
+ else
367
+ groups.push([[key], arm]);
368
+ }
369
+ if (groups.length === 1) {
370
+ renderPick(s, e, groups[0][1], d);
371
+ return;
372
+ }
373
+ const tags = groups.map(([, arm]) => (arm.k === "one" && "tag" in arm.choice ? arm.choice.tag : null));
374
+ if (tags.every((t) => t !== null && !isVoidTag(t))) {
375
+ const name = `fv_tag${++ctx.narrowCount}`;
376
+ const arms = groups.map(([keys], i) => `${i === groups.length - 1 ? "_" : keys.map(rustStr).join(" | ")} => ${rustStr(tags[i])}`);
377
+ e.stmt(`let ${name} = match ${v.code} { ${arms.join(", ")} };`);
378
+ e.expect(Math.max(...tags.map((t) => t.length)) * 2);
379
+ renderElement(s, e, { code: name }, d);
380
+ return;
381
+ }
382
+ e.open(`match ${v.code}`);
383
+ groups.forEach(([keys, arm], i) => {
384
+ e.open(`${i === groups.length - 1 ? "_" : keys.map(rustStr).join(" | ")} =>`);
385
+ renderPick(s, e, arm, d);
386
+ e.close();
387
+ });
388
+ e.close();
389
+ }
390
+ /** Render `_ssrRenderVNode` of a `<component :is>`, or return false for a statement that is not one. */
391
+ export function dynamicComponent(s, e, st) {
392
+ const d = dynamicOf(st);
393
+ if (d === null)
394
+ return false;
395
+ renderPick(s, e, pickOf(s, d.is), d);
396
+ return true;
397
+ }
398
+ function refuseContentDirectives(comp) {
399
+ const visit = (n) => {
400
+ if (n.type === 1 && n.tag === "component") {
401
+ const dir = n.props.find((p) => p.type === 7 && (p.name === "html" || p.name === "text"));
402
+ if (dir) {
403
+ const at = { type: "VueTemplate", loc: { start: { line: dir.loc.start.line, column: dir.loc.start.column - 1 } }, __fv: "source" };
404
+ fail(comp, "FV0422", `\`v-${dir.name}\` on \`<component :is>\`: Vue's server renders the element empty, which hydration then fills; put the content inside it`, at);
405
+ }
406
+ }
407
+ for (const c of n.children ?? [])
408
+ visit(c);
409
+ };
410
+ if (comp.templateAst)
411
+ visit(comp.templateAst);
412
+ }
413
+ function outletsOf(p, byName) {
414
+ const outlets = [];
415
+ const childOf = (target) => {
416
+ const name = setupName(target);
417
+ const file = name === null ? undefined : p.scope.children.get(name);
418
+ return file === undefined ? undefined : byName.get(file);
419
+ };
420
+ const slotsWith = (slots, mode, walkAs) => {
421
+ if (slots?.type !== "ObjectExpression")
422
+ return;
423
+ for (const prop of slots.properties) {
424
+ const name = prop.key?.name ?? prop.key?.value;
425
+ const as = name === "_" ? null : mode(name);
426
+ if (as !== null)
427
+ walkAs(prop.value, as);
428
+ }
429
+ };
430
+ const walk = (n, vnode) => {
431
+ if (!n || typeof n !== "object")
432
+ return;
433
+ if (Array.isArray(n)) {
434
+ for (const x of n)
435
+ walk(x, vnode);
436
+ return;
437
+ }
438
+ const choices = p.picks.get(n);
439
+ if (choices) {
440
+ const d = dynamicOf(n);
441
+ walk(d.props, vnode);
442
+ for (const c of choices) {
443
+ const child = "tag" in c ? undefined : byName.get(c.child);
444
+ slotsWith(d.slots, (name) => ("tag" in c ? (name === "default" ? true : null) : child !== undefined && slotAsVnodes(child, name)), walk);
445
+ }
446
+ return;
447
+ }
448
+ if (n.type === "CallExpression" && n.callee.type === "Identifier" && n.callee.name === "_ssrRenderComponent") {
449
+ const child = childOf(n.arguments[0]);
450
+ walk(n.arguments[1], vnode);
451
+ slotsWith(n.arguments[2], (name) => (child ? slotAsVnodes(child, name) : false), walk);
452
+ return;
453
+ }
454
+ if (n.type === "CallExpression" && n.callee.type === "Identifier" && n.callee.name === "_ssrRenderSlot" && n.arguments[1]?.type === "StringLiteral") {
455
+ const fallback = n.arguments[3];
456
+ outlets.push({ name: n.arguments[1].value, vnode, fallback: !!fallback && fallback.type !== "NullLiteral", node: n });
457
+ }
458
+ for (const [key, v] of Object.entries(n))
459
+ if (key !== "loc" && key !== "start" && key !== "end" && key !== "__fv")
460
+ walk(v, vnode);
461
+ };
462
+ walk(p.program, false);
463
+ return outlets;
464
+ }
465
+ function expanded(p) {
466
+ const text = (n) => {
467
+ const inner = [];
468
+ const find = (x) => {
469
+ if (!x || typeof x !== "object")
470
+ return;
471
+ if (Array.isArray(x)) {
472
+ x.forEach(find);
473
+ return;
474
+ }
475
+ if (x !== n && p.picks.has(x)) {
476
+ inner.push(x);
477
+ return;
478
+ }
479
+ for (const [key, v] of Object.entries(x))
480
+ if (key !== "loc" && key !== "__fv")
481
+ find(v);
482
+ };
483
+ find(n);
484
+ if (p.picks.has(n)) {
485
+ const d = dynamicOf(n);
486
+ const arg = (x) => (x ? text(x) : "null");
487
+ return p.picks
488
+ .get(n)
489
+ .map((c) => "tag" in c
490
+ ? `_fvElement(${arg(d.props)}, ${arg(d.slots)});`
491
+ : `_push(_ssrRenderComponent($setup[${JSON.stringify(c.local)}], ${arg(d.props)}, ${arg(d.slots)}, ${arg(d.parent)}${d.sid ? `, ${text(d.sid)}` : ""}));`)
492
+ .join("\n");
493
+ }
494
+ inner.sort((a, b) => a.start - b.start);
495
+ let out = "";
496
+ let at = n.start;
497
+ for (const x of inner) {
498
+ out += p.code.slice(at, x.start) + text(x);
499
+ at = x.end;
500
+ }
501
+ return out + p.code.slice(at, n.end);
502
+ };
503
+ return text(p.program);
504
+ }
505
+ /** Resolve every `<component :is>` of the components before any is generated: the slots whose
506
+ * content renders as virtual nodes, and the compiled templates as the analyses read them, with
507
+ * each `<component :is>` written out as its choices. */
508
+ export function prepareDynamic(read) {
509
+ for (const r of read)
510
+ setupAsts.set(r.comp, r.ast);
511
+ const prepared = read.map((r) => {
512
+ const program = parseJs(r.ssr, { sourceType: "module" }).program;
513
+ tagAst(program, "template");
514
+ const picks = new Map();
515
+ const visit = (n) => {
516
+ if (!n || typeof n !== "object")
517
+ return;
518
+ if (Array.isArray(n)) {
519
+ n.forEach(visit);
520
+ return;
521
+ }
522
+ const d = dynamicOf(n);
523
+ if (d) {
524
+ try {
525
+ const choices = choicesOf(pickOf(r.scope, d.is));
526
+ for (const c of choices)
527
+ if (!("tag" in c))
528
+ r.comp.imports.add(c.child);
529
+ picks.set(n, choices);
530
+ }
531
+ catch (e) {
532
+ if (!(e instanceof GenError))
533
+ throw e;
534
+ }
535
+ }
536
+ for (const [key, v] of Object.entries(n))
537
+ if (key !== "loc" && key !== "__fv")
538
+ visit(v);
539
+ };
540
+ visit(program);
541
+ if (picks.size)
542
+ refuseContentDirectives(r.comp);
543
+ return { comp: r.comp, scope: r.scope, program, code: r.ssr, picks };
544
+ });
545
+ const byName = new Map(read.map((r) => [r.comp.name, r.comp]));
546
+ const outlets = new Map();
547
+ for (let changed = true; changed;) {
548
+ changed = false;
549
+ for (const p of prepared) {
550
+ if (!p.picks.size && !p.code.includes("_ssrRenderSlot"))
551
+ continue;
552
+ const found = outletsOf(p, byName);
553
+ outlets.set(p.comp, found);
554
+ const set = vnodeSlots.get(p.comp) ?? new Set();
555
+ for (const o of found) {
556
+ if (o.vnode && !set.has(o.name)) {
557
+ set.add(o.name);
558
+ changed = true;
559
+ }
560
+ }
561
+ vnodeSlots.set(p.comp, set);
562
+ }
563
+ }
564
+ for (const [comp, found] of outlets) {
565
+ for (const o of found) {
566
+ const outlet = `\`<slot${o.name === "default" ? "" : ` name="${o.name}"`}>\``;
567
+ if (o.vnode && o.fallback) {
568
+ fail(comp, "FV0919", `${outlet} has fallback content and is rendered inside an element \`<component :is>\` chooses, where Vue tests its content by other rules: move the fallback into the parent`, o.node);
569
+ }
570
+ if (!o.vnode && found.some((q) => q.vnode && q.name === o.name)) {
571
+ fail(comp, "FV0920", `${outlet} is rendered both inside an element \`<component :is>\` chooses and outside one, where Vue renders the content it is given differently`, o.node);
572
+ }
573
+ }
574
+ }
575
+ return prepared.map(expanded);
576
+ }
@@ -0,0 +1,18 @@
1
+ import { type Diagnostic } from "./diagnostics.ts";
2
+ /** A refusal in the file an editor holds, with its range as offsets into the text it was given. The
3
+ * end is exclusive, and `null` where the compiler does not know it. */
4
+ export interface Located extends Diagnostic {
5
+ start: number;
6
+ end: number | null;
7
+ }
8
+ /** Where an editor's checks find the project: the configuration file's path from the project root. */
9
+ export interface EditorOptions {
10
+ config?: string;
11
+ }
12
+ /** The directory holding `config` that is `dir` or the nearest above it. */
13
+ export declare function projectRoot(dir: string, config?: string): string | null;
14
+ /** The refusals in a component of a ferrovue project, compiling the project with `text` in place of
15
+ * the file. A project is the nearest directory above the file holding the configuration; a file in
16
+ * none has no refusals. The compiler stops at the first refusal, so a refusal in another file hides
17
+ * those in this one. */
18
+ export declare function refusalsIn(file: string, text: string, options?: EditorOptions): Located[];
package/dist/editor.js ADDED
@@ -0,0 +1,59 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { CONFIG_FILE, generate, loadConfig } from "./compiler.js";
4
+ import { diagnose, isRefusal } from "./diagnostics.js";
5
+ const FRESH_MS = 1000;
6
+ const recent = new Map();
7
+ /** The directory holding `config` that is `dir` or the nearest above it. */
8
+ export function projectRoot(dir, config = CONFIG_FILE) {
9
+ for (let at = resolve(dir);; at = dirname(at)) {
10
+ if (existsSync(join(at, config)))
11
+ return at;
12
+ if (dirname(at) === at)
13
+ return null;
14
+ }
15
+ }
16
+ function offsetOf(text, line, column) {
17
+ let at = 0;
18
+ for (let l = 1; l < line; l++) {
19
+ const next = text.indexOf("\n", at);
20
+ if (next < 0)
21
+ return text.length;
22
+ at = next + 1;
23
+ }
24
+ return Math.min(at + column - 1, text.length);
25
+ }
26
+ function check(file, text, options) {
27
+ const root = projectRoot(dirname(file), options.config);
28
+ if (!root)
29
+ return [];
30
+ try {
31
+ generate(root, loadConfig(root, options.config), new Map([[file, text]]));
32
+ return [];
33
+ }
34
+ catch (e) {
35
+ if (!isRefusal(e)) {
36
+ console.error(e);
37
+ return [];
38
+ }
39
+ const d = diagnose(e);
40
+ if (d.file === null || resolve(root, d.file) !== file)
41
+ return [];
42
+ const start = d.line === null ? 0 : offsetOf(text, d.line, d.column ?? 1);
43
+ const end = d.endLine === null ? null : offsetOf(text, d.endLine, d.endColumn ?? 1);
44
+ return [{ ...d, start, end }];
45
+ }
46
+ }
47
+ /** The refusals in a component of a ferrovue project, compiling the project with `text` in place of
48
+ * the file. A project is the nearest directory above the file holding the configuration; a file in
49
+ * none has no refusals. The compiler stops at the first refusal, so a refusal in another file hides
50
+ * those in this one. */
51
+ export function refusalsIn(file, text, options = {}) {
52
+ const path = resolve(file);
53
+ const hit = recent.get(path);
54
+ if (hit && hit.text === text && Date.now() - hit.at < FRESH_MS)
55
+ return hit.found;
56
+ const found = check(path, text, options);
57
+ recent.set(path, { text, at: Date.now(), found });
58
+ return found;
59
+ }