ferrovue 0.4.0 → 0.6.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.
- package/README.md +378 -63
- package/dist/attrs.d.ts +3 -2
- package/dist/attrs.js +31 -26
- package/dist/calls.js +17 -15
- package/dist/children.d.ts +11 -4
- package/dist/children.js +110 -44
- package/dist/classes.d.ts +4 -0
- package/dist/classes.js +33 -11
- package/dist/cli.js +31 -5
- package/dist/client.d.ts +44 -6
- package/dist/client.js +182 -18
- package/dist/compiler.d.ts +1 -1
- package/dist/compiler.js +42 -10
- package/dist/component.d.ts +3 -1
- package/dist/component.js +114 -20
- package/dist/conformance.d.ts +64 -0
- package/dist/conformance.js +276 -0
- package/dist/constants.d.ts +42 -0
- package/dist/constants.js +292 -0
- package/dist/context.d.ts +40 -7
- package/dist/context.js +14 -5
- package/dist/diagnostics.d.ts +23 -0
- package/dist/diagnostics.js +30 -0
- package/dist/dynamic.d.ts +18 -0
- package/dist/dynamic.js +576 -0
- package/dist/errors.d.ts +1034 -0
- package/dist/errors.js +425 -0
- package/dist/expr.d.ts +1 -0
- package/dist/expr.js +120 -49
- package/dist/file-routes.d.ts +33 -0
- package/dist/file-routes.js +224 -0
- package/dist/fixture.d.ts +19 -10
- package/dist/fixture.js +23 -12
- package/dist/hydration.d.ts +15 -0
- package/dist/hydration.js +80 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/link-router.d.ts +24 -0
- package/dist/link-router.js +37 -0
- package/dist/lists.d.ts +4 -0
- package/dist/lists.js +32 -23
- package/dist/loops.js +18 -14
- package/dist/model.d.ts +43 -1
- package/dist/model.js +167 -7
- package/dist/narrowing.d.ts +8 -0
- package/dist/narrowing.js +40 -9
- package/dist/numbers.js +3 -2
- package/dist/page-routes.d.ts +9 -0
- package/dist/page-routes.js +10 -0
- package/dist/plugin.d.ts +13 -1
- package/dist/plugin.js +3 -0
- package/dist/plugins/client-only.d.ts +8 -0
- package/dist/plugins/client-only.js +110 -0
- package/dist/plugins/head.d.ts +15 -0
- package/dist/plugins/head.js +385 -0
- package/dist/plugins/i18n.js +14 -14
- package/dist/plugins/index.js +5 -1
- package/dist/plugins/provide.d.ts +44 -0
- package/dist/plugins/provide.js +528 -0
- package/dist/plugins/router-link.js +39 -32
- package/dist/plugins/router.d.ts +4 -1
- package/dist/plugins/router.js +65 -19
- package/dist/plugins/scoped.js +1 -1
- package/dist/plugins/shared-types.js +1 -1
- package/dist/plugins/stores.js +45 -33
- package/dist/plugins/teleport.js +3 -3
- package/dist/plugins/twins.d.ts +10 -0
- package/dist/plugins/twins.js +164 -0
- package/dist/routes.d.ts +13 -0
- package/dist/routes.js +12 -0
- package/dist/rust.js +64 -32
- package/dist/script.d.ts +3 -0
- package/dist/script.js +138 -20
- package/dist/settle.d.ts +8 -0
- package/dist/settle.js +39 -0
- package/dist/slots.js +36 -22
- package/dist/ssr.d.ts +3 -0
- package/dist/ssr.js +29 -0
- package/dist/strings.d.ts +3 -0
- package/dist/strings.js +27 -13
- package/dist/styles.js +13 -13
- package/dist/template.js +102 -21
- package/dist/testing.d.ts +3 -3
- package/dist/testing.js +3 -29
- package/dist/types.d.ts +5 -0
- package/dist/typescript.d.ts +2 -1
- package/dist/typescript.js +52 -21
- package/dist/vhtml.d.ts +4 -0
- package/dist/vhtml.js +63 -0
- package/dist/vite.d.ts +8 -2
- package/dist/vite.js +75 -14
- package/package.json +23 -5
package/dist/dynamic.js
ADDED
|
@@ -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
|
+
}
|