@collie-lang/compiler 1.2.0 → 1.3.1
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/dist/index.cjs +780 -387
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +780 -387
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,337 +1,36 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
function
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
const functionLines = [
|
|
9
|
-
isTsx ? "export function render(props: any) {" : "export function render(props) {"
|
|
10
|
-
];
|
|
11
|
-
if (propsDestructure) {
|
|
12
|
-
functionLines.push(` ${propsDestructure}`);
|
|
13
|
-
}
|
|
14
|
-
functionLines.push(` return ${jsx};`, `}`);
|
|
15
|
-
parts.push(functionLines.join("\n"));
|
|
16
|
-
return parts.join("\n\n");
|
|
17
|
-
}
|
|
18
|
-
function buildModuleParts(root, options) {
|
|
19
|
-
const { jsxRuntime, flavor } = options;
|
|
20
|
-
const isTsx = flavor === "tsx";
|
|
21
|
-
const aliasEnv = buildClassAliasEnvironment(root.classAliases);
|
|
22
|
-
const jsx = renderRootChildren(root.children, aliasEnv);
|
|
23
|
-
const propsDestructure = emitPropsDestructure(root.props);
|
|
24
|
-
const prelude = [];
|
|
25
|
-
if (root.clientComponent) {
|
|
26
|
-
prelude.push(`"use client";`);
|
|
27
|
-
}
|
|
28
|
-
if (jsxRuntime === "classic" && templateUsesJsx(root)) {
|
|
29
|
-
prelude.push(`import React from "react";`);
|
|
30
|
-
}
|
|
31
|
-
const propsType = emitPropsType(root.props, flavor);
|
|
32
|
-
return { prelude, propsType, propsDestructure, jsx, isTsx };
|
|
33
|
-
}
|
|
34
|
-
function buildClassAliasEnvironment(decl) {
|
|
35
|
-
const env = /* @__PURE__ */ new Map();
|
|
36
|
-
if (!decl) {
|
|
37
|
-
return env;
|
|
38
|
-
}
|
|
39
|
-
for (const alias of decl.aliases) {
|
|
40
|
-
env.set(alias.name, alias.classes);
|
|
41
|
-
}
|
|
42
|
-
return env;
|
|
43
|
-
}
|
|
44
|
-
function renderRootChildren(children, aliasEnv) {
|
|
45
|
-
return emitNodesExpression(children, aliasEnv, /* @__PURE__ */ new Set());
|
|
46
|
-
}
|
|
47
|
-
function templateUsesJsx(root) {
|
|
48
|
-
if (root.children.length === 0) {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
if (root.children.length > 1) {
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
return nodeUsesJsx(root.children[0]);
|
|
55
|
-
}
|
|
56
|
-
function nodeUsesJsx(node) {
|
|
57
|
-
if (node.type === "Element" || node.type === "Text" || node.type === "Component") {
|
|
58
|
-
return true;
|
|
59
|
-
}
|
|
60
|
-
if (node.type === "Expression" || node.type === "JSXPassthrough") {
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
if (node.type === "Conditional") {
|
|
64
|
-
return node.branches.some((branch) => branchUsesJsx(branch));
|
|
65
|
-
}
|
|
66
|
-
if (node.type === "For") {
|
|
67
|
-
return node.body.some((child) => nodeUsesJsx(child));
|
|
68
|
-
}
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
function branchUsesJsx(branch) {
|
|
72
|
-
if (!branch.body.length) {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
return branch.body.some((child) => nodeUsesJsx(child));
|
|
76
|
-
}
|
|
77
|
-
function emitNodeInJsx(node, aliasEnv, locals) {
|
|
78
|
-
if (node.type === "Text") {
|
|
79
|
-
return emitText(node, locals);
|
|
80
|
-
}
|
|
81
|
-
if (node.type === "Expression") {
|
|
82
|
-
return `{${emitExpressionValue(node.value, locals)}}`;
|
|
83
|
-
}
|
|
84
|
-
if (node.type === "JSXPassthrough") {
|
|
85
|
-
return `{${emitJsxExpression(node.expression, locals)}}`;
|
|
86
|
-
}
|
|
87
|
-
if (node.type === "Conditional") {
|
|
88
|
-
return `{${emitConditionalExpression(node, aliasEnv, locals)}}`;
|
|
89
|
-
}
|
|
90
|
-
if (node.type === "For") {
|
|
91
|
-
return `{${emitForExpression(node, aliasEnv, locals)}}`;
|
|
92
|
-
}
|
|
93
|
-
if (node.type === "Component") {
|
|
94
|
-
return wrapWithGuard(emitComponent(node, aliasEnv, locals), node.guard, "jsx", locals);
|
|
95
|
-
}
|
|
96
|
-
return wrapWithGuard(emitElement(node, aliasEnv, locals), node.guard, "jsx", locals);
|
|
97
|
-
}
|
|
98
|
-
function emitElement(node, aliasEnv, locals) {
|
|
99
|
-
const expanded = expandClasses(node.classes, aliasEnv);
|
|
100
|
-
const classAttr = expanded.length ? ` className="${expanded.join(" ")}"` : "";
|
|
101
|
-
const attrs = emitAttributes(node.attributes, aliasEnv, locals);
|
|
102
|
-
const allAttrs = classAttr + attrs;
|
|
103
|
-
const children = emitChildrenWithSpacing(node.children, aliasEnv, locals);
|
|
104
|
-
if (children.length > 0) {
|
|
105
|
-
return `<${node.name}${allAttrs}>${children}</${node.name}>`;
|
|
106
|
-
} else {
|
|
107
|
-
return `<${node.name}${allAttrs} />`;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
function emitComponent(node, aliasEnv, locals) {
|
|
111
|
-
const attrs = emitAttributes(node.attributes, aliasEnv, locals);
|
|
112
|
-
const slotProps = emitSlotProps(node, aliasEnv, locals);
|
|
113
|
-
const allAttrs = `${attrs}${slotProps}`;
|
|
114
|
-
const children = emitChildrenWithSpacing(node.children, aliasEnv, locals);
|
|
115
|
-
if (children.length > 0) {
|
|
116
|
-
return `<${node.name}${allAttrs}>${children}</${node.name}>`;
|
|
117
|
-
} else {
|
|
118
|
-
return `<${node.name}${allAttrs} />`;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
function emitChildrenWithSpacing(children, aliasEnv, locals) {
|
|
122
|
-
if (children.length === 0) {
|
|
123
|
-
return "";
|
|
124
|
-
}
|
|
125
|
-
const parts = [];
|
|
126
|
-
for (let i = 0; i < children.length; i++) {
|
|
127
|
-
const child = children[i];
|
|
128
|
-
const emitted = emitNodeInJsx(child, aliasEnv, locals);
|
|
129
|
-
parts.push(emitted);
|
|
130
|
-
if (i < children.length - 1) {
|
|
131
|
-
const nextChild = children[i + 1];
|
|
132
|
-
const needsSpace = child.type === "Text" && (nextChild.type === "Element" || nextChild.type === "Component" || nextChild.type === "Expression" || nextChild.type === "JSXPassthrough");
|
|
133
|
-
if (needsSpace) {
|
|
134
|
-
parts.push(" ");
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return parts.join("");
|
|
139
|
-
}
|
|
140
|
-
function emitAttributes(attributes, aliasEnv, locals) {
|
|
141
|
-
if (attributes.length === 0) {
|
|
142
|
-
return "";
|
|
143
|
-
}
|
|
144
|
-
return attributes.map((attr) => {
|
|
145
|
-
if (attr.value === null) {
|
|
146
|
-
return ` ${attr.name}`;
|
|
147
|
-
}
|
|
148
|
-
return ` ${attr.name}=${emitAttributeValue(attr.value, locals)}`;
|
|
149
|
-
}).join("");
|
|
150
|
-
}
|
|
151
|
-
function emitSlotProps(node, aliasEnv, locals) {
|
|
152
|
-
if (!node.slots || node.slots.length === 0) {
|
|
153
|
-
return "";
|
|
154
|
-
}
|
|
155
|
-
return node.slots.map((slot) => {
|
|
156
|
-
const expr = emitNodesExpression(slot.children, aliasEnv, locals);
|
|
157
|
-
return ` ${slot.name}={${expr}}`;
|
|
158
|
-
}).join("");
|
|
159
|
-
}
|
|
160
|
-
function wrapWithGuard(rendered, guard, context, locals) {
|
|
161
|
-
if (!guard) {
|
|
162
|
-
return rendered;
|
|
163
|
-
}
|
|
164
|
-
const condition = emitExpressionValue(guard, locals);
|
|
165
|
-
const expression = `(${condition}) && ${rendered}`;
|
|
166
|
-
return context === "jsx" ? `{${expression}}` : expression;
|
|
167
|
-
}
|
|
168
|
-
function emitForExpression(node, aliasEnv, locals) {
|
|
169
|
-
const arrayExpr = emitExpressionValue(node.arrayExpr, locals);
|
|
170
|
-
const nextLocals = new Set(locals);
|
|
171
|
-
nextLocals.add(node.itemName);
|
|
172
|
-
const body = emitNodesExpression(node.body, aliasEnv, nextLocals);
|
|
173
|
-
return `(${arrayExpr} ?? []).map((${node.itemName}) => ${body})`;
|
|
174
|
-
}
|
|
175
|
-
function expandClasses(classes, aliasEnv) {
|
|
176
|
-
const result = [];
|
|
177
|
-
for (const cls of classes) {
|
|
178
|
-
const match = cls.match(/^\$([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
179
|
-
if (!match) {
|
|
180
|
-
result.push(cls);
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
const aliasClasses = aliasEnv.get(match[1]);
|
|
184
|
-
if (!aliasClasses) {
|
|
185
|
-
continue;
|
|
1
|
+
// src/rewrite.ts
|
|
2
|
+
function createTemplateEnv(propsDecls) {
|
|
3
|
+
const propAliases = /* @__PURE__ */ new Map();
|
|
4
|
+
if (propsDecls) {
|
|
5
|
+
for (const decl of propsDecls) {
|
|
6
|
+
propAliases.set(decl.name, decl.kind);
|
|
186
7
|
}
|
|
187
|
-
result.push(...aliasClasses);
|
|
188
8
|
}
|
|
189
|
-
return
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
9
|
+
return {
|
|
10
|
+
propAliases,
|
|
11
|
+
localsStack: []
|
|
12
|
+
};
|
|
193
13
|
}
|
|
194
|
-
function
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
return rewriteJsxExpression(expression, locals);
|
|
198
|
-
}
|
|
199
|
-
return rewriteExpression(expression, locals);
|
|
14
|
+
function pushLocals(env, names) {
|
|
15
|
+
const locals = new Set(names);
|
|
16
|
+
env.localsStack.push(locals);
|
|
200
17
|
}
|
|
201
|
-
function
|
|
202
|
-
|
|
203
|
-
if (trimmed.startsWith('"') || trimmed.startsWith("'")) {
|
|
204
|
-
return value;
|
|
205
|
-
}
|
|
206
|
-
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
207
|
-
const inner = trimmed.slice(1, -1);
|
|
208
|
-
const rewritten = rewriteExpression(inner, locals);
|
|
209
|
-
return `{${rewritten}}`;
|
|
210
|
-
}
|
|
211
|
-
return rewriteExpression(trimmed, locals);
|
|
18
|
+
function popLocals(env) {
|
|
19
|
+
env.localsStack.pop();
|
|
212
20
|
}
|
|
213
|
-
function
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return node.parts.map((part) => {
|
|
218
|
-
if (part.type === "text") {
|
|
219
|
-
return escapeText(part.value);
|
|
21
|
+
function isLocal(env, name) {
|
|
22
|
+
for (let i = env.localsStack.length - 1; i >= 0; i--) {
|
|
23
|
+
if (env.localsStack[i].has(name)) {
|
|
24
|
+
return true;
|
|
220
25
|
}
|
|
221
|
-
return `{${emitExpressionValue(part.value, locals)}}`;
|
|
222
|
-
}).join("");
|
|
223
|
-
}
|
|
224
|
-
function emitConditionalExpression(node, aliasEnv, locals) {
|
|
225
|
-
if (!node.branches.length) {
|
|
226
|
-
return "null";
|
|
227
|
-
}
|
|
228
|
-
const first = node.branches[0];
|
|
229
|
-
if (node.branches.length === 1 && first.test) {
|
|
230
|
-
const test = emitExpressionValue(first.test, locals);
|
|
231
|
-
return `(${test}) && ${emitBranchExpression(first, aliasEnv, locals)}`;
|
|
232
|
-
}
|
|
233
|
-
const hasElse = node.branches[node.branches.length - 1].test === void 0;
|
|
234
|
-
let fallback = hasElse ? emitBranchExpression(node.branches[node.branches.length - 1], aliasEnv, locals) : "null";
|
|
235
|
-
const startIndex = hasElse ? node.branches.length - 2 : node.branches.length - 1;
|
|
236
|
-
if (startIndex < 0) {
|
|
237
|
-
return fallback;
|
|
238
|
-
}
|
|
239
|
-
for (let i = startIndex; i >= 0; i--) {
|
|
240
|
-
const branch = node.branches[i];
|
|
241
|
-
const test = branch.test ? emitExpressionValue(branch.test, locals) : "false";
|
|
242
|
-
fallback = `(${test}) ? ${emitBranchExpression(branch, aliasEnv, locals)} : ${fallback}`;
|
|
243
|
-
}
|
|
244
|
-
return fallback;
|
|
245
|
-
}
|
|
246
|
-
function emitBranchExpression(branch, aliasEnv, locals) {
|
|
247
|
-
return emitNodesExpression(branch.body, aliasEnv, locals);
|
|
248
|
-
}
|
|
249
|
-
function emitNodesExpression(children, aliasEnv, locals) {
|
|
250
|
-
if (children.length === 0) {
|
|
251
|
-
return "null";
|
|
252
|
-
}
|
|
253
|
-
if (children.length === 1) {
|
|
254
|
-
return emitSingleNodeExpression(children[0], aliasEnv, locals);
|
|
255
|
-
}
|
|
256
|
-
return `<>${children.map((child) => emitNodeInJsx(child, aliasEnv, locals)).join("")}</>`;
|
|
257
|
-
}
|
|
258
|
-
function emitSingleNodeExpression(node, aliasEnv, locals) {
|
|
259
|
-
if (node.type === "Expression") {
|
|
260
|
-
return emitExpressionValue(node.value, locals);
|
|
261
|
-
}
|
|
262
|
-
if (node.type === "JSXPassthrough") {
|
|
263
|
-
return emitJsxExpression(node.expression, locals);
|
|
264
|
-
}
|
|
265
|
-
if (node.type === "Conditional") {
|
|
266
|
-
return emitConditionalExpression(node, aliasEnv, locals);
|
|
267
|
-
}
|
|
268
|
-
if (node.type === "For") {
|
|
269
|
-
return emitForExpression(node, aliasEnv, locals);
|
|
270
|
-
}
|
|
271
|
-
if (node.type === "Element") {
|
|
272
|
-
return wrapWithGuard(emitElement(node, aliasEnv, locals), node.guard, "expression", locals);
|
|
273
|
-
}
|
|
274
|
-
if (node.type === "Component") {
|
|
275
|
-
return wrapWithGuard(emitComponent(node, aliasEnv, locals), node.guard, "expression", locals);
|
|
276
|
-
}
|
|
277
|
-
if (node.type === "Text") {
|
|
278
|
-
return `<>${emitNodeInJsx(node, aliasEnv, locals)}</>`;
|
|
279
|
-
}
|
|
280
|
-
return emitNodeInJsx(node, aliasEnv, locals);
|
|
281
|
-
}
|
|
282
|
-
function emitPropsType(props, flavor) {
|
|
283
|
-
if (flavor === "tsx") {
|
|
284
|
-
return emitTsPropsType(props);
|
|
285
|
-
}
|
|
286
|
-
return emitJsDocPropsType(props);
|
|
287
|
-
}
|
|
288
|
-
function emitJsDocPropsType(props) {
|
|
289
|
-
if (!props) {
|
|
290
|
-
return "/** @typedef {any} Props */";
|
|
291
|
-
}
|
|
292
|
-
if (!props.fields.length) {
|
|
293
|
-
return "/** @typedef {{}} Props */";
|
|
294
|
-
}
|
|
295
|
-
const fields = props.fields.map((field) => {
|
|
296
|
-
const optional = field.optional ? "?" : "";
|
|
297
|
-
return `${field.name}${optional}: ${field.typeText}`;
|
|
298
|
-
}).join("; ");
|
|
299
|
-
return `/** @typedef {{ ${fields} }} Props */`;
|
|
300
|
-
}
|
|
301
|
-
function emitTsPropsType(props) {
|
|
302
|
-
if (!props || props.fields.length === 0) {
|
|
303
|
-
return "export type Props = Record<string, never>;";
|
|
304
26
|
}
|
|
305
|
-
|
|
306
|
-
const optional = field.optional ? "?" : "";
|
|
307
|
-
return ` ${field.name}${optional}: ${field.typeText};`;
|
|
308
|
-
});
|
|
309
|
-
return ["export interface Props {", ...lines, "}"].join("\n");
|
|
27
|
+
return false;
|
|
310
28
|
}
|
|
311
|
-
function
|
|
312
|
-
if (
|
|
313
|
-
return
|
|
29
|
+
function isPropAlias(env, name) {
|
|
30
|
+
if (isLocal(env, name)) {
|
|
31
|
+
return false;
|
|
314
32
|
}
|
|
315
|
-
|
|
316
|
-
return `const { ${names.join(", ")} } = props ?? {};`;
|
|
317
|
-
}
|
|
318
|
-
function escapeText(value) {
|
|
319
|
-
return value.replace(/[&<>{}]/g, (char) => {
|
|
320
|
-
switch (char) {
|
|
321
|
-
case "&":
|
|
322
|
-
return "&";
|
|
323
|
-
case "<":
|
|
324
|
-
return "<";
|
|
325
|
-
case ">":
|
|
326
|
-
return ">";
|
|
327
|
-
case "{":
|
|
328
|
-
return "{";
|
|
329
|
-
case "}":
|
|
330
|
-
return "}";
|
|
331
|
-
default:
|
|
332
|
-
return char;
|
|
333
|
-
}
|
|
334
|
-
});
|
|
33
|
+
return env.propAliases.has(name);
|
|
335
34
|
}
|
|
336
35
|
var IGNORED_IDENTIFIERS = /* @__PURE__ */ new Set([
|
|
337
36
|
"null",
|
|
@@ -384,13 +83,15 @@ var RESERVED_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
384
83
|
"with",
|
|
385
84
|
"yield"
|
|
386
85
|
]);
|
|
387
|
-
function
|
|
388
|
-
return `props?.${name}`;
|
|
389
|
-
}
|
|
390
|
-
function rewriteExpression(expression, locals) {
|
|
86
|
+
function rewriteExpression(expression, env) {
|
|
391
87
|
let i = 0;
|
|
392
88
|
let state = "code";
|
|
393
89
|
let output = "";
|
|
90
|
+
const usedBare = /* @__PURE__ */ new Set();
|
|
91
|
+
const usedPropsDot = /* @__PURE__ */ new Set();
|
|
92
|
+
const callSitesBare = /* @__PURE__ */ new Set();
|
|
93
|
+
const callSitesPropsDot = /* @__PURE__ */ new Set();
|
|
94
|
+
const rewrittenAliases = /* @__PURE__ */ new Set();
|
|
394
95
|
while (i < expression.length) {
|
|
395
96
|
const ch = expression[i];
|
|
396
97
|
if (state === "code") {
|
|
@@ -428,12 +129,37 @@ function rewriteExpression(expression, locals) {
|
|
|
428
129
|
const prevNonSpace = findPreviousNonSpace(expression, start - 1);
|
|
429
130
|
const nextNonSpace = findNextNonSpace(expression, i);
|
|
430
131
|
const isMemberAccess = prevNonSpace === ".";
|
|
431
|
-
const isObjectKey = nextNonSpace === ":";
|
|
432
|
-
|
|
132
|
+
const isObjectKey = nextNonSpace === ":" && (prevNonSpace === "{" || prevNonSpace === ",");
|
|
133
|
+
const isCall = nextNonSpace === "(";
|
|
134
|
+
if (prevNonSpace === "." && start >= 2) {
|
|
135
|
+
const propsStart = findPreviousIdentifierStart(expression, start - 2);
|
|
136
|
+
if (propsStart !== null) {
|
|
137
|
+
const possibleProps = expression.slice(propsStart, start - 1).trim();
|
|
138
|
+
if (possibleProps === "props") {
|
|
139
|
+
usedPropsDot.add(name);
|
|
140
|
+
if (isCall) {
|
|
141
|
+
callSitesPropsDot.add(name);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (isMemberAccess || isObjectKey || isLocal(env, name) || shouldIgnoreIdentifier(name)) {
|
|
433
147
|
output += name;
|
|
434
148
|
continue;
|
|
435
149
|
}
|
|
436
|
-
|
|
150
|
+
if (isPropAlias(env, name)) {
|
|
151
|
+
output += `props.${name}`;
|
|
152
|
+
rewrittenAliases.add(name);
|
|
153
|
+
if (isCall) {
|
|
154
|
+
callSitesBare.add(name);
|
|
155
|
+
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
usedBare.add(name);
|
|
159
|
+
if (isCall) {
|
|
160
|
+
callSitesBare.add(name);
|
|
161
|
+
}
|
|
162
|
+
output += name;
|
|
437
163
|
continue;
|
|
438
164
|
}
|
|
439
165
|
output += ch;
|
|
@@ -505,11 +231,16 @@ function rewriteExpression(expression, locals) {
|
|
|
505
231
|
continue;
|
|
506
232
|
}
|
|
507
233
|
}
|
|
508
|
-
return output;
|
|
234
|
+
return { code: output, usedBare, usedPropsDot, callSitesBare, callSitesPropsDot, rewrittenAliases };
|
|
509
235
|
}
|
|
510
|
-
function rewriteJsxExpression(expression,
|
|
236
|
+
function rewriteJsxExpression(expression, env) {
|
|
511
237
|
let output = "";
|
|
512
238
|
let i = 0;
|
|
239
|
+
const usedBare = /* @__PURE__ */ new Set();
|
|
240
|
+
const usedPropsDot = /* @__PURE__ */ new Set();
|
|
241
|
+
const callSitesBare = /* @__PURE__ */ new Set();
|
|
242
|
+
const callSitesPropsDot = /* @__PURE__ */ new Set();
|
|
243
|
+
const rewrittenAliases = /* @__PURE__ */ new Set();
|
|
513
244
|
while (i < expression.length) {
|
|
514
245
|
const ch = expression[i];
|
|
515
246
|
if (ch === "{") {
|
|
@@ -518,15 +249,20 @@ function rewriteJsxExpression(expression, locals) {
|
|
|
518
249
|
output += expression.slice(i);
|
|
519
250
|
break;
|
|
520
251
|
}
|
|
521
|
-
const
|
|
522
|
-
output += `{${
|
|
252
|
+
const result = rewriteExpression(braceResult.content, env);
|
|
253
|
+
output += `{${result.code}}`;
|
|
254
|
+
for (const name of result.usedBare) usedBare.add(name);
|
|
255
|
+
for (const name of result.usedPropsDot) usedPropsDot.add(name);
|
|
256
|
+
for (const name of result.callSitesBare) callSitesBare.add(name);
|
|
257
|
+
for (const name of result.callSitesPropsDot) callSitesPropsDot.add(name);
|
|
258
|
+
for (const name of result.rewrittenAliases) rewrittenAliases.add(name);
|
|
523
259
|
i = braceResult.endIndex + 1;
|
|
524
260
|
continue;
|
|
525
261
|
}
|
|
526
262
|
output += ch;
|
|
527
263
|
i++;
|
|
528
264
|
}
|
|
529
|
-
return output;
|
|
265
|
+
return { code: output, usedBare, usedPropsDot, callSitesBare, callSitesPropsDot, rewrittenAliases };
|
|
530
266
|
}
|
|
531
267
|
function readBalancedBraces(source, startIndex) {
|
|
532
268
|
let i = startIndex;
|
|
@@ -616,34 +352,385 @@ function readBalancedBraces(source, startIndex) {
|
|
|
616
352
|
continue;
|
|
617
353
|
}
|
|
618
354
|
}
|
|
619
|
-
return null;
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
function findPreviousNonSpace(text, index) {
|
|
358
|
+
for (let i = index; i >= 0; i--) {
|
|
359
|
+
const ch = text[i];
|
|
360
|
+
if (!/\s/.test(ch)) {
|
|
361
|
+
return ch;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
function findNextNonSpace(text, index) {
|
|
367
|
+
for (let i = index; i < text.length; i++) {
|
|
368
|
+
const ch = text[i];
|
|
369
|
+
if (!/\s/.test(ch)) {
|
|
370
|
+
return ch;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
function findPreviousIdentifierStart(text, index) {
|
|
376
|
+
let i = index;
|
|
377
|
+
while (i >= 0 && /\s/.test(text[i])) {
|
|
378
|
+
i--;
|
|
379
|
+
}
|
|
380
|
+
if (i < 0) return null;
|
|
381
|
+
if (!isIdentifierPart(text[i])) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
while (i > 0 && isIdentifierPart(text[i - 1])) {
|
|
385
|
+
i--;
|
|
386
|
+
}
|
|
387
|
+
return i;
|
|
388
|
+
}
|
|
389
|
+
function isIdentifierStart(ch) {
|
|
390
|
+
return /[A-Za-z_$]/.test(ch);
|
|
391
|
+
}
|
|
392
|
+
function isIdentifierPart(ch) {
|
|
393
|
+
return /[A-Za-z0-9_$]/.test(ch);
|
|
394
|
+
}
|
|
395
|
+
function shouldIgnoreIdentifier(name) {
|
|
396
|
+
return IGNORED_IDENTIFIERS.has(name) || RESERVED_KEYWORDS.has(name);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// src/codegen.ts
|
|
400
|
+
function generateRenderModule(root, options) {
|
|
401
|
+
const { prelude, propsType, propsDestructure, jsx, isTsx } = buildModuleParts(root, options);
|
|
402
|
+
const parts = [...prelude, propsType];
|
|
403
|
+
if (!isTsx) {
|
|
404
|
+
parts.push(`/** @param {any} props */`);
|
|
405
|
+
}
|
|
406
|
+
const functionLines = [
|
|
407
|
+
isTsx ? "export function render(props: any) {" : "export function render(props) {"
|
|
408
|
+
];
|
|
409
|
+
if (propsDestructure) {
|
|
410
|
+
functionLines.push(` ${propsDestructure}`);
|
|
411
|
+
}
|
|
412
|
+
functionLines.push(` return ${jsx};`, `}`);
|
|
413
|
+
parts.push(functionLines.join("\n"));
|
|
414
|
+
return parts.join("\n\n");
|
|
415
|
+
}
|
|
416
|
+
function buildModuleParts(root, options) {
|
|
417
|
+
const { jsxRuntime, flavor } = options;
|
|
418
|
+
const isTsx = flavor === "tsx";
|
|
419
|
+
const aliasEnv = buildClassAliasEnvironment(root.classAliases);
|
|
420
|
+
const env = createTemplateEnv(root.propsDecls);
|
|
421
|
+
const jsx = renderRootChildren(root.children, aliasEnv, env);
|
|
422
|
+
const propsDestructure = emitPropsDestructure(root.props);
|
|
423
|
+
const prelude = [];
|
|
424
|
+
if (root.clientComponent) {
|
|
425
|
+
prelude.push(`"use client";`);
|
|
426
|
+
}
|
|
427
|
+
if (jsxRuntime === "classic" && templateUsesJsx(root)) {
|
|
428
|
+
prelude.push(`import React from "react";`);
|
|
429
|
+
}
|
|
430
|
+
const propsType = emitPropsType(root.props, flavor);
|
|
431
|
+
return { prelude, propsType, propsDestructure, jsx, isTsx };
|
|
432
|
+
}
|
|
433
|
+
function buildClassAliasEnvironment(decl) {
|
|
434
|
+
const env = /* @__PURE__ */ new Map();
|
|
435
|
+
if (!decl) {
|
|
436
|
+
return env;
|
|
437
|
+
}
|
|
438
|
+
for (const alias of decl.aliases) {
|
|
439
|
+
env.set(alias.name, alias.classes);
|
|
440
|
+
}
|
|
441
|
+
return env;
|
|
442
|
+
}
|
|
443
|
+
function renderRootChildren(children, aliasEnv, env) {
|
|
444
|
+
return emitNodesExpression(children, aliasEnv, env);
|
|
445
|
+
}
|
|
446
|
+
function templateUsesJsx(root) {
|
|
447
|
+
if (root.children.length === 0) {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
if (root.children.length > 1) {
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
return nodeUsesJsx(root.children[0]);
|
|
454
|
+
}
|
|
455
|
+
function nodeUsesJsx(node) {
|
|
456
|
+
if (node.type === "Element" || node.type === "Text" || node.type === "Component") {
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
if (node.type === "Expression" || node.type === "JSXPassthrough") {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
if (node.type === "Conditional") {
|
|
463
|
+
return node.branches.some((branch) => branchUsesJsx(branch));
|
|
464
|
+
}
|
|
465
|
+
if (node.type === "For") {
|
|
466
|
+
return node.body.some((child) => nodeUsesJsx(child));
|
|
467
|
+
}
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
function branchUsesJsx(branch) {
|
|
471
|
+
if (!branch.body.length) {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
return branch.body.some((child) => nodeUsesJsx(child));
|
|
475
|
+
}
|
|
476
|
+
function emitNodeInJsx(node, aliasEnv, env) {
|
|
477
|
+
if (node.type === "Text") {
|
|
478
|
+
return emitText(node, env);
|
|
479
|
+
}
|
|
480
|
+
if (node.type === "Expression") {
|
|
481
|
+
return `{${emitExpressionValue(node.value, env)}}`;
|
|
482
|
+
}
|
|
483
|
+
if (node.type === "JSXPassthrough") {
|
|
484
|
+
return `{${emitJsxExpression(node.expression, env)}}`;
|
|
485
|
+
}
|
|
486
|
+
if (node.type === "Conditional") {
|
|
487
|
+
return `{${emitConditionalExpression(node, aliasEnv, env)}}`;
|
|
488
|
+
}
|
|
489
|
+
if (node.type === "For") {
|
|
490
|
+
return `{${emitForExpression(node, aliasEnv, env)}}`;
|
|
491
|
+
}
|
|
492
|
+
if (node.type === "Component") {
|
|
493
|
+
return wrapWithGuard(emitComponent(node, aliasEnv, env), node.guard, "jsx", env);
|
|
494
|
+
}
|
|
495
|
+
return wrapWithGuard(emitElement(node, aliasEnv, env), node.guard, "jsx", env);
|
|
496
|
+
}
|
|
497
|
+
function emitElement(node, aliasEnv, env) {
|
|
498
|
+
const expanded = expandClasses(node.classes, aliasEnv);
|
|
499
|
+
const classAttr = expanded.length ? ` className="${expanded.join(" ")}"` : "";
|
|
500
|
+
const attrs = emitAttributes(node.attributes, aliasEnv, env);
|
|
501
|
+
const allAttrs = classAttr + attrs;
|
|
502
|
+
const children = emitChildrenWithSpacing(node.children, aliasEnv, env);
|
|
503
|
+
if (children.length > 0) {
|
|
504
|
+
return `<${node.name}${allAttrs}>${children}</${node.name}>`;
|
|
505
|
+
} else {
|
|
506
|
+
return `<${node.name}${allAttrs} />`;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
function emitComponent(node, aliasEnv, env) {
|
|
510
|
+
const attrs = emitAttributes(node.attributes, aliasEnv, env);
|
|
511
|
+
const slotProps = emitSlotProps(node, aliasEnv, env);
|
|
512
|
+
const allAttrs = `${attrs}${slotProps}`;
|
|
513
|
+
const children = emitChildrenWithSpacing(node.children, aliasEnv, env);
|
|
514
|
+
if (children.length > 0) {
|
|
515
|
+
return `<${node.name}${allAttrs}>${children}</${node.name}>`;
|
|
516
|
+
} else {
|
|
517
|
+
return `<${node.name}${allAttrs} />`;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
function emitChildrenWithSpacing(children, aliasEnv, env) {
|
|
521
|
+
if (children.length === 0) {
|
|
522
|
+
return "";
|
|
523
|
+
}
|
|
524
|
+
const parts = [];
|
|
525
|
+
for (let i = 0; i < children.length; i++) {
|
|
526
|
+
const child = children[i];
|
|
527
|
+
const emitted = emitNodeInJsx(child, aliasEnv, env);
|
|
528
|
+
parts.push(emitted);
|
|
529
|
+
if (i < children.length - 1) {
|
|
530
|
+
const nextChild = children[i + 1];
|
|
531
|
+
const needsSpace = child.type === "Text" && (nextChild.type === "Element" || nextChild.type === "Component" || nextChild.type === "Expression" || nextChild.type === "JSXPassthrough");
|
|
532
|
+
if (needsSpace) {
|
|
533
|
+
parts.push(" ");
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return parts.join("");
|
|
538
|
+
}
|
|
539
|
+
function emitAttributes(attributes, aliasEnv, env) {
|
|
540
|
+
if (attributes.length === 0) {
|
|
541
|
+
return "";
|
|
542
|
+
}
|
|
543
|
+
return attributes.map((attr) => {
|
|
544
|
+
if (attr.value === null) {
|
|
545
|
+
return ` ${attr.name}`;
|
|
546
|
+
}
|
|
547
|
+
return ` ${attr.name}=${emitAttributeValue(attr.value, env)}`;
|
|
548
|
+
}).join("");
|
|
549
|
+
}
|
|
550
|
+
function emitSlotProps(node, aliasEnv, env) {
|
|
551
|
+
if (!node.slots || node.slots.length === 0) {
|
|
552
|
+
return "";
|
|
553
|
+
}
|
|
554
|
+
return node.slots.map((slot) => {
|
|
555
|
+
const expr = emitNodesExpression(slot.children, aliasEnv, env);
|
|
556
|
+
return ` ${slot.name}={${expr}}`;
|
|
557
|
+
}).join("");
|
|
558
|
+
}
|
|
559
|
+
function wrapWithGuard(rendered, guard, context, env) {
|
|
560
|
+
if (!guard) {
|
|
561
|
+
return rendered;
|
|
562
|
+
}
|
|
563
|
+
const condition = emitExpressionValue(guard, env);
|
|
564
|
+
const expression = `(${condition}) && ${rendered}`;
|
|
565
|
+
return context === "jsx" ? `{${expression}}` : expression;
|
|
566
|
+
}
|
|
567
|
+
function emitForExpression(node, aliasEnv, env) {
|
|
568
|
+
const arrayExpr = emitExpressionValue(node.arrayExpr, env);
|
|
569
|
+
pushLocals(env, [node.itemName]);
|
|
570
|
+
const body = emitNodesExpression(node.body, aliasEnv, env);
|
|
571
|
+
popLocals(env);
|
|
572
|
+
return `(${arrayExpr} ?? []).map((${node.itemName}) => ${body})`;
|
|
573
|
+
}
|
|
574
|
+
function expandClasses(classes, aliasEnv) {
|
|
575
|
+
const result = [];
|
|
576
|
+
for (const cls of classes) {
|
|
577
|
+
const match = cls.match(/^\$([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
578
|
+
if (!match) {
|
|
579
|
+
result.push(cls);
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
const aliasClasses = aliasEnv.get(match[1]);
|
|
583
|
+
if (!aliasClasses) {
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
result.push(...aliasClasses);
|
|
587
|
+
}
|
|
588
|
+
return result;
|
|
589
|
+
}
|
|
590
|
+
function emitExpressionValue(expression, env) {
|
|
591
|
+
return rewriteExpression(expression, env).code;
|
|
592
|
+
}
|
|
593
|
+
function emitJsxExpression(expression, env) {
|
|
594
|
+
const trimmed = expression.trimStart();
|
|
595
|
+
if (trimmed.startsWith("<")) {
|
|
596
|
+
return rewriteJsxExpression(expression, env).code;
|
|
597
|
+
}
|
|
598
|
+
return rewriteExpression(expression, env).code;
|
|
599
|
+
}
|
|
600
|
+
function emitAttributeValue(value, env) {
|
|
601
|
+
const trimmed = value.trim();
|
|
602
|
+
if (trimmed.startsWith('"') || trimmed.startsWith("'")) {
|
|
603
|
+
return value;
|
|
604
|
+
}
|
|
605
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
606
|
+
const inner = trimmed.slice(1, -1);
|
|
607
|
+
const rewritten = rewriteExpression(inner, env).code;
|
|
608
|
+
return `{${rewritten}}`;
|
|
609
|
+
}
|
|
610
|
+
return rewriteExpression(trimmed, env).code;
|
|
611
|
+
}
|
|
612
|
+
function emitText(node, env) {
|
|
613
|
+
if (!node.parts.length) {
|
|
614
|
+
return "";
|
|
615
|
+
}
|
|
616
|
+
return node.parts.map((part) => {
|
|
617
|
+
if (part.type === "text") {
|
|
618
|
+
return escapeText(part.value);
|
|
619
|
+
}
|
|
620
|
+
return `{${emitExpressionValue(part.value, env)}}`;
|
|
621
|
+
}).join("");
|
|
622
|
+
}
|
|
623
|
+
function emitConditionalExpression(node, aliasEnv, env) {
|
|
624
|
+
if (!node.branches.length) {
|
|
625
|
+
return "null";
|
|
626
|
+
}
|
|
627
|
+
const first = node.branches[0];
|
|
628
|
+
if (node.branches.length === 1 && first.test) {
|
|
629
|
+
const test = emitExpressionValue(first.test, env);
|
|
630
|
+
return `(${test}) && ${emitBranchExpression(first, aliasEnv, env)}`;
|
|
631
|
+
}
|
|
632
|
+
const hasElse = node.branches[node.branches.length - 1].test === void 0;
|
|
633
|
+
let fallback = hasElse ? emitBranchExpression(node.branches[node.branches.length - 1], aliasEnv, env) : "null";
|
|
634
|
+
const startIndex = hasElse ? node.branches.length - 2 : node.branches.length - 1;
|
|
635
|
+
if (startIndex < 0) {
|
|
636
|
+
return fallback;
|
|
637
|
+
}
|
|
638
|
+
for (let i = startIndex; i >= 0; i--) {
|
|
639
|
+
const branch = node.branches[i];
|
|
640
|
+
const test = branch.test ? emitExpressionValue(branch.test, env) : "false";
|
|
641
|
+
fallback = `(${test}) ? ${emitBranchExpression(branch, aliasEnv, env)} : ${fallback}`;
|
|
642
|
+
}
|
|
643
|
+
return fallback;
|
|
644
|
+
}
|
|
645
|
+
function emitBranchExpression(branch, aliasEnv, env) {
|
|
646
|
+
return emitNodesExpression(branch.body, aliasEnv, env);
|
|
647
|
+
}
|
|
648
|
+
function emitNodesExpression(children, aliasEnv, env) {
|
|
649
|
+
if (children.length === 0) {
|
|
650
|
+
return "null";
|
|
651
|
+
}
|
|
652
|
+
if (children.length === 1) {
|
|
653
|
+
return emitSingleNodeExpression(children[0], aliasEnv, env);
|
|
654
|
+
}
|
|
655
|
+
return `<>${children.map((child) => emitNodeInJsx(child, aliasEnv, env)).join("")}</>`;
|
|
620
656
|
}
|
|
621
|
-
function
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
if (!/\s/.test(ch)) {
|
|
625
|
-
return ch;
|
|
626
|
-
}
|
|
657
|
+
function emitSingleNodeExpression(node, aliasEnv, env) {
|
|
658
|
+
if (node.type === "Expression") {
|
|
659
|
+
return emitExpressionValue(node.value, env);
|
|
627
660
|
}
|
|
628
|
-
|
|
661
|
+
if (node.type === "JSXPassthrough") {
|
|
662
|
+
return emitJsxExpression(node.expression, env);
|
|
663
|
+
}
|
|
664
|
+
if (node.type === "Conditional") {
|
|
665
|
+
return emitConditionalExpression(node, aliasEnv, env);
|
|
666
|
+
}
|
|
667
|
+
if (node.type === "For") {
|
|
668
|
+
return emitForExpression(node, aliasEnv, env);
|
|
669
|
+
}
|
|
670
|
+
if (node.type === "Element") {
|
|
671
|
+
return wrapWithGuard(emitElement(node, aliasEnv, env), node.guard, "expression", env);
|
|
672
|
+
}
|
|
673
|
+
if (node.type === "Component") {
|
|
674
|
+
return wrapWithGuard(emitComponent(node, aliasEnv, env), node.guard, "expression", env);
|
|
675
|
+
}
|
|
676
|
+
if (node.type === "Text") {
|
|
677
|
+
return `<>${emitNodeInJsx(node, aliasEnv, env)}</>`;
|
|
678
|
+
}
|
|
679
|
+
return emitNodeInJsx(node, aliasEnv, env);
|
|
629
680
|
}
|
|
630
|
-
function
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
if (!/\s/.test(ch)) {
|
|
634
|
-
return ch;
|
|
635
|
-
}
|
|
681
|
+
function emitPropsType(props, flavor) {
|
|
682
|
+
if (flavor === "tsx") {
|
|
683
|
+
return emitTsPropsType(props);
|
|
636
684
|
}
|
|
637
|
-
return
|
|
685
|
+
return emitJsDocPropsType(props);
|
|
638
686
|
}
|
|
639
|
-
function
|
|
640
|
-
|
|
687
|
+
function emitJsDocPropsType(props) {
|
|
688
|
+
if (!props) {
|
|
689
|
+
return "/** @typedef {any} Props */";
|
|
690
|
+
}
|
|
691
|
+
if (!props.fields.length) {
|
|
692
|
+
return "/** @typedef {{}} Props */";
|
|
693
|
+
}
|
|
694
|
+
const fields = props.fields.map((field) => {
|
|
695
|
+
const optional = field.optional ? "?" : "";
|
|
696
|
+
return `${field.name}${optional}: ${field.typeText}`;
|
|
697
|
+
}).join("; ");
|
|
698
|
+
return `/** @typedef {{ ${fields} }} Props */`;
|
|
641
699
|
}
|
|
642
|
-
function
|
|
643
|
-
|
|
700
|
+
function emitTsPropsType(props) {
|
|
701
|
+
if (!props || props.fields.length === 0) {
|
|
702
|
+
return "export type Props = Record<string, never>;";
|
|
703
|
+
}
|
|
704
|
+
const lines = props.fields.map((field) => {
|
|
705
|
+
const optional = field.optional ? "?" : "";
|
|
706
|
+
return ` ${field.name}${optional}: ${field.typeText};`;
|
|
707
|
+
});
|
|
708
|
+
return ["export interface Props {", ...lines, "}"].join("\n");
|
|
644
709
|
}
|
|
645
|
-
function
|
|
646
|
-
|
|
710
|
+
function emitPropsDestructure(props) {
|
|
711
|
+
if (!props || props.fields.length === 0) {
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
const names = props.fields.map((field) => field.name);
|
|
715
|
+
return `const { ${names.join(", ")} } = props ?? {};`;
|
|
716
|
+
}
|
|
717
|
+
function escapeText(value) {
|
|
718
|
+
return value.replace(/[&<>{}]/g, (char) => {
|
|
719
|
+
switch (char) {
|
|
720
|
+
case "&":
|
|
721
|
+
return "&";
|
|
722
|
+
case "<":
|
|
723
|
+
return "<";
|
|
724
|
+
case ">":
|
|
725
|
+
return ">";
|
|
726
|
+
case "{":
|
|
727
|
+
return "{";
|
|
728
|
+
case "}":
|
|
729
|
+
return "}";
|
|
730
|
+
default:
|
|
731
|
+
return char;
|
|
732
|
+
}
|
|
733
|
+
});
|
|
647
734
|
}
|
|
648
735
|
|
|
649
736
|
// src/html-codegen.ts
|
|
@@ -1439,6 +1526,185 @@ function offsetSpan(base, index, length) {
|
|
|
1439
1526
|
}
|
|
1440
1527
|
};
|
|
1441
1528
|
}
|
|
1529
|
+
function enforcePropAliases(root) {
|
|
1530
|
+
if (!root.propsDecls || root.propsDecls.length === 0) {
|
|
1531
|
+
return [];
|
|
1532
|
+
}
|
|
1533
|
+
const diagnostics = [];
|
|
1534
|
+
const declaredProps = new Map(root.propsDecls.map((d) => [d.name, d]));
|
|
1535
|
+
const allUsage = collectTemplateUsage(root);
|
|
1536
|
+
for (const name of allUsage.usedBare) {
|
|
1537
|
+
if (!declaredProps.has(name) && !shouldIgnoreForDiagnostics(name)) {
|
|
1538
|
+
diagnostics.push({
|
|
1539
|
+
severity: "warning",
|
|
1540
|
+
code: "props.missingDeclaration",
|
|
1541
|
+
message: `Identifier "${name}" is used without "props." but is not declared in #props. Declare "${name}" in #props or use "props.${name}".`
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
for (const [name, decl] of declaredProps) {
|
|
1546
|
+
const usedAsBare = allUsage.usedBareAliases.has(name);
|
|
1547
|
+
const usedAsProps = allUsage.usedPropsDot.has(name);
|
|
1548
|
+
if (!usedAsBare && !usedAsProps) {
|
|
1549
|
+
diagnostics.push({
|
|
1550
|
+
severity: "warning",
|
|
1551
|
+
code: "props.unusedDeclaration",
|
|
1552
|
+
message: `Prop "${name}" is declared in #props but never used in this template.`,
|
|
1553
|
+
span: decl.span
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
for (const name of allUsage.usedPropsDot) {
|
|
1558
|
+
if (declaredProps.has(name)) {
|
|
1559
|
+
diagnostics.push({
|
|
1560
|
+
severity: "warning",
|
|
1561
|
+
code: "props.style.nonPreferred",
|
|
1562
|
+
message: `"props.${name}" is unnecessary because "${name}" is declared in #props. Use "{${name}}" instead.`
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
for (const [name, decl] of declaredProps) {
|
|
1567
|
+
const isCallable = decl.kind === "callable";
|
|
1568
|
+
const usedAsCall = allUsage.callSitesBare.has(name);
|
|
1569
|
+
const usedAsValue = allUsage.usedBareAliases.has(name) && !usedAsCall;
|
|
1570
|
+
if (isCallable && usedAsValue) {
|
|
1571
|
+
diagnostics.push({
|
|
1572
|
+
severity: "warning",
|
|
1573
|
+
code: "props.style.nonPreferred",
|
|
1574
|
+
message: `"${name}" is declared as callable in #props (${name}()) but used as a value.`
|
|
1575
|
+
});
|
|
1576
|
+
} else if (!isCallable && usedAsCall) {
|
|
1577
|
+
diagnostics.push({
|
|
1578
|
+
severity: "warning",
|
|
1579
|
+
code: "props.style.nonPreferred",
|
|
1580
|
+
message: `"${name}" is declared as a value in #props but used as a function call.`
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
return diagnostics;
|
|
1585
|
+
}
|
|
1586
|
+
function collectTemplateUsage(root) {
|
|
1587
|
+
const usage = {
|
|
1588
|
+
usedBare: /* @__PURE__ */ new Set(),
|
|
1589
|
+
usedBareAliases: /* @__PURE__ */ new Set(),
|
|
1590
|
+
usedPropsDot: /* @__PURE__ */ new Set(),
|
|
1591
|
+
callSitesBare: /* @__PURE__ */ new Set(),
|
|
1592
|
+
callSitesPropsDot: /* @__PURE__ */ new Set()
|
|
1593
|
+
};
|
|
1594
|
+
const env = createTemplateEnv(root.propsDecls);
|
|
1595
|
+
function mergeResult(result) {
|
|
1596
|
+
for (const name of result.usedBare) {
|
|
1597
|
+
usage.usedBare.add(name);
|
|
1598
|
+
}
|
|
1599
|
+
for (const name of result.rewrittenAliases) {
|
|
1600
|
+
usage.usedBareAliases.add(name);
|
|
1601
|
+
}
|
|
1602
|
+
for (const name of result.usedPropsDot) {
|
|
1603
|
+
usage.usedPropsDot.add(name);
|
|
1604
|
+
}
|
|
1605
|
+
for (const name of result.callSitesBare) {
|
|
1606
|
+
usage.callSitesBare.add(name);
|
|
1607
|
+
}
|
|
1608
|
+
for (const name of result.callSitesPropsDot) {
|
|
1609
|
+
usage.callSitesPropsDot.add(name);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
function analyzeExpression(expr) {
|
|
1613
|
+
if (!expr) return;
|
|
1614
|
+
const result = rewriteExpression(expr, env);
|
|
1615
|
+
mergeResult(result);
|
|
1616
|
+
}
|
|
1617
|
+
function analyzeJsxExpression(expr) {
|
|
1618
|
+
if (!expr) return;
|
|
1619
|
+
const result = rewriteJsxExpression(expr, env);
|
|
1620
|
+
mergeResult(result);
|
|
1621
|
+
}
|
|
1622
|
+
function walkNode(node) {
|
|
1623
|
+
switch (node.type) {
|
|
1624
|
+
case "Text":
|
|
1625
|
+
for (const part of node.parts) {
|
|
1626
|
+
if (part.type === "expr") {
|
|
1627
|
+
analyzeExpression(part.value);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
break;
|
|
1631
|
+
case "Expression":
|
|
1632
|
+
analyzeExpression(node.value);
|
|
1633
|
+
break;
|
|
1634
|
+
case "JSXPassthrough":
|
|
1635
|
+
analyzeJsxExpression(node.expression);
|
|
1636
|
+
break;
|
|
1637
|
+
case "Element":
|
|
1638
|
+
if (node.guard) {
|
|
1639
|
+
analyzeExpression(node.guard);
|
|
1640
|
+
}
|
|
1641
|
+
for (const attr of node.attributes) {
|
|
1642
|
+
if (attr.value) {
|
|
1643
|
+
analyzeAttributeValue(attr.value);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
for (const child of node.children) {
|
|
1647
|
+
walkNode(child);
|
|
1648
|
+
}
|
|
1649
|
+
break;
|
|
1650
|
+
case "Component":
|
|
1651
|
+
if (node.guard) {
|
|
1652
|
+
analyzeExpression(node.guard);
|
|
1653
|
+
}
|
|
1654
|
+
for (const attr of node.attributes) {
|
|
1655
|
+
if (attr.value) {
|
|
1656
|
+
analyzeAttributeValue(attr.value);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
if (node.slots) {
|
|
1660
|
+
for (const slot of node.slots) {
|
|
1661
|
+
for (const child of slot.children) {
|
|
1662
|
+
walkNode(child);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
for (const child of node.children) {
|
|
1667
|
+
walkNode(child);
|
|
1668
|
+
}
|
|
1669
|
+
break;
|
|
1670
|
+
case "Conditional":
|
|
1671
|
+
for (const branch of node.branches) {
|
|
1672
|
+
if (branch.test) {
|
|
1673
|
+
analyzeExpression(branch.test);
|
|
1674
|
+
}
|
|
1675
|
+
for (const child of branch.body) {
|
|
1676
|
+
walkNode(child);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
break;
|
|
1680
|
+
case "For":
|
|
1681
|
+
analyzeExpression(node.arrayExpr);
|
|
1682
|
+
for (const child of node.body) {
|
|
1683
|
+
walkNode(child);
|
|
1684
|
+
}
|
|
1685
|
+
break;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
function analyzeAttributeValue(value) {
|
|
1689
|
+
const trimmed = value.trim();
|
|
1690
|
+
if (trimmed.startsWith('"') || trimmed.startsWith("'")) {
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
1694
|
+
const inner = trimmed.slice(1, -1);
|
|
1695
|
+
analyzeExpression(inner);
|
|
1696
|
+
} else {
|
|
1697
|
+
analyzeExpression(trimmed);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
for (const child of root.children) {
|
|
1701
|
+
walkNode(child);
|
|
1702
|
+
}
|
|
1703
|
+
return usage;
|
|
1704
|
+
}
|
|
1705
|
+
function shouldIgnoreForDiagnostics(name) {
|
|
1706
|
+
return IGNORED_IDENTIFIERS2.has(name) || RESERVED_KEYWORDS2.has(name);
|
|
1707
|
+
}
|
|
1442
1708
|
|
|
1443
1709
|
// src/parser.ts
|
|
1444
1710
|
var TEMPLATE_ID_PATTERN = /^[A-Za-z][A-Za-z0-9._-]*$/;
|
|
@@ -1755,6 +2021,7 @@ function parseTemplateBlock(lines, lineOffsets, startIndex, endIndex, options) {
|
|
|
1755
2021
|
);
|
|
1756
2022
|
} else {
|
|
1757
2023
|
root.props = { fields: [] };
|
|
2024
|
+
root.propsDecls = [];
|
|
1758
2025
|
}
|
|
1759
2026
|
if (level === 0) {
|
|
1760
2027
|
propsBlockLevel = level;
|
|
@@ -1809,6 +2076,23 @@ function parseTemplateBlock(lines, lineOffsets, startIndex, endIndex, options) {
|
|
|
1809
2076
|
);
|
|
1810
2077
|
continue;
|
|
1811
2078
|
}
|
|
2079
|
+
const decl = parsePropDecl(lineContent, lineNumber, indent + 1, lineOffset, diagnostics);
|
|
2080
|
+
if (decl && root.propsDecls) {
|
|
2081
|
+
const existing = root.propsDecls.find((d) => d.name === decl.name);
|
|
2082
|
+
if (existing) {
|
|
2083
|
+
pushDiag(
|
|
2084
|
+
diagnostics,
|
|
2085
|
+
"COLLIE106",
|
|
2086
|
+
`Duplicate prop declaration "${decl.name}".`,
|
|
2087
|
+
lineNumber,
|
|
2088
|
+
indent + 1,
|
|
2089
|
+
lineOffset,
|
|
2090
|
+
trimmed.length
|
|
2091
|
+
);
|
|
2092
|
+
} else {
|
|
2093
|
+
root.propsDecls.push(decl);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
1812
2096
|
continue;
|
|
1813
2097
|
}
|
|
1814
2098
|
if (classesBlockLevel !== null && level > classesBlockLevel) {
|
|
@@ -2221,6 +2505,7 @@ function parseTemplateBlock(lines, lineOffsets, startIndex, endIndex, options) {
|
|
|
2221
2505
|
diagnostics.push(...enforceDialect(root, options.dialect));
|
|
2222
2506
|
diagnostics.push(...enforceProps(root, options.dialect.props));
|
|
2223
2507
|
}
|
|
2508
|
+
diagnostics.push(...enforcePropAliases(root));
|
|
2224
2509
|
return { root, diagnostics };
|
|
2225
2510
|
}
|
|
2226
2511
|
function buildLineOffsets(lines) {
|
|
@@ -2271,7 +2556,7 @@ function parseConditionalHeader(kind, lineContent, lineNumber, column, lineOffse
|
|
|
2271
2556
|
pushDiag(
|
|
2272
2557
|
diagnostics,
|
|
2273
2558
|
"COLLIE201",
|
|
2274
|
-
kind === "if" ? "Invalid @if syntax. Use @if
|
|
2559
|
+
kind === "if" ? "Invalid @if syntax. Use @if (condition)." : "Invalid @elseIf syntax. Use @elseIf (condition).",
|
|
2275
2560
|
lineNumber,
|
|
2276
2561
|
column,
|
|
2277
2562
|
lineOffset,
|
|
@@ -2295,29 +2580,36 @@ function parseConditionalHeader(kind, lineContent, lineNumber, column, lineOffse
|
|
|
2295
2580
|
}
|
|
2296
2581
|
const remainderTrimmed = remainder.trimStart();
|
|
2297
2582
|
const usesParens = remainderTrimmed.startsWith("(");
|
|
2583
|
+
if (!usesParens) {
|
|
2584
|
+
pushDiag(
|
|
2585
|
+
diagnostics,
|
|
2586
|
+
"COLLIE211",
|
|
2587
|
+
kind === "if" ? "@if requires parentheses: @if (condition)" : "@elseIf requires parentheses: @elseIf (condition)",
|
|
2588
|
+
lineNumber,
|
|
2589
|
+
column,
|
|
2590
|
+
lineOffset,
|
|
2591
|
+
trimmed.length || token.length
|
|
2592
|
+
);
|
|
2593
|
+
return null;
|
|
2594
|
+
}
|
|
2298
2595
|
let testRaw = "";
|
|
2299
2596
|
let remainderRaw = "";
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
return null;
|
|
2314
|
-
}
|
|
2315
|
-
testRaw = trimmed.slice(openIndex + 1, closeIndex);
|
|
2316
|
-
remainderRaw = trimmed.slice(closeIndex + 1);
|
|
2317
|
-
} else {
|
|
2318
|
-
testRaw = remainderTrimmed;
|
|
2319
|
-
remainderRaw = "";
|
|
2597
|
+
const openIndex = trimmed.indexOf("(", token.length);
|
|
2598
|
+
const closeIndex = trimmed.lastIndexOf(")");
|
|
2599
|
+
if (openIndex === -1 || closeIndex <= openIndex) {
|
|
2600
|
+
pushDiag(
|
|
2601
|
+
diagnostics,
|
|
2602
|
+
"COLLIE212",
|
|
2603
|
+
kind === "if" ? "Unclosed parentheses in @if ( ... )" : "Unclosed parentheses in @elseIf ( ... )",
|
|
2604
|
+
lineNumber,
|
|
2605
|
+
column,
|
|
2606
|
+
lineOffset,
|
|
2607
|
+
trimmed.length || token.length
|
|
2608
|
+
);
|
|
2609
|
+
return null;
|
|
2320
2610
|
}
|
|
2611
|
+
testRaw = trimmed.slice(openIndex + 1, closeIndex);
|
|
2612
|
+
remainderRaw = trimmed.slice(closeIndex + 1);
|
|
2321
2613
|
const test = testRaw.trim();
|
|
2322
2614
|
if (!test) {
|
|
2323
2615
|
pushDiag(
|
|
@@ -2366,7 +2658,20 @@ function parseElseHeader(lineContent, lineNumber, column, lineOffset, diagnostic
|
|
|
2366
2658
|
const token = "@else";
|
|
2367
2659
|
const tokenSpan = createSpan(lineNumber, column, token.length, lineOffset);
|
|
2368
2660
|
const remainderRaw = match[1] ?? "";
|
|
2369
|
-
const
|
|
2661
|
+
const remainderTrimmed = remainderRaw.trim();
|
|
2662
|
+
if (remainderTrimmed.startsWith("(")) {
|
|
2663
|
+
pushDiag(
|
|
2664
|
+
diagnostics,
|
|
2665
|
+
"COLLIE213",
|
|
2666
|
+
"@else does not accept a condition",
|
|
2667
|
+
lineNumber,
|
|
2668
|
+
column,
|
|
2669
|
+
lineOffset,
|
|
2670
|
+
trimmed.length || 4
|
|
2671
|
+
);
|
|
2672
|
+
return null;
|
|
2673
|
+
}
|
|
2674
|
+
const inlineBody = remainderTrimmed;
|
|
2370
2675
|
const remainderOffset = trimmed.length - remainderRaw.length;
|
|
2371
2676
|
const leadingWhitespace = remainderRaw.length - inlineBody.length;
|
|
2372
2677
|
const inlineColumn = inlineBody.length > 0 ? column + remainderOffset + leadingWhitespace : void 0;
|
|
@@ -2380,12 +2685,12 @@ function parseElseHeader(lineContent, lineNumber, column, lineOffset, diagnostic
|
|
|
2380
2685
|
}
|
|
2381
2686
|
function parseForHeader(lineContent, lineNumber, column, lineOffset, diagnostics) {
|
|
2382
2687
|
const trimmed = lineContent.trimEnd();
|
|
2383
|
-
const
|
|
2384
|
-
if (!
|
|
2688
|
+
const token = "@for";
|
|
2689
|
+
if (!trimmed.startsWith(token)) {
|
|
2385
2690
|
pushDiag(
|
|
2386
2691
|
diagnostics,
|
|
2387
2692
|
"COLLIE210",
|
|
2388
|
-
"Invalid @for syntax. Use @for
|
|
2693
|
+
"Invalid @for syntax. Use @for (item in array).",
|
|
2389
2694
|
lineNumber,
|
|
2390
2695
|
column,
|
|
2391
2696
|
lineOffset,
|
|
@@ -2393,15 +2698,55 @@ function parseForHeader(lineContent, lineNumber, column, lineOffset, diagnostics
|
|
|
2393
2698
|
);
|
|
2394
2699
|
return null;
|
|
2395
2700
|
}
|
|
2396
|
-
const token = "@for";
|
|
2397
2701
|
const tokenSpan = createSpan(lineNumber, column, token.length, lineOffset);
|
|
2702
|
+
const remainder = trimmed.slice(token.length).trimStart();
|
|
2703
|
+
if (!remainder.startsWith("(")) {
|
|
2704
|
+
pushDiag(
|
|
2705
|
+
diagnostics,
|
|
2706
|
+
"COLLIE211",
|
|
2707
|
+
"@for requires parentheses: @for (item in array)",
|
|
2708
|
+
lineNumber,
|
|
2709
|
+
column,
|
|
2710
|
+
lineOffset,
|
|
2711
|
+
trimmed.length || 4
|
|
2712
|
+
);
|
|
2713
|
+
return null;
|
|
2714
|
+
}
|
|
2715
|
+
const openIndex = trimmed.indexOf("(", token.length);
|
|
2716
|
+
const closeIndex = trimmed.lastIndexOf(")");
|
|
2717
|
+
if (openIndex === -1 || closeIndex <= openIndex) {
|
|
2718
|
+
pushDiag(
|
|
2719
|
+
diagnostics,
|
|
2720
|
+
"COLLIE212",
|
|
2721
|
+
"Unclosed parentheses in @for ( ... )",
|
|
2722
|
+
lineNumber,
|
|
2723
|
+
column,
|
|
2724
|
+
lineOffset,
|
|
2725
|
+
trimmed.length || 4
|
|
2726
|
+
);
|
|
2727
|
+
return null;
|
|
2728
|
+
}
|
|
2729
|
+
const content = trimmed.slice(openIndex + 1, closeIndex).trim();
|
|
2730
|
+
const match = content.match(/^([A-Za-z_][A-Za-z0-9_]*)\s+in\s+(.+)$/);
|
|
2731
|
+
if (!match) {
|
|
2732
|
+
pushDiag(
|
|
2733
|
+
diagnostics,
|
|
2734
|
+
"COLLIE210",
|
|
2735
|
+
"Invalid @for syntax. Use @for (item in array).",
|
|
2736
|
+
lineNumber,
|
|
2737
|
+
column,
|
|
2738
|
+
lineOffset,
|
|
2739
|
+
trimmed.length || 4
|
|
2740
|
+
);
|
|
2741
|
+
return null;
|
|
2742
|
+
}
|
|
2398
2743
|
const itemName = match[1];
|
|
2399
2744
|
const arrayExprRaw = match[2];
|
|
2400
2745
|
if (!itemName || !arrayExprRaw) {
|
|
2401
2746
|
pushDiag(
|
|
2402
2747
|
diagnostics,
|
|
2403
2748
|
"COLLIE210",
|
|
2404
|
-
"Invalid @for syntax. Use @for
|
|
2749
|
+
"Invalid @for syntax. Use @for (item in array).",
|
|
2405
2750
|
lineNumber,
|
|
2406
2751
|
column,
|
|
2407
2752
|
lineOffset,
|
|
@@ -2423,8 +2768,9 @@ function parseForHeader(lineContent, lineNumber, column, lineOffset, diagnostics
|
|
|
2423
2768
|
return null;
|
|
2424
2769
|
}
|
|
2425
2770
|
const arrayExprLeadingWhitespace = arrayExprRaw.length - arrayExprRaw.trimStart().length;
|
|
2426
|
-
const
|
|
2427
|
-
const
|
|
2771
|
+
const contentStart = openIndex + 1;
|
|
2772
|
+
const arrayExprStartInContent = content.length - arrayExprRaw.length;
|
|
2773
|
+
const arrayExprColumn = column + contentStart + arrayExprStartInContent + arrayExprLeadingWhitespace;
|
|
2428
2774
|
const arrayExprSpan = createSpan(lineNumber, arrayExprColumn, arrayExpr.length, lineOffset);
|
|
2429
2775
|
return { itemName, arrayExpr, token, tokenSpan, arrayExprSpan };
|
|
2430
2776
|
}
|
|
@@ -3319,6 +3665,53 @@ function parseAndAddAttribute(attrStr, attributes, diagnostics, lineNumber, colu
|
|
|
3319
3665
|
}
|
|
3320
3666
|
}
|
|
3321
3667
|
}
|
|
3668
|
+
function parsePropDecl(line, lineNumber, column, lineOffset, diagnostics) {
|
|
3669
|
+
const trimmed = line.trim();
|
|
3670
|
+
if (trimmed.includes(":") || trimmed.includes("<") || trimmed.includes("?")) {
|
|
3671
|
+
pushDiag(
|
|
3672
|
+
diagnostics,
|
|
3673
|
+
"COLLIE104",
|
|
3674
|
+
'Types are not supported in #props yet. Use "name" or "name()".',
|
|
3675
|
+
lineNumber,
|
|
3676
|
+
column,
|
|
3677
|
+
lineOffset,
|
|
3678
|
+
trimmed.length
|
|
3679
|
+
);
|
|
3680
|
+
return null;
|
|
3681
|
+
}
|
|
3682
|
+
const callableMatch = trimmed.match(/^([A-Za-z_$][A-Za-z0-9_$]*)\(\)$/);
|
|
3683
|
+
if (callableMatch) {
|
|
3684
|
+
const name = callableMatch[1];
|
|
3685
|
+
const nameStart = line.indexOf(name);
|
|
3686
|
+
const nameColumn = column + nameStart;
|
|
3687
|
+
return {
|
|
3688
|
+
name,
|
|
3689
|
+
kind: "callable",
|
|
3690
|
+
span: createSpan(lineNumber, nameColumn, name.length, lineOffset)
|
|
3691
|
+
};
|
|
3692
|
+
}
|
|
3693
|
+
const valueMatch = trimmed.match(/^([A-Za-z_$][A-Za-z0-9_$]*)$/);
|
|
3694
|
+
if (valueMatch) {
|
|
3695
|
+
const name = valueMatch[1];
|
|
3696
|
+
const nameStart = line.indexOf(name);
|
|
3697
|
+
const nameColumn = column + nameStart;
|
|
3698
|
+
return {
|
|
3699
|
+
name,
|
|
3700
|
+
kind: "value",
|
|
3701
|
+
span: createSpan(lineNumber, nameColumn, name.length, lineOffset)
|
|
3702
|
+
};
|
|
3703
|
+
}
|
|
3704
|
+
pushDiag(
|
|
3705
|
+
diagnostics,
|
|
3706
|
+
"COLLIE105",
|
|
3707
|
+
'Invalid #props declaration. Use "name" or "name()".',
|
|
3708
|
+
lineNumber,
|
|
3709
|
+
column,
|
|
3710
|
+
lineOffset,
|
|
3711
|
+
trimmed.length
|
|
3712
|
+
);
|
|
3713
|
+
return null;
|
|
3714
|
+
}
|
|
3322
3715
|
function pushDiag(diagnostics, code, message, line, column, lineOffset, length = 1) {
|
|
3323
3716
|
diagnostics.push({
|
|
3324
3717
|
severity: "error",
|