@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.cjs
CHANGED
|
@@ -49,340 +49,39 @@ __export(index_exports, {
|
|
|
49
49
|
});
|
|
50
50
|
module.exports = __toCommonJS(index_exports);
|
|
51
51
|
|
|
52
|
-
// src/
|
|
53
|
-
function
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
const functionLines = [
|
|
60
|
-
isTsx ? "export function render(props: any) {" : "export function render(props) {"
|
|
61
|
-
];
|
|
62
|
-
if (propsDestructure) {
|
|
63
|
-
functionLines.push(` ${propsDestructure}`);
|
|
64
|
-
}
|
|
65
|
-
functionLines.push(` return ${jsx};`, `}`);
|
|
66
|
-
parts.push(functionLines.join("\n"));
|
|
67
|
-
return parts.join("\n\n");
|
|
68
|
-
}
|
|
69
|
-
function buildModuleParts(root, options) {
|
|
70
|
-
const { jsxRuntime, flavor } = options;
|
|
71
|
-
const isTsx = flavor === "tsx";
|
|
72
|
-
const aliasEnv = buildClassAliasEnvironment(root.classAliases);
|
|
73
|
-
const jsx = renderRootChildren(root.children, aliasEnv);
|
|
74
|
-
const propsDestructure = emitPropsDestructure(root.props);
|
|
75
|
-
const prelude = [];
|
|
76
|
-
if (root.clientComponent) {
|
|
77
|
-
prelude.push(`"use client";`);
|
|
78
|
-
}
|
|
79
|
-
if (jsxRuntime === "classic" && templateUsesJsx(root)) {
|
|
80
|
-
prelude.push(`import React from "react";`);
|
|
81
|
-
}
|
|
82
|
-
const propsType = emitPropsType(root.props, flavor);
|
|
83
|
-
return { prelude, propsType, propsDestructure, jsx, isTsx };
|
|
84
|
-
}
|
|
85
|
-
function buildClassAliasEnvironment(decl) {
|
|
86
|
-
const env = /* @__PURE__ */ new Map();
|
|
87
|
-
if (!decl) {
|
|
88
|
-
return env;
|
|
89
|
-
}
|
|
90
|
-
for (const alias of decl.aliases) {
|
|
91
|
-
env.set(alias.name, alias.classes);
|
|
92
|
-
}
|
|
93
|
-
return env;
|
|
94
|
-
}
|
|
95
|
-
function renderRootChildren(children, aliasEnv) {
|
|
96
|
-
return emitNodesExpression(children, aliasEnv, /* @__PURE__ */ new Set());
|
|
97
|
-
}
|
|
98
|
-
function templateUsesJsx(root) {
|
|
99
|
-
if (root.children.length === 0) {
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
if (root.children.length > 1) {
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
-
return nodeUsesJsx(root.children[0]);
|
|
106
|
-
}
|
|
107
|
-
function nodeUsesJsx(node) {
|
|
108
|
-
if (node.type === "Element" || node.type === "Text" || node.type === "Component") {
|
|
109
|
-
return true;
|
|
110
|
-
}
|
|
111
|
-
if (node.type === "Expression" || node.type === "JSXPassthrough") {
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
if (node.type === "Conditional") {
|
|
115
|
-
return node.branches.some((branch) => branchUsesJsx(branch));
|
|
116
|
-
}
|
|
117
|
-
if (node.type === "For") {
|
|
118
|
-
return node.body.some((child) => nodeUsesJsx(child));
|
|
119
|
-
}
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
function branchUsesJsx(branch) {
|
|
123
|
-
if (!branch.body.length) {
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
return branch.body.some((child) => nodeUsesJsx(child));
|
|
127
|
-
}
|
|
128
|
-
function emitNodeInJsx(node, aliasEnv, locals) {
|
|
129
|
-
if (node.type === "Text") {
|
|
130
|
-
return emitText(node, locals);
|
|
131
|
-
}
|
|
132
|
-
if (node.type === "Expression") {
|
|
133
|
-
return `{${emitExpressionValue(node.value, locals)}}`;
|
|
134
|
-
}
|
|
135
|
-
if (node.type === "JSXPassthrough") {
|
|
136
|
-
return `{${emitJsxExpression(node.expression, locals)}}`;
|
|
137
|
-
}
|
|
138
|
-
if (node.type === "Conditional") {
|
|
139
|
-
return `{${emitConditionalExpression(node, aliasEnv, locals)}}`;
|
|
140
|
-
}
|
|
141
|
-
if (node.type === "For") {
|
|
142
|
-
return `{${emitForExpression(node, aliasEnv, locals)}}`;
|
|
143
|
-
}
|
|
144
|
-
if (node.type === "Component") {
|
|
145
|
-
return wrapWithGuard(emitComponent(node, aliasEnv, locals), node.guard, "jsx", locals);
|
|
146
|
-
}
|
|
147
|
-
return wrapWithGuard(emitElement(node, aliasEnv, locals), node.guard, "jsx", locals);
|
|
148
|
-
}
|
|
149
|
-
function emitElement(node, aliasEnv, locals) {
|
|
150
|
-
const expanded = expandClasses(node.classes, aliasEnv);
|
|
151
|
-
const classAttr = expanded.length ? ` className="${expanded.join(" ")}"` : "";
|
|
152
|
-
const attrs = emitAttributes(node.attributes, aliasEnv, locals);
|
|
153
|
-
const allAttrs = classAttr + attrs;
|
|
154
|
-
const children = emitChildrenWithSpacing(node.children, aliasEnv, locals);
|
|
155
|
-
if (children.length > 0) {
|
|
156
|
-
return `<${node.name}${allAttrs}>${children}</${node.name}>`;
|
|
157
|
-
} else {
|
|
158
|
-
return `<${node.name}${allAttrs} />`;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
function emitComponent(node, aliasEnv, locals) {
|
|
162
|
-
const attrs = emitAttributes(node.attributes, aliasEnv, locals);
|
|
163
|
-
const slotProps = emitSlotProps(node, aliasEnv, locals);
|
|
164
|
-
const allAttrs = `${attrs}${slotProps}`;
|
|
165
|
-
const children = emitChildrenWithSpacing(node.children, aliasEnv, locals);
|
|
166
|
-
if (children.length > 0) {
|
|
167
|
-
return `<${node.name}${allAttrs}>${children}</${node.name}>`;
|
|
168
|
-
} else {
|
|
169
|
-
return `<${node.name}${allAttrs} />`;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
function emitChildrenWithSpacing(children, aliasEnv, locals) {
|
|
173
|
-
if (children.length === 0) {
|
|
174
|
-
return "";
|
|
175
|
-
}
|
|
176
|
-
const parts = [];
|
|
177
|
-
for (let i = 0; i < children.length; i++) {
|
|
178
|
-
const child = children[i];
|
|
179
|
-
const emitted = emitNodeInJsx(child, aliasEnv, locals);
|
|
180
|
-
parts.push(emitted);
|
|
181
|
-
if (i < children.length - 1) {
|
|
182
|
-
const nextChild = children[i + 1];
|
|
183
|
-
const needsSpace = child.type === "Text" && (nextChild.type === "Element" || nextChild.type === "Component" || nextChild.type === "Expression" || nextChild.type === "JSXPassthrough");
|
|
184
|
-
if (needsSpace) {
|
|
185
|
-
parts.push(" ");
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
return parts.join("");
|
|
190
|
-
}
|
|
191
|
-
function emitAttributes(attributes, aliasEnv, locals) {
|
|
192
|
-
if (attributes.length === 0) {
|
|
193
|
-
return "";
|
|
194
|
-
}
|
|
195
|
-
return attributes.map((attr) => {
|
|
196
|
-
if (attr.value === null) {
|
|
197
|
-
return ` ${attr.name}`;
|
|
198
|
-
}
|
|
199
|
-
return ` ${attr.name}=${emitAttributeValue(attr.value, locals)}`;
|
|
200
|
-
}).join("");
|
|
201
|
-
}
|
|
202
|
-
function emitSlotProps(node, aliasEnv, locals) {
|
|
203
|
-
if (!node.slots || node.slots.length === 0) {
|
|
204
|
-
return "";
|
|
205
|
-
}
|
|
206
|
-
return node.slots.map((slot) => {
|
|
207
|
-
const expr = emitNodesExpression(slot.children, aliasEnv, locals);
|
|
208
|
-
return ` ${slot.name}={${expr}}`;
|
|
209
|
-
}).join("");
|
|
210
|
-
}
|
|
211
|
-
function wrapWithGuard(rendered, guard, context, locals) {
|
|
212
|
-
if (!guard) {
|
|
213
|
-
return rendered;
|
|
214
|
-
}
|
|
215
|
-
const condition = emitExpressionValue(guard, locals);
|
|
216
|
-
const expression = `(${condition}) && ${rendered}`;
|
|
217
|
-
return context === "jsx" ? `{${expression}}` : expression;
|
|
218
|
-
}
|
|
219
|
-
function emitForExpression(node, aliasEnv, locals) {
|
|
220
|
-
const arrayExpr = emitExpressionValue(node.arrayExpr, locals);
|
|
221
|
-
const nextLocals = new Set(locals);
|
|
222
|
-
nextLocals.add(node.itemName);
|
|
223
|
-
const body = emitNodesExpression(node.body, aliasEnv, nextLocals);
|
|
224
|
-
return `(${arrayExpr} ?? []).map((${node.itemName}) => ${body})`;
|
|
225
|
-
}
|
|
226
|
-
function expandClasses(classes, aliasEnv) {
|
|
227
|
-
const result = [];
|
|
228
|
-
for (const cls of classes) {
|
|
229
|
-
const match = cls.match(/^\$([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
230
|
-
if (!match) {
|
|
231
|
-
result.push(cls);
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
const aliasClasses = aliasEnv.get(match[1]);
|
|
235
|
-
if (!aliasClasses) {
|
|
236
|
-
continue;
|
|
52
|
+
// src/rewrite.ts
|
|
53
|
+
function createTemplateEnv(propsDecls) {
|
|
54
|
+
const propAliases = /* @__PURE__ */ new Map();
|
|
55
|
+
if (propsDecls) {
|
|
56
|
+
for (const decl of propsDecls) {
|
|
57
|
+
propAliases.set(decl.name, decl.kind);
|
|
237
58
|
}
|
|
238
|
-
result.push(...aliasClasses);
|
|
239
59
|
}
|
|
240
|
-
return
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
60
|
+
return {
|
|
61
|
+
propAliases,
|
|
62
|
+
localsStack: []
|
|
63
|
+
};
|
|
244
64
|
}
|
|
245
|
-
function
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
return rewriteJsxExpression(expression, locals);
|
|
249
|
-
}
|
|
250
|
-
return rewriteExpression(expression, locals);
|
|
65
|
+
function pushLocals(env, names) {
|
|
66
|
+
const locals = new Set(names);
|
|
67
|
+
env.localsStack.push(locals);
|
|
251
68
|
}
|
|
252
|
-
function
|
|
253
|
-
|
|
254
|
-
if (trimmed.startsWith('"') || trimmed.startsWith("'")) {
|
|
255
|
-
return value;
|
|
256
|
-
}
|
|
257
|
-
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
258
|
-
const inner = trimmed.slice(1, -1);
|
|
259
|
-
const rewritten = rewriteExpression(inner, locals);
|
|
260
|
-
return `{${rewritten}}`;
|
|
261
|
-
}
|
|
262
|
-
return rewriteExpression(trimmed, locals);
|
|
69
|
+
function popLocals(env) {
|
|
70
|
+
env.localsStack.pop();
|
|
263
71
|
}
|
|
264
|
-
function
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
return node.parts.map((part) => {
|
|
269
|
-
if (part.type === "text") {
|
|
270
|
-
return escapeText(part.value);
|
|
72
|
+
function isLocal(env, name) {
|
|
73
|
+
for (let i = env.localsStack.length - 1; i >= 0; i--) {
|
|
74
|
+
if (env.localsStack[i].has(name)) {
|
|
75
|
+
return true;
|
|
271
76
|
}
|
|
272
|
-
return `{${emitExpressionValue(part.value, locals)}}`;
|
|
273
|
-
}).join("");
|
|
274
|
-
}
|
|
275
|
-
function emitConditionalExpression(node, aliasEnv, locals) {
|
|
276
|
-
if (!node.branches.length) {
|
|
277
|
-
return "null";
|
|
278
|
-
}
|
|
279
|
-
const first = node.branches[0];
|
|
280
|
-
if (node.branches.length === 1 && first.test) {
|
|
281
|
-
const test = emitExpressionValue(first.test, locals);
|
|
282
|
-
return `(${test}) && ${emitBranchExpression(first, aliasEnv, locals)}`;
|
|
283
|
-
}
|
|
284
|
-
const hasElse = node.branches[node.branches.length - 1].test === void 0;
|
|
285
|
-
let fallback = hasElse ? emitBranchExpression(node.branches[node.branches.length - 1], aliasEnv, locals) : "null";
|
|
286
|
-
const startIndex = hasElse ? node.branches.length - 2 : node.branches.length - 1;
|
|
287
|
-
if (startIndex < 0) {
|
|
288
|
-
return fallback;
|
|
289
|
-
}
|
|
290
|
-
for (let i = startIndex; i >= 0; i--) {
|
|
291
|
-
const branch = node.branches[i];
|
|
292
|
-
const test = branch.test ? emitExpressionValue(branch.test, locals) : "false";
|
|
293
|
-
fallback = `(${test}) ? ${emitBranchExpression(branch, aliasEnv, locals)} : ${fallback}`;
|
|
294
|
-
}
|
|
295
|
-
return fallback;
|
|
296
|
-
}
|
|
297
|
-
function emitBranchExpression(branch, aliasEnv, locals) {
|
|
298
|
-
return emitNodesExpression(branch.body, aliasEnv, locals);
|
|
299
|
-
}
|
|
300
|
-
function emitNodesExpression(children, aliasEnv, locals) {
|
|
301
|
-
if (children.length === 0) {
|
|
302
|
-
return "null";
|
|
303
|
-
}
|
|
304
|
-
if (children.length === 1) {
|
|
305
|
-
return emitSingleNodeExpression(children[0], aliasEnv, locals);
|
|
306
|
-
}
|
|
307
|
-
return `<>${children.map((child) => emitNodeInJsx(child, aliasEnv, locals)).join("")}</>`;
|
|
308
|
-
}
|
|
309
|
-
function emitSingleNodeExpression(node, aliasEnv, locals) {
|
|
310
|
-
if (node.type === "Expression") {
|
|
311
|
-
return emitExpressionValue(node.value, locals);
|
|
312
|
-
}
|
|
313
|
-
if (node.type === "JSXPassthrough") {
|
|
314
|
-
return emitJsxExpression(node.expression, locals);
|
|
315
|
-
}
|
|
316
|
-
if (node.type === "Conditional") {
|
|
317
|
-
return emitConditionalExpression(node, aliasEnv, locals);
|
|
318
|
-
}
|
|
319
|
-
if (node.type === "For") {
|
|
320
|
-
return emitForExpression(node, aliasEnv, locals);
|
|
321
|
-
}
|
|
322
|
-
if (node.type === "Element") {
|
|
323
|
-
return wrapWithGuard(emitElement(node, aliasEnv, locals), node.guard, "expression", locals);
|
|
324
|
-
}
|
|
325
|
-
if (node.type === "Component") {
|
|
326
|
-
return wrapWithGuard(emitComponent(node, aliasEnv, locals), node.guard, "expression", locals);
|
|
327
|
-
}
|
|
328
|
-
if (node.type === "Text") {
|
|
329
|
-
return `<>${emitNodeInJsx(node, aliasEnv, locals)}</>`;
|
|
330
|
-
}
|
|
331
|
-
return emitNodeInJsx(node, aliasEnv, locals);
|
|
332
|
-
}
|
|
333
|
-
function emitPropsType(props, flavor) {
|
|
334
|
-
if (flavor === "tsx") {
|
|
335
|
-
return emitTsPropsType(props);
|
|
336
|
-
}
|
|
337
|
-
return emitJsDocPropsType(props);
|
|
338
|
-
}
|
|
339
|
-
function emitJsDocPropsType(props) {
|
|
340
|
-
if (!props) {
|
|
341
|
-
return "/** @typedef {any} Props */";
|
|
342
|
-
}
|
|
343
|
-
if (!props.fields.length) {
|
|
344
|
-
return "/** @typedef {{}} Props */";
|
|
345
|
-
}
|
|
346
|
-
const fields = props.fields.map((field) => {
|
|
347
|
-
const optional = field.optional ? "?" : "";
|
|
348
|
-
return `${field.name}${optional}: ${field.typeText}`;
|
|
349
|
-
}).join("; ");
|
|
350
|
-
return `/** @typedef {{ ${fields} }} Props */`;
|
|
351
|
-
}
|
|
352
|
-
function emitTsPropsType(props) {
|
|
353
|
-
if (!props || props.fields.length === 0) {
|
|
354
|
-
return "export type Props = Record<string, never>;";
|
|
355
77
|
}
|
|
356
|
-
|
|
357
|
-
const optional = field.optional ? "?" : "";
|
|
358
|
-
return ` ${field.name}${optional}: ${field.typeText};`;
|
|
359
|
-
});
|
|
360
|
-
return ["export interface Props {", ...lines, "}"].join("\n");
|
|
78
|
+
return false;
|
|
361
79
|
}
|
|
362
|
-
function
|
|
363
|
-
if (
|
|
364
|
-
return
|
|
80
|
+
function isPropAlias(env, name) {
|
|
81
|
+
if (isLocal(env, name)) {
|
|
82
|
+
return false;
|
|
365
83
|
}
|
|
366
|
-
|
|
367
|
-
return `const { ${names.join(", ")} } = props ?? {};`;
|
|
368
|
-
}
|
|
369
|
-
function escapeText(value) {
|
|
370
|
-
return value.replace(/[&<>{}]/g, (char) => {
|
|
371
|
-
switch (char) {
|
|
372
|
-
case "&":
|
|
373
|
-
return "&";
|
|
374
|
-
case "<":
|
|
375
|
-
return "<";
|
|
376
|
-
case ">":
|
|
377
|
-
return ">";
|
|
378
|
-
case "{":
|
|
379
|
-
return "{";
|
|
380
|
-
case "}":
|
|
381
|
-
return "}";
|
|
382
|
-
default:
|
|
383
|
-
return char;
|
|
384
|
-
}
|
|
385
|
-
});
|
|
84
|
+
return env.propAliases.has(name);
|
|
386
85
|
}
|
|
387
86
|
var IGNORED_IDENTIFIERS = /* @__PURE__ */ new Set([
|
|
388
87
|
"null",
|
|
@@ -435,13 +134,15 @@ var RESERVED_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
435
134
|
"with",
|
|
436
135
|
"yield"
|
|
437
136
|
]);
|
|
438
|
-
function
|
|
439
|
-
return `props?.${name}`;
|
|
440
|
-
}
|
|
441
|
-
function rewriteExpression(expression, locals) {
|
|
137
|
+
function rewriteExpression(expression, env) {
|
|
442
138
|
let i = 0;
|
|
443
139
|
let state = "code";
|
|
444
140
|
let output = "";
|
|
141
|
+
const usedBare = /* @__PURE__ */ new Set();
|
|
142
|
+
const usedPropsDot = /* @__PURE__ */ new Set();
|
|
143
|
+
const callSitesBare = /* @__PURE__ */ new Set();
|
|
144
|
+
const callSitesPropsDot = /* @__PURE__ */ new Set();
|
|
145
|
+
const rewrittenAliases = /* @__PURE__ */ new Set();
|
|
445
146
|
while (i < expression.length) {
|
|
446
147
|
const ch = expression[i];
|
|
447
148
|
if (state === "code") {
|
|
@@ -479,12 +180,37 @@ function rewriteExpression(expression, locals) {
|
|
|
479
180
|
const prevNonSpace = findPreviousNonSpace(expression, start - 1);
|
|
480
181
|
const nextNonSpace = findNextNonSpace(expression, i);
|
|
481
182
|
const isMemberAccess = prevNonSpace === ".";
|
|
482
|
-
const isObjectKey = nextNonSpace === ":";
|
|
483
|
-
|
|
183
|
+
const isObjectKey = nextNonSpace === ":" && (prevNonSpace === "{" || prevNonSpace === ",");
|
|
184
|
+
const isCall = nextNonSpace === "(";
|
|
185
|
+
if (prevNonSpace === "." && start >= 2) {
|
|
186
|
+
const propsStart = findPreviousIdentifierStart(expression, start - 2);
|
|
187
|
+
if (propsStart !== null) {
|
|
188
|
+
const possibleProps = expression.slice(propsStart, start - 1).trim();
|
|
189
|
+
if (possibleProps === "props") {
|
|
190
|
+
usedPropsDot.add(name);
|
|
191
|
+
if (isCall) {
|
|
192
|
+
callSitesPropsDot.add(name);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (isMemberAccess || isObjectKey || isLocal(env, name) || shouldIgnoreIdentifier(name)) {
|
|
484
198
|
output += name;
|
|
485
199
|
continue;
|
|
486
200
|
}
|
|
487
|
-
|
|
201
|
+
if (isPropAlias(env, name)) {
|
|
202
|
+
output += `props.${name}`;
|
|
203
|
+
rewrittenAliases.add(name);
|
|
204
|
+
if (isCall) {
|
|
205
|
+
callSitesBare.add(name);
|
|
206
|
+
}
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
usedBare.add(name);
|
|
210
|
+
if (isCall) {
|
|
211
|
+
callSitesBare.add(name);
|
|
212
|
+
}
|
|
213
|
+
output += name;
|
|
488
214
|
continue;
|
|
489
215
|
}
|
|
490
216
|
output += ch;
|
|
@@ -556,11 +282,16 @@ function rewriteExpression(expression, locals) {
|
|
|
556
282
|
continue;
|
|
557
283
|
}
|
|
558
284
|
}
|
|
559
|
-
return output;
|
|
285
|
+
return { code: output, usedBare, usedPropsDot, callSitesBare, callSitesPropsDot, rewrittenAliases };
|
|
560
286
|
}
|
|
561
|
-
function rewriteJsxExpression(expression,
|
|
287
|
+
function rewriteJsxExpression(expression, env) {
|
|
562
288
|
let output = "";
|
|
563
289
|
let i = 0;
|
|
290
|
+
const usedBare = /* @__PURE__ */ new Set();
|
|
291
|
+
const usedPropsDot = /* @__PURE__ */ new Set();
|
|
292
|
+
const callSitesBare = /* @__PURE__ */ new Set();
|
|
293
|
+
const callSitesPropsDot = /* @__PURE__ */ new Set();
|
|
294
|
+
const rewrittenAliases = /* @__PURE__ */ new Set();
|
|
564
295
|
while (i < expression.length) {
|
|
565
296
|
const ch = expression[i];
|
|
566
297
|
if (ch === "{") {
|
|
@@ -569,15 +300,20 @@ function rewriteJsxExpression(expression, locals) {
|
|
|
569
300
|
output += expression.slice(i);
|
|
570
301
|
break;
|
|
571
302
|
}
|
|
572
|
-
const
|
|
573
|
-
output += `{${
|
|
303
|
+
const result = rewriteExpression(braceResult.content, env);
|
|
304
|
+
output += `{${result.code}}`;
|
|
305
|
+
for (const name of result.usedBare) usedBare.add(name);
|
|
306
|
+
for (const name of result.usedPropsDot) usedPropsDot.add(name);
|
|
307
|
+
for (const name of result.callSitesBare) callSitesBare.add(name);
|
|
308
|
+
for (const name of result.callSitesPropsDot) callSitesPropsDot.add(name);
|
|
309
|
+
for (const name of result.rewrittenAliases) rewrittenAliases.add(name);
|
|
574
310
|
i = braceResult.endIndex + 1;
|
|
575
311
|
continue;
|
|
576
312
|
}
|
|
577
313
|
output += ch;
|
|
578
314
|
i++;
|
|
579
315
|
}
|
|
580
|
-
return output;
|
|
316
|
+
return { code: output, usedBare, usedPropsDot, callSitesBare, callSitesPropsDot, rewrittenAliases };
|
|
581
317
|
}
|
|
582
318
|
function readBalancedBraces(source, startIndex) {
|
|
583
319
|
let i = startIndex;
|
|
@@ -667,34 +403,385 @@ function readBalancedBraces(source, startIndex) {
|
|
|
667
403
|
continue;
|
|
668
404
|
}
|
|
669
405
|
}
|
|
670
|
-
return null;
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
function findPreviousNonSpace(text, index) {
|
|
409
|
+
for (let i = index; i >= 0; i--) {
|
|
410
|
+
const ch = text[i];
|
|
411
|
+
if (!/\s/.test(ch)) {
|
|
412
|
+
return ch;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
function findNextNonSpace(text, index) {
|
|
418
|
+
for (let i = index; i < text.length; i++) {
|
|
419
|
+
const ch = text[i];
|
|
420
|
+
if (!/\s/.test(ch)) {
|
|
421
|
+
return ch;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
function findPreviousIdentifierStart(text, index) {
|
|
427
|
+
let i = index;
|
|
428
|
+
while (i >= 0 && /\s/.test(text[i])) {
|
|
429
|
+
i--;
|
|
430
|
+
}
|
|
431
|
+
if (i < 0) return null;
|
|
432
|
+
if (!isIdentifierPart(text[i])) {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
while (i > 0 && isIdentifierPart(text[i - 1])) {
|
|
436
|
+
i--;
|
|
437
|
+
}
|
|
438
|
+
return i;
|
|
439
|
+
}
|
|
440
|
+
function isIdentifierStart(ch) {
|
|
441
|
+
return /[A-Za-z_$]/.test(ch);
|
|
442
|
+
}
|
|
443
|
+
function isIdentifierPart(ch) {
|
|
444
|
+
return /[A-Za-z0-9_$]/.test(ch);
|
|
445
|
+
}
|
|
446
|
+
function shouldIgnoreIdentifier(name) {
|
|
447
|
+
return IGNORED_IDENTIFIERS.has(name) || RESERVED_KEYWORDS.has(name);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/codegen.ts
|
|
451
|
+
function generateRenderModule(root, options) {
|
|
452
|
+
const { prelude, propsType, propsDestructure, jsx, isTsx } = buildModuleParts(root, options);
|
|
453
|
+
const parts = [...prelude, propsType];
|
|
454
|
+
if (!isTsx) {
|
|
455
|
+
parts.push(`/** @param {any} props */`);
|
|
456
|
+
}
|
|
457
|
+
const functionLines = [
|
|
458
|
+
isTsx ? "export function render(props: any) {" : "export function render(props) {"
|
|
459
|
+
];
|
|
460
|
+
if (propsDestructure) {
|
|
461
|
+
functionLines.push(` ${propsDestructure}`);
|
|
462
|
+
}
|
|
463
|
+
functionLines.push(` return ${jsx};`, `}`);
|
|
464
|
+
parts.push(functionLines.join("\n"));
|
|
465
|
+
return parts.join("\n\n");
|
|
466
|
+
}
|
|
467
|
+
function buildModuleParts(root, options) {
|
|
468
|
+
const { jsxRuntime, flavor } = options;
|
|
469
|
+
const isTsx = flavor === "tsx";
|
|
470
|
+
const aliasEnv = buildClassAliasEnvironment(root.classAliases);
|
|
471
|
+
const env = createTemplateEnv(root.propsDecls);
|
|
472
|
+
const jsx = renderRootChildren(root.children, aliasEnv, env);
|
|
473
|
+
const propsDestructure = emitPropsDestructure(root.props);
|
|
474
|
+
const prelude = [];
|
|
475
|
+
if (root.clientComponent) {
|
|
476
|
+
prelude.push(`"use client";`);
|
|
477
|
+
}
|
|
478
|
+
if (jsxRuntime === "classic" && templateUsesJsx(root)) {
|
|
479
|
+
prelude.push(`import React from "react";`);
|
|
480
|
+
}
|
|
481
|
+
const propsType = emitPropsType(root.props, flavor);
|
|
482
|
+
return { prelude, propsType, propsDestructure, jsx, isTsx };
|
|
483
|
+
}
|
|
484
|
+
function buildClassAliasEnvironment(decl) {
|
|
485
|
+
const env = /* @__PURE__ */ new Map();
|
|
486
|
+
if (!decl) {
|
|
487
|
+
return env;
|
|
488
|
+
}
|
|
489
|
+
for (const alias of decl.aliases) {
|
|
490
|
+
env.set(alias.name, alias.classes);
|
|
491
|
+
}
|
|
492
|
+
return env;
|
|
493
|
+
}
|
|
494
|
+
function renderRootChildren(children, aliasEnv, env) {
|
|
495
|
+
return emitNodesExpression(children, aliasEnv, env);
|
|
496
|
+
}
|
|
497
|
+
function templateUsesJsx(root) {
|
|
498
|
+
if (root.children.length === 0) {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
if (root.children.length > 1) {
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
return nodeUsesJsx(root.children[0]);
|
|
505
|
+
}
|
|
506
|
+
function nodeUsesJsx(node) {
|
|
507
|
+
if (node.type === "Element" || node.type === "Text" || node.type === "Component") {
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
if (node.type === "Expression" || node.type === "JSXPassthrough") {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
if (node.type === "Conditional") {
|
|
514
|
+
return node.branches.some((branch) => branchUsesJsx(branch));
|
|
515
|
+
}
|
|
516
|
+
if (node.type === "For") {
|
|
517
|
+
return node.body.some((child) => nodeUsesJsx(child));
|
|
518
|
+
}
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
function branchUsesJsx(branch) {
|
|
522
|
+
if (!branch.body.length) {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
return branch.body.some((child) => nodeUsesJsx(child));
|
|
526
|
+
}
|
|
527
|
+
function emitNodeInJsx(node, aliasEnv, env) {
|
|
528
|
+
if (node.type === "Text") {
|
|
529
|
+
return emitText(node, env);
|
|
530
|
+
}
|
|
531
|
+
if (node.type === "Expression") {
|
|
532
|
+
return `{${emitExpressionValue(node.value, env)}}`;
|
|
533
|
+
}
|
|
534
|
+
if (node.type === "JSXPassthrough") {
|
|
535
|
+
return `{${emitJsxExpression(node.expression, env)}}`;
|
|
536
|
+
}
|
|
537
|
+
if (node.type === "Conditional") {
|
|
538
|
+
return `{${emitConditionalExpression(node, aliasEnv, env)}}`;
|
|
539
|
+
}
|
|
540
|
+
if (node.type === "For") {
|
|
541
|
+
return `{${emitForExpression(node, aliasEnv, env)}}`;
|
|
542
|
+
}
|
|
543
|
+
if (node.type === "Component") {
|
|
544
|
+
return wrapWithGuard(emitComponent(node, aliasEnv, env), node.guard, "jsx", env);
|
|
545
|
+
}
|
|
546
|
+
return wrapWithGuard(emitElement(node, aliasEnv, env), node.guard, "jsx", env);
|
|
547
|
+
}
|
|
548
|
+
function emitElement(node, aliasEnv, env) {
|
|
549
|
+
const expanded = expandClasses(node.classes, aliasEnv);
|
|
550
|
+
const classAttr = expanded.length ? ` className="${expanded.join(" ")}"` : "";
|
|
551
|
+
const attrs = emitAttributes(node.attributes, aliasEnv, env);
|
|
552
|
+
const allAttrs = classAttr + attrs;
|
|
553
|
+
const children = emitChildrenWithSpacing(node.children, aliasEnv, env);
|
|
554
|
+
if (children.length > 0) {
|
|
555
|
+
return `<${node.name}${allAttrs}>${children}</${node.name}>`;
|
|
556
|
+
} else {
|
|
557
|
+
return `<${node.name}${allAttrs} />`;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function emitComponent(node, aliasEnv, env) {
|
|
561
|
+
const attrs = emitAttributes(node.attributes, aliasEnv, env);
|
|
562
|
+
const slotProps = emitSlotProps(node, aliasEnv, env);
|
|
563
|
+
const allAttrs = `${attrs}${slotProps}`;
|
|
564
|
+
const children = emitChildrenWithSpacing(node.children, aliasEnv, env);
|
|
565
|
+
if (children.length > 0) {
|
|
566
|
+
return `<${node.name}${allAttrs}>${children}</${node.name}>`;
|
|
567
|
+
} else {
|
|
568
|
+
return `<${node.name}${allAttrs} />`;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
function emitChildrenWithSpacing(children, aliasEnv, env) {
|
|
572
|
+
if (children.length === 0) {
|
|
573
|
+
return "";
|
|
574
|
+
}
|
|
575
|
+
const parts = [];
|
|
576
|
+
for (let i = 0; i < children.length; i++) {
|
|
577
|
+
const child = children[i];
|
|
578
|
+
const emitted = emitNodeInJsx(child, aliasEnv, env);
|
|
579
|
+
parts.push(emitted);
|
|
580
|
+
if (i < children.length - 1) {
|
|
581
|
+
const nextChild = children[i + 1];
|
|
582
|
+
const needsSpace = child.type === "Text" && (nextChild.type === "Element" || nextChild.type === "Component" || nextChild.type === "Expression" || nextChild.type === "JSXPassthrough");
|
|
583
|
+
if (needsSpace) {
|
|
584
|
+
parts.push(" ");
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return parts.join("");
|
|
589
|
+
}
|
|
590
|
+
function emitAttributes(attributes, aliasEnv, env) {
|
|
591
|
+
if (attributes.length === 0) {
|
|
592
|
+
return "";
|
|
593
|
+
}
|
|
594
|
+
return attributes.map((attr) => {
|
|
595
|
+
if (attr.value === null) {
|
|
596
|
+
return ` ${attr.name}`;
|
|
597
|
+
}
|
|
598
|
+
return ` ${attr.name}=${emitAttributeValue(attr.value, env)}`;
|
|
599
|
+
}).join("");
|
|
600
|
+
}
|
|
601
|
+
function emitSlotProps(node, aliasEnv, env) {
|
|
602
|
+
if (!node.slots || node.slots.length === 0) {
|
|
603
|
+
return "";
|
|
604
|
+
}
|
|
605
|
+
return node.slots.map((slot) => {
|
|
606
|
+
const expr = emitNodesExpression(slot.children, aliasEnv, env);
|
|
607
|
+
return ` ${slot.name}={${expr}}`;
|
|
608
|
+
}).join("");
|
|
609
|
+
}
|
|
610
|
+
function wrapWithGuard(rendered, guard, context, env) {
|
|
611
|
+
if (!guard) {
|
|
612
|
+
return rendered;
|
|
613
|
+
}
|
|
614
|
+
const condition = emitExpressionValue(guard, env);
|
|
615
|
+
const expression = `(${condition}) && ${rendered}`;
|
|
616
|
+
return context === "jsx" ? `{${expression}}` : expression;
|
|
617
|
+
}
|
|
618
|
+
function emitForExpression(node, aliasEnv, env) {
|
|
619
|
+
const arrayExpr = emitExpressionValue(node.arrayExpr, env);
|
|
620
|
+
pushLocals(env, [node.itemName]);
|
|
621
|
+
const body = emitNodesExpression(node.body, aliasEnv, env);
|
|
622
|
+
popLocals(env);
|
|
623
|
+
return `(${arrayExpr} ?? []).map((${node.itemName}) => ${body})`;
|
|
624
|
+
}
|
|
625
|
+
function expandClasses(classes, aliasEnv) {
|
|
626
|
+
const result = [];
|
|
627
|
+
for (const cls of classes) {
|
|
628
|
+
const match = cls.match(/^\$([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
629
|
+
if (!match) {
|
|
630
|
+
result.push(cls);
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
const aliasClasses = aliasEnv.get(match[1]);
|
|
634
|
+
if (!aliasClasses) {
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
result.push(...aliasClasses);
|
|
638
|
+
}
|
|
639
|
+
return result;
|
|
640
|
+
}
|
|
641
|
+
function emitExpressionValue(expression, env) {
|
|
642
|
+
return rewriteExpression(expression, env).code;
|
|
643
|
+
}
|
|
644
|
+
function emitJsxExpression(expression, env) {
|
|
645
|
+
const trimmed = expression.trimStart();
|
|
646
|
+
if (trimmed.startsWith("<")) {
|
|
647
|
+
return rewriteJsxExpression(expression, env).code;
|
|
648
|
+
}
|
|
649
|
+
return rewriteExpression(expression, env).code;
|
|
650
|
+
}
|
|
651
|
+
function emitAttributeValue(value, env) {
|
|
652
|
+
const trimmed = value.trim();
|
|
653
|
+
if (trimmed.startsWith('"') || trimmed.startsWith("'")) {
|
|
654
|
+
return value;
|
|
655
|
+
}
|
|
656
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
657
|
+
const inner = trimmed.slice(1, -1);
|
|
658
|
+
const rewritten = rewriteExpression(inner, env).code;
|
|
659
|
+
return `{${rewritten}}`;
|
|
660
|
+
}
|
|
661
|
+
return rewriteExpression(trimmed, env).code;
|
|
662
|
+
}
|
|
663
|
+
function emitText(node, env) {
|
|
664
|
+
if (!node.parts.length) {
|
|
665
|
+
return "";
|
|
666
|
+
}
|
|
667
|
+
return node.parts.map((part) => {
|
|
668
|
+
if (part.type === "text") {
|
|
669
|
+
return escapeText(part.value);
|
|
670
|
+
}
|
|
671
|
+
return `{${emitExpressionValue(part.value, env)}}`;
|
|
672
|
+
}).join("");
|
|
673
|
+
}
|
|
674
|
+
function emitConditionalExpression(node, aliasEnv, env) {
|
|
675
|
+
if (!node.branches.length) {
|
|
676
|
+
return "null";
|
|
677
|
+
}
|
|
678
|
+
const first = node.branches[0];
|
|
679
|
+
if (node.branches.length === 1 && first.test) {
|
|
680
|
+
const test = emitExpressionValue(first.test, env);
|
|
681
|
+
return `(${test}) && ${emitBranchExpression(first, aliasEnv, env)}`;
|
|
682
|
+
}
|
|
683
|
+
const hasElse = node.branches[node.branches.length - 1].test === void 0;
|
|
684
|
+
let fallback = hasElse ? emitBranchExpression(node.branches[node.branches.length - 1], aliasEnv, env) : "null";
|
|
685
|
+
const startIndex = hasElse ? node.branches.length - 2 : node.branches.length - 1;
|
|
686
|
+
if (startIndex < 0) {
|
|
687
|
+
return fallback;
|
|
688
|
+
}
|
|
689
|
+
for (let i = startIndex; i >= 0; i--) {
|
|
690
|
+
const branch = node.branches[i];
|
|
691
|
+
const test = branch.test ? emitExpressionValue(branch.test, env) : "false";
|
|
692
|
+
fallback = `(${test}) ? ${emitBranchExpression(branch, aliasEnv, env)} : ${fallback}`;
|
|
693
|
+
}
|
|
694
|
+
return fallback;
|
|
695
|
+
}
|
|
696
|
+
function emitBranchExpression(branch, aliasEnv, env) {
|
|
697
|
+
return emitNodesExpression(branch.body, aliasEnv, env);
|
|
698
|
+
}
|
|
699
|
+
function emitNodesExpression(children, aliasEnv, env) {
|
|
700
|
+
if (children.length === 0) {
|
|
701
|
+
return "null";
|
|
702
|
+
}
|
|
703
|
+
if (children.length === 1) {
|
|
704
|
+
return emitSingleNodeExpression(children[0], aliasEnv, env);
|
|
705
|
+
}
|
|
706
|
+
return `<>${children.map((child) => emitNodeInJsx(child, aliasEnv, env)).join("")}</>`;
|
|
671
707
|
}
|
|
672
|
-
function
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
if (!/\s/.test(ch)) {
|
|
676
|
-
return ch;
|
|
677
|
-
}
|
|
708
|
+
function emitSingleNodeExpression(node, aliasEnv, env) {
|
|
709
|
+
if (node.type === "Expression") {
|
|
710
|
+
return emitExpressionValue(node.value, env);
|
|
678
711
|
}
|
|
679
|
-
|
|
712
|
+
if (node.type === "JSXPassthrough") {
|
|
713
|
+
return emitJsxExpression(node.expression, env);
|
|
714
|
+
}
|
|
715
|
+
if (node.type === "Conditional") {
|
|
716
|
+
return emitConditionalExpression(node, aliasEnv, env);
|
|
717
|
+
}
|
|
718
|
+
if (node.type === "For") {
|
|
719
|
+
return emitForExpression(node, aliasEnv, env);
|
|
720
|
+
}
|
|
721
|
+
if (node.type === "Element") {
|
|
722
|
+
return wrapWithGuard(emitElement(node, aliasEnv, env), node.guard, "expression", env);
|
|
723
|
+
}
|
|
724
|
+
if (node.type === "Component") {
|
|
725
|
+
return wrapWithGuard(emitComponent(node, aliasEnv, env), node.guard, "expression", env);
|
|
726
|
+
}
|
|
727
|
+
if (node.type === "Text") {
|
|
728
|
+
return `<>${emitNodeInJsx(node, aliasEnv, env)}</>`;
|
|
729
|
+
}
|
|
730
|
+
return emitNodeInJsx(node, aliasEnv, env);
|
|
680
731
|
}
|
|
681
|
-
function
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
if (!/\s/.test(ch)) {
|
|
685
|
-
return ch;
|
|
686
|
-
}
|
|
732
|
+
function emitPropsType(props, flavor) {
|
|
733
|
+
if (flavor === "tsx") {
|
|
734
|
+
return emitTsPropsType(props);
|
|
687
735
|
}
|
|
688
|
-
return
|
|
736
|
+
return emitJsDocPropsType(props);
|
|
689
737
|
}
|
|
690
|
-
function
|
|
691
|
-
|
|
738
|
+
function emitJsDocPropsType(props) {
|
|
739
|
+
if (!props) {
|
|
740
|
+
return "/** @typedef {any} Props */";
|
|
741
|
+
}
|
|
742
|
+
if (!props.fields.length) {
|
|
743
|
+
return "/** @typedef {{}} Props */";
|
|
744
|
+
}
|
|
745
|
+
const fields = props.fields.map((field) => {
|
|
746
|
+
const optional = field.optional ? "?" : "";
|
|
747
|
+
return `${field.name}${optional}: ${field.typeText}`;
|
|
748
|
+
}).join("; ");
|
|
749
|
+
return `/** @typedef {{ ${fields} }} Props */`;
|
|
692
750
|
}
|
|
693
|
-
function
|
|
694
|
-
|
|
751
|
+
function emitTsPropsType(props) {
|
|
752
|
+
if (!props || props.fields.length === 0) {
|
|
753
|
+
return "export type Props = Record<string, never>;";
|
|
754
|
+
}
|
|
755
|
+
const lines = props.fields.map((field) => {
|
|
756
|
+
const optional = field.optional ? "?" : "";
|
|
757
|
+
return ` ${field.name}${optional}: ${field.typeText};`;
|
|
758
|
+
});
|
|
759
|
+
return ["export interface Props {", ...lines, "}"].join("\n");
|
|
695
760
|
}
|
|
696
|
-
function
|
|
697
|
-
|
|
761
|
+
function emitPropsDestructure(props) {
|
|
762
|
+
if (!props || props.fields.length === 0) {
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
const names = props.fields.map((field) => field.name);
|
|
766
|
+
return `const { ${names.join(", ")} } = props ?? {};`;
|
|
767
|
+
}
|
|
768
|
+
function escapeText(value) {
|
|
769
|
+
return value.replace(/[&<>{}]/g, (char) => {
|
|
770
|
+
switch (char) {
|
|
771
|
+
case "&":
|
|
772
|
+
return "&";
|
|
773
|
+
case "<":
|
|
774
|
+
return "<";
|
|
775
|
+
case ">":
|
|
776
|
+
return ">";
|
|
777
|
+
case "{":
|
|
778
|
+
return "{";
|
|
779
|
+
case "}":
|
|
780
|
+
return "}";
|
|
781
|
+
default:
|
|
782
|
+
return char;
|
|
783
|
+
}
|
|
784
|
+
});
|
|
698
785
|
}
|
|
699
786
|
|
|
700
787
|
// src/html-codegen.ts
|
|
@@ -1490,6 +1577,185 @@ function offsetSpan(base, index, length) {
|
|
|
1490
1577
|
}
|
|
1491
1578
|
};
|
|
1492
1579
|
}
|
|
1580
|
+
function enforcePropAliases(root) {
|
|
1581
|
+
if (!root.propsDecls || root.propsDecls.length === 0) {
|
|
1582
|
+
return [];
|
|
1583
|
+
}
|
|
1584
|
+
const diagnostics = [];
|
|
1585
|
+
const declaredProps = new Map(root.propsDecls.map((d) => [d.name, d]));
|
|
1586
|
+
const allUsage = collectTemplateUsage(root);
|
|
1587
|
+
for (const name of allUsage.usedBare) {
|
|
1588
|
+
if (!declaredProps.has(name) && !shouldIgnoreForDiagnostics(name)) {
|
|
1589
|
+
diagnostics.push({
|
|
1590
|
+
severity: "warning",
|
|
1591
|
+
code: "props.missingDeclaration",
|
|
1592
|
+
message: `Identifier "${name}" is used without "props." but is not declared in #props. Declare "${name}" in #props or use "props.${name}".`
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
for (const [name, decl] of declaredProps) {
|
|
1597
|
+
const usedAsBare = allUsage.usedBareAliases.has(name);
|
|
1598
|
+
const usedAsProps = allUsage.usedPropsDot.has(name);
|
|
1599
|
+
if (!usedAsBare && !usedAsProps) {
|
|
1600
|
+
diagnostics.push({
|
|
1601
|
+
severity: "warning",
|
|
1602
|
+
code: "props.unusedDeclaration",
|
|
1603
|
+
message: `Prop "${name}" is declared in #props but never used in this template.`,
|
|
1604
|
+
span: decl.span
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
for (const name of allUsage.usedPropsDot) {
|
|
1609
|
+
if (declaredProps.has(name)) {
|
|
1610
|
+
diagnostics.push({
|
|
1611
|
+
severity: "warning",
|
|
1612
|
+
code: "props.style.nonPreferred",
|
|
1613
|
+
message: `"props.${name}" is unnecessary because "${name}" is declared in #props. Use "{${name}}" instead.`
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
for (const [name, decl] of declaredProps) {
|
|
1618
|
+
const isCallable = decl.kind === "callable";
|
|
1619
|
+
const usedAsCall = allUsage.callSitesBare.has(name);
|
|
1620
|
+
const usedAsValue = allUsage.usedBareAliases.has(name) && !usedAsCall;
|
|
1621
|
+
if (isCallable && usedAsValue) {
|
|
1622
|
+
diagnostics.push({
|
|
1623
|
+
severity: "warning",
|
|
1624
|
+
code: "props.style.nonPreferred",
|
|
1625
|
+
message: `"${name}" is declared as callable in #props (${name}()) but used as a value.`
|
|
1626
|
+
});
|
|
1627
|
+
} else if (!isCallable && usedAsCall) {
|
|
1628
|
+
diagnostics.push({
|
|
1629
|
+
severity: "warning",
|
|
1630
|
+
code: "props.style.nonPreferred",
|
|
1631
|
+
message: `"${name}" is declared as a value in #props but used as a function call.`
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
return diagnostics;
|
|
1636
|
+
}
|
|
1637
|
+
function collectTemplateUsage(root) {
|
|
1638
|
+
const usage = {
|
|
1639
|
+
usedBare: /* @__PURE__ */ new Set(),
|
|
1640
|
+
usedBareAliases: /* @__PURE__ */ new Set(),
|
|
1641
|
+
usedPropsDot: /* @__PURE__ */ new Set(),
|
|
1642
|
+
callSitesBare: /* @__PURE__ */ new Set(),
|
|
1643
|
+
callSitesPropsDot: /* @__PURE__ */ new Set()
|
|
1644
|
+
};
|
|
1645
|
+
const env = createTemplateEnv(root.propsDecls);
|
|
1646
|
+
function mergeResult(result) {
|
|
1647
|
+
for (const name of result.usedBare) {
|
|
1648
|
+
usage.usedBare.add(name);
|
|
1649
|
+
}
|
|
1650
|
+
for (const name of result.rewrittenAliases) {
|
|
1651
|
+
usage.usedBareAliases.add(name);
|
|
1652
|
+
}
|
|
1653
|
+
for (const name of result.usedPropsDot) {
|
|
1654
|
+
usage.usedPropsDot.add(name);
|
|
1655
|
+
}
|
|
1656
|
+
for (const name of result.callSitesBare) {
|
|
1657
|
+
usage.callSitesBare.add(name);
|
|
1658
|
+
}
|
|
1659
|
+
for (const name of result.callSitesPropsDot) {
|
|
1660
|
+
usage.callSitesPropsDot.add(name);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
function analyzeExpression(expr) {
|
|
1664
|
+
if (!expr) return;
|
|
1665
|
+
const result = rewriteExpression(expr, env);
|
|
1666
|
+
mergeResult(result);
|
|
1667
|
+
}
|
|
1668
|
+
function analyzeJsxExpression(expr) {
|
|
1669
|
+
if (!expr) return;
|
|
1670
|
+
const result = rewriteJsxExpression(expr, env);
|
|
1671
|
+
mergeResult(result);
|
|
1672
|
+
}
|
|
1673
|
+
function walkNode(node) {
|
|
1674
|
+
switch (node.type) {
|
|
1675
|
+
case "Text":
|
|
1676
|
+
for (const part of node.parts) {
|
|
1677
|
+
if (part.type === "expr") {
|
|
1678
|
+
analyzeExpression(part.value);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
break;
|
|
1682
|
+
case "Expression":
|
|
1683
|
+
analyzeExpression(node.value);
|
|
1684
|
+
break;
|
|
1685
|
+
case "JSXPassthrough":
|
|
1686
|
+
analyzeJsxExpression(node.expression);
|
|
1687
|
+
break;
|
|
1688
|
+
case "Element":
|
|
1689
|
+
if (node.guard) {
|
|
1690
|
+
analyzeExpression(node.guard);
|
|
1691
|
+
}
|
|
1692
|
+
for (const attr of node.attributes) {
|
|
1693
|
+
if (attr.value) {
|
|
1694
|
+
analyzeAttributeValue(attr.value);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
for (const child of node.children) {
|
|
1698
|
+
walkNode(child);
|
|
1699
|
+
}
|
|
1700
|
+
break;
|
|
1701
|
+
case "Component":
|
|
1702
|
+
if (node.guard) {
|
|
1703
|
+
analyzeExpression(node.guard);
|
|
1704
|
+
}
|
|
1705
|
+
for (const attr of node.attributes) {
|
|
1706
|
+
if (attr.value) {
|
|
1707
|
+
analyzeAttributeValue(attr.value);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
if (node.slots) {
|
|
1711
|
+
for (const slot of node.slots) {
|
|
1712
|
+
for (const child of slot.children) {
|
|
1713
|
+
walkNode(child);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
for (const child of node.children) {
|
|
1718
|
+
walkNode(child);
|
|
1719
|
+
}
|
|
1720
|
+
break;
|
|
1721
|
+
case "Conditional":
|
|
1722
|
+
for (const branch of node.branches) {
|
|
1723
|
+
if (branch.test) {
|
|
1724
|
+
analyzeExpression(branch.test);
|
|
1725
|
+
}
|
|
1726
|
+
for (const child of branch.body) {
|
|
1727
|
+
walkNode(child);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
break;
|
|
1731
|
+
case "For":
|
|
1732
|
+
analyzeExpression(node.arrayExpr);
|
|
1733
|
+
for (const child of node.body) {
|
|
1734
|
+
walkNode(child);
|
|
1735
|
+
}
|
|
1736
|
+
break;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
function analyzeAttributeValue(value) {
|
|
1740
|
+
const trimmed = value.trim();
|
|
1741
|
+
if (trimmed.startsWith('"') || trimmed.startsWith("'")) {
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
1745
|
+
const inner = trimmed.slice(1, -1);
|
|
1746
|
+
analyzeExpression(inner);
|
|
1747
|
+
} else {
|
|
1748
|
+
analyzeExpression(trimmed);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
for (const child of root.children) {
|
|
1752
|
+
walkNode(child);
|
|
1753
|
+
}
|
|
1754
|
+
return usage;
|
|
1755
|
+
}
|
|
1756
|
+
function shouldIgnoreForDiagnostics(name) {
|
|
1757
|
+
return IGNORED_IDENTIFIERS2.has(name) || RESERVED_KEYWORDS2.has(name);
|
|
1758
|
+
}
|
|
1493
1759
|
|
|
1494
1760
|
// src/parser.ts
|
|
1495
1761
|
var TEMPLATE_ID_PATTERN = /^[A-Za-z][A-Za-z0-9._-]*$/;
|
|
@@ -1806,6 +2072,7 @@ function parseTemplateBlock(lines, lineOffsets, startIndex, endIndex, options) {
|
|
|
1806
2072
|
);
|
|
1807
2073
|
} else {
|
|
1808
2074
|
root.props = { fields: [] };
|
|
2075
|
+
root.propsDecls = [];
|
|
1809
2076
|
}
|
|
1810
2077
|
if (level === 0) {
|
|
1811
2078
|
propsBlockLevel = level;
|
|
@@ -1860,6 +2127,23 @@ function parseTemplateBlock(lines, lineOffsets, startIndex, endIndex, options) {
|
|
|
1860
2127
|
);
|
|
1861
2128
|
continue;
|
|
1862
2129
|
}
|
|
2130
|
+
const decl = parsePropDecl(lineContent, lineNumber, indent + 1, lineOffset, diagnostics);
|
|
2131
|
+
if (decl && root.propsDecls) {
|
|
2132
|
+
const existing = root.propsDecls.find((d) => d.name === decl.name);
|
|
2133
|
+
if (existing) {
|
|
2134
|
+
pushDiag(
|
|
2135
|
+
diagnostics,
|
|
2136
|
+
"COLLIE106",
|
|
2137
|
+
`Duplicate prop declaration "${decl.name}".`,
|
|
2138
|
+
lineNumber,
|
|
2139
|
+
indent + 1,
|
|
2140
|
+
lineOffset,
|
|
2141
|
+
trimmed.length
|
|
2142
|
+
);
|
|
2143
|
+
} else {
|
|
2144
|
+
root.propsDecls.push(decl);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
1863
2147
|
continue;
|
|
1864
2148
|
}
|
|
1865
2149
|
if (classesBlockLevel !== null && level > classesBlockLevel) {
|
|
@@ -2272,6 +2556,7 @@ function parseTemplateBlock(lines, lineOffsets, startIndex, endIndex, options) {
|
|
|
2272
2556
|
diagnostics.push(...enforceDialect(root, options.dialect));
|
|
2273
2557
|
diagnostics.push(...enforceProps(root, options.dialect.props));
|
|
2274
2558
|
}
|
|
2559
|
+
diagnostics.push(...enforcePropAliases(root));
|
|
2275
2560
|
return { root, diagnostics };
|
|
2276
2561
|
}
|
|
2277
2562
|
function buildLineOffsets(lines) {
|
|
@@ -2322,7 +2607,7 @@ function parseConditionalHeader(kind, lineContent, lineNumber, column, lineOffse
|
|
|
2322
2607
|
pushDiag(
|
|
2323
2608
|
diagnostics,
|
|
2324
2609
|
"COLLIE201",
|
|
2325
|
-
kind === "if" ? "Invalid @if syntax. Use @if
|
|
2610
|
+
kind === "if" ? "Invalid @if syntax. Use @if (condition)." : "Invalid @elseIf syntax. Use @elseIf (condition).",
|
|
2326
2611
|
lineNumber,
|
|
2327
2612
|
column,
|
|
2328
2613
|
lineOffset,
|
|
@@ -2346,29 +2631,36 @@ function parseConditionalHeader(kind, lineContent, lineNumber, column, lineOffse
|
|
|
2346
2631
|
}
|
|
2347
2632
|
const remainderTrimmed = remainder.trimStart();
|
|
2348
2633
|
const usesParens = remainderTrimmed.startsWith("(");
|
|
2634
|
+
if (!usesParens) {
|
|
2635
|
+
pushDiag(
|
|
2636
|
+
diagnostics,
|
|
2637
|
+
"COLLIE211",
|
|
2638
|
+
kind === "if" ? "@if requires parentheses: @if (condition)" : "@elseIf requires parentheses: @elseIf (condition)",
|
|
2639
|
+
lineNumber,
|
|
2640
|
+
column,
|
|
2641
|
+
lineOffset,
|
|
2642
|
+
trimmed.length || token.length
|
|
2643
|
+
);
|
|
2644
|
+
return null;
|
|
2645
|
+
}
|
|
2349
2646
|
let testRaw = "";
|
|
2350
2647
|
let remainderRaw = "";
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
return null;
|
|
2365
|
-
}
|
|
2366
|
-
testRaw = trimmed.slice(openIndex + 1, closeIndex);
|
|
2367
|
-
remainderRaw = trimmed.slice(closeIndex + 1);
|
|
2368
|
-
} else {
|
|
2369
|
-
testRaw = remainderTrimmed;
|
|
2370
|
-
remainderRaw = "";
|
|
2648
|
+
const openIndex = trimmed.indexOf("(", token.length);
|
|
2649
|
+
const closeIndex = trimmed.lastIndexOf(")");
|
|
2650
|
+
if (openIndex === -1 || closeIndex <= openIndex) {
|
|
2651
|
+
pushDiag(
|
|
2652
|
+
diagnostics,
|
|
2653
|
+
"COLLIE212",
|
|
2654
|
+
kind === "if" ? "Unclosed parentheses in @if ( ... )" : "Unclosed parentheses in @elseIf ( ... )",
|
|
2655
|
+
lineNumber,
|
|
2656
|
+
column,
|
|
2657
|
+
lineOffset,
|
|
2658
|
+
trimmed.length || token.length
|
|
2659
|
+
);
|
|
2660
|
+
return null;
|
|
2371
2661
|
}
|
|
2662
|
+
testRaw = trimmed.slice(openIndex + 1, closeIndex);
|
|
2663
|
+
remainderRaw = trimmed.slice(closeIndex + 1);
|
|
2372
2664
|
const test = testRaw.trim();
|
|
2373
2665
|
if (!test) {
|
|
2374
2666
|
pushDiag(
|
|
@@ -2417,7 +2709,20 @@ function parseElseHeader(lineContent, lineNumber, column, lineOffset, diagnostic
|
|
|
2417
2709
|
const token = "@else";
|
|
2418
2710
|
const tokenSpan = createSpan(lineNumber, column, token.length, lineOffset);
|
|
2419
2711
|
const remainderRaw = match[1] ?? "";
|
|
2420
|
-
const
|
|
2712
|
+
const remainderTrimmed = remainderRaw.trim();
|
|
2713
|
+
if (remainderTrimmed.startsWith("(")) {
|
|
2714
|
+
pushDiag(
|
|
2715
|
+
diagnostics,
|
|
2716
|
+
"COLLIE213",
|
|
2717
|
+
"@else does not accept a condition",
|
|
2718
|
+
lineNumber,
|
|
2719
|
+
column,
|
|
2720
|
+
lineOffset,
|
|
2721
|
+
trimmed.length || 4
|
|
2722
|
+
);
|
|
2723
|
+
return null;
|
|
2724
|
+
}
|
|
2725
|
+
const inlineBody = remainderTrimmed;
|
|
2421
2726
|
const remainderOffset = trimmed.length - remainderRaw.length;
|
|
2422
2727
|
const leadingWhitespace = remainderRaw.length - inlineBody.length;
|
|
2423
2728
|
const inlineColumn = inlineBody.length > 0 ? column + remainderOffset + leadingWhitespace : void 0;
|
|
@@ -2431,12 +2736,12 @@ function parseElseHeader(lineContent, lineNumber, column, lineOffset, diagnostic
|
|
|
2431
2736
|
}
|
|
2432
2737
|
function parseForHeader(lineContent, lineNumber, column, lineOffset, diagnostics) {
|
|
2433
2738
|
const trimmed = lineContent.trimEnd();
|
|
2434
|
-
const
|
|
2435
|
-
if (!
|
|
2739
|
+
const token = "@for";
|
|
2740
|
+
if (!trimmed.startsWith(token)) {
|
|
2436
2741
|
pushDiag(
|
|
2437
2742
|
diagnostics,
|
|
2438
2743
|
"COLLIE210",
|
|
2439
|
-
"Invalid @for syntax. Use @for
|
|
2744
|
+
"Invalid @for syntax. Use @for (item in array).",
|
|
2440
2745
|
lineNumber,
|
|
2441
2746
|
column,
|
|
2442
2747
|
lineOffset,
|
|
@@ -2444,15 +2749,55 @@ function parseForHeader(lineContent, lineNumber, column, lineOffset, diagnostics
|
|
|
2444
2749
|
);
|
|
2445
2750
|
return null;
|
|
2446
2751
|
}
|
|
2447
|
-
const token = "@for";
|
|
2448
2752
|
const tokenSpan = createSpan(lineNumber, column, token.length, lineOffset);
|
|
2753
|
+
const remainder = trimmed.slice(token.length).trimStart();
|
|
2754
|
+
if (!remainder.startsWith("(")) {
|
|
2755
|
+
pushDiag(
|
|
2756
|
+
diagnostics,
|
|
2757
|
+
"COLLIE211",
|
|
2758
|
+
"@for requires parentheses: @for (item in array)",
|
|
2759
|
+
lineNumber,
|
|
2760
|
+
column,
|
|
2761
|
+
lineOffset,
|
|
2762
|
+
trimmed.length || 4
|
|
2763
|
+
);
|
|
2764
|
+
return null;
|
|
2765
|
+
}
|
|
2766
|
+
const openIndex = trimmed.indexOf("(", token.length);
|
|
2767
|
+
const closeIndex = trimmed.lastIndexOf(")");
|
|
2768
|
+
if (openIndex === -1 || closeIndex <= openIndex) {
|
|
2769
|
+
pushDiag(
|
|
2770
|
+
diagnostics,
|
|
2771
|
+
"COLLIE212",
|
|
2772
|
+
"Unclosed parentheses in @for ( ... )",
|
|
2773
|
+
lineNumber,
|
|
2774
|
+
column,
|
|
2775
|
+
lineOffset,
|
|
2776
|
+
trimmed.length || 4
|
|
2777
|
+
);
|
|
2778
|
+
return null;
|
|
2779
|
+
}
|
|
2780
|
+
const content = trimmed.slice(openIndex + 1, closeIndex).trim();
|
|
2781
|
+
const match = content.match(/^([A-Za-z_][A-Za-z0-9_]*)\s+in\s+(.+)$/);
|
|
2782
|
+
if (!match) {
|
|
2783
|
+
pushDiag(
|
|
2784
|
+
diagnostics,
|
|
2785
|
+
"COLLIE210",
|
|
2786
|
+
"Invalid @for syntax. Use @for (item in array).",
|
|
2787
|
+
lineNumber,
|
|
2788
|
+
column,
|
|
2789
|
+
lineOffset,
|
|
2790
|
+
trimmed.length || 4
|
|
2791
|
+
);
|
|
2792
|
+
return null;
|
|
2793
|
+
}
|
|
2449
2794
|
const itemName = match[1];
|
|
2450
2795
|
const arrayExprRaw = match[2];
|
|
2451
2796
|
if (!itemName || !arrayExprRaw) {
|
|
2452
2797
|
pushDiag(
|
|
2453
2798
|
diagnostics,
|
|
2454
2799
|
"COLLIE210",
|
|
2455
|
-
"Invalid @for syntax. Use @for
|
|
2800
|
+
"Invalid @for syntax. Use @for (item in array).",
|
|
2456
2801
|
lineNumber,
|
|
2457
2802
|
column,
|
|
2458
2803
|
lineOffset,
|
|
@@ -2474,8 +2819,9 @@ function parseForHeader(lineContent, lineNumber, column, lineOffset, diagnostics
|
|
|
2474
2819
|
return null;
|
|
2475
2820
|
}
|
|
2476
2821
|
const arrayExprLeadingWhitespace = arrayExprRaw.length - arrayExprRaw.trimStart().length;
|
|
2477
|
-
const
|
|
2478
|
-
const
|
|
2822
|
+
const contentStart = openIndex + 1;
|
|
2823
|
+
const arrayExprStartInContent = content.length - arrayExprRaw.length;
|
|
2824
|
+
const arrayExprColumn = column + contentStart + arrayExprStartInContent + arrayExprLeadingWhitespace;
|
|
2479
2825
|
const arrayExprSpan = createSpan(lineNumber, arrayExprColumn, arrayExpr.length, lineOffset);
|
|
2480
2826
|
return { itemName, arrayExpr, token, tokenSpan, arrayExprSpan };
|
|
2481
2827
|
}
|
|
@@ -3370,6 +3716,53 @@ function parseAndAddAttribute(attrStr, attributes, diagnostics, lineNumber, colu
|
|
|
3370
3716
|
}
|
|
3371
3717
|
}
|
|
3372
3718
|
}
|
|
3719
|
+
function parsePropDecl(line, lineNumber, column, lineOffset, diagnostics) {
|
|
3720
|
+
const trimmed = line.trim();
|
|
3721
|
+
if (trimmed.includes(":") || trimmed.includes("<") || trimmed.includes("?")) {
|
|
3722
|
+
pushDiag(
|
|
3723
|
+
diagnostics,
|
|
3724
|
+
"COLLIE104",
|
|
3725
|
+
'Types are not supported in #props yet. Use "name" or "name()".',
|
|
3726
|
+
lineNumber,
|
|
3727
|
+
column,
|
|
3728
|
+
lineOffset,
|
|
3729
|
+
trimmed.length
|
|
3730
|
+
);
|
|
3731
|
+
return null;
|
|
3732
|
+
}
|
|
3733
|
+
const callableMatch = trimmed.match(/^([A-Za-z_$][A-Za-z0-9_$]*)\(\)$/);
|
|
3734
|
+
if (callableMatch) {
|
|
3735
|
+
const name = callableMatch[1];
|
|
3736
|
+
const nameStart = line.indexOf(name);
|
|
3737
|
+
const nameColumn = column + nameStart;
|
|
3738
|
+
return {
|
|
3739
|
+
name,
|
|
3740
|
+
kind: "callable",
|
|
3741
|
+
span: createSpan(lineNumber, nameColumn, name.length, lineOffset)
|
|
3742
|
+
};
|
|
3743
|
+
}
|
|
3744
|
+
const valueMatch = trimmed.match(/^([A-Za-z_$][A-Za-z0-9_$]*)$/);
|
|
3745
|
+
if (valueMatch) {
|
|
3746
|
+
const name = valueMatch[1];
|
|
3747
|
+
const nameStart = line.indexOf(name);
|
|
3748
|
+
const nameColumn = column + nameStart;
|
|
3749
|
+
return {
|
|
3750
|
+
name,
|
|
3751
|
+
kind: "value",
|
|
3752
|
+
span: createSpan(lineNumber, nameColumn, name.length, lineOffset)
|
|
3753
|
+
};
|
|
3754
|
+
}
|
|
3755
|
+
pushDiag(
|
|
3756
|
+
diagnostics,
|
|
3757
|
+
"COLLIE105",
|
|
3758
|
+
'Invalid #props declaration. Use "name" or "name()".',
|
|
3759
|
+
lineNumber,
|
|
3760
|
+
column,
|
|
3761
|
+
lineOffset,
|
|
3762
|
+
trimmed.length
|
|
3763
|
+
);
|
|
3764
|
+
return null;
|
|
3765
|
+
}
|
|
3373
3766
|
function pushDiag(diagnostics, code, message, line, column, lineOffset, length = 1) {
|
|
3374
3767
|
diagnostics.push({
|
|
3375
3768
|
severity: "error",
|