@vertz/ui-compiler 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/index.d.ts +564 -0
- package/dist/index.js +3139 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3139 @@
|
|
|
1
|
+
// src/analyzers/component-analyzer.ts
|
|
2
|
+
import {
|
|
3
|
+
SyntaxKind
|
|
4
|
+
} from "ts-morph";
|
|
5
|
+
|
|
6
|
+
class ComponentAnalyzer {
|
|
7
|
+
analyze(sourceFile) {
|
|
8
|
+
const components = [];
|
|
9
|
+
for (const fn of sourceFile.getFunctions()) {
|
|
10
|
+
if (this._returnsJsx(fn)) {
|
|
11
|
+
components.push(this._fromFunctionDeclaration(fn));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
for (const stmt of sourceFile.getVariableStatements()) {
|
|
15
|
+
for (const decl of stmt.getDeclarationList().getDeclarations()) {
|
|
16
|
+
const init = decl.getInitializer();
|
|
17
|
+
if (!init)
|
|
18
|
+
continue;
|
|
19
|
+
if (init.isKind(SyntaxKind.ArrowFunction) || init.isKind(SyntaxKind.FunctionExpression)) {
|
|
20
|
+
if (this._returnsJsx(init)) {
|
|
21
|
+
components.push(this._fromVariableDeclaration(decl, init));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return components;
|
|
27
|
+
}
|
|
28
|
+
_returnsJsx(node) {
|
|
29
|
+
if (node.isKind(SyntaxKind.ArrowFunction)) {
|
|
30
|
+
const body2 = node.getBody();
|
|
31
|
+
if (this._containsJsx(body2))
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
const body = node.isKind(SyntaxKind.FunctionDeclaration) || node.isKind(SyntaxKind.FunctionExpression) ? node.getBody() : node;
|
|
35
|
+
if (body && this._containsJsx(body))
|
|
36
|
+
return true;
|
|
37
|
+
if (this._containsJsx(node))
|
|
38
|
+
return true;
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
_containsJsx(node) {
|
|
42
|
+
if (node.isKind(SyntaxKind.JsxElement) || node.isKind(SyntaxKind.JsxSelfClosingElement) || node.isKind(SyntaxKind.JsxFragment)) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return node.getChildren().some((c) => this._containsJsx(c));
|
|
46
|
+
}
|
|
47
|
+
_fromFunctionDeclaration(fn) {
|
|
48
|
+
const body = fn.getBody();
|
|
49
|
+
const bodyStart = body ? body.getStart() : fn.getStart();
|
|
50
|
+
const bodyEnd = body ? body.getEnd() : fn.getEnd();
|
|
51
|
+
const param = fn.getParameters()[0];
|
|
52
|
+
let propsParam = null;
|
|
53
|
+
let hasDestructuredProps = false;
|
|
54
|
+
if (param) {
|
|
55
|
+
const nameNode = param.getNameNode();
|
|
56
|
+
if (nameNode.isKind(SyntaxKind.ObjectBindingPattern)) {
|
|
57
|
+
hasDestructuredProps = true;
|
|
58
|
+
propsParam = null;
|
|
59
|
+
} else {
|
|
60
|
+
propsParam = param.getName();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
name: fn.getName() ?? "anonymous",
|
|
65
|
+
propsParam,
|
|
66
|
+
hasDestructuredProps,
|
|
67
|
+
bodyStart,
|
|
68
|
+
bodyEnd
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
_fromVariableDeclaration(decl, init) {
|
|
72
|
+
const name = decl.getName();
|
|
73
|
+
let propsParam = null;
|
|
74
|
+
let hasDestructuredProps = false;
|
|
75
|
+
const params = init.isKind(SyntaxKind.ArrowFunction) ? init.getParameters() : init.isKind(SyntaxKind.FunctionExpression) ? init.getParameters() : [];
|
|
76
|
+
const param = params[0];
|
|
77
|
+
if (param) {
|
|
78
|
+
const nameNode = param.getNameNode();
|
|
79
|
+
if (nameNode.isKind(SyntaxKind.ObjectBindingPattern)) {
|
|
80
|
+
hasDestructuredProps = true;
|
|
81
|
+
propsParam = null;
|
|
82
|
+
} else {
|
|
83
|
+
propsParam = param.getName();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
let bodyNode;
|
|
87
|
+
if (init.isKind(SyntaxKind.ArrowFunction)) {
|
|
88
|
+
bodyNode = init.getBody();
|
|
89
|
+
} else if (init.isKind(SyntaxKind.FunctionExpression)) {
|
|
90
|
+
bodyNode = init.getBody() ?? init;
|
|
91
|
+
} else {
|
|
92
|
+
bodyNode = init;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
name,
|
|
96
|
+
propsParam,
|
|
97
|
+
hasDestructuredProps,
|
|
98
|
+
bodyStart: bodyNode.getStart(),
|
|
99
|
+
bodyEnd: bodyNode.getEnd()
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// src/analyzers/css-analyzer.ts
|
|
104
|
+
import { SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
105
|
+
|
|
106
|
+
class CSSAnalyzer {
|
|
107
|
+
analyze(sourceFile) {
|
|
108
|
+
const results = [];
|
|
109
|
+
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind2.CallExpression);
|
|
110
|
+
for (const call of callExpressions) {
|
|
111
|
+
const expression = call.getExpression();
|
|
112
|
+
if (expression.isKind(SyntaxKind2.Identifier) && expression.getText() === "css") {
|
|
113
|
+
const args = call.getArguments();
|
|
114
|
+
if (args.length === 0)
|
|
115
|
+
continue;
|
|
116
|
+
const firstArg = args[0];
|
|
117
|
+
const kind = this.classifyArgument(firstArg);
|
|
118
|
+
const blockNames = kind === "static" ? this.extractBlockNames(firstArg) : [];
|
|
119
|
+
const pos = sourceFile.getLineAndColumnAtPos(call.getStart());
|
|
120
|
+
results.push({
|
|
121
|
+
kind,
|
|
122
|
+
start: call.getStart(),
|
|
123
|
+
end: call.getEnd(),
|
|
124
|
+
line: pos.line,
|
|
125
|
+
column: pos.column - 1,
|
|
126
|
+
text: call.getText(),
|
|
127
|
+
blockNames
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return results;
|
|
132
|
+
}
|
|
133
|
+
classifyArgument(node) {
|
|
134
|
+
if (!node.isKind(SyntaxKind2.ObjectLiteralExpression)) {
|
|
135
|
+
return "reactive";
|
|
136
|
+
}
|
|
137
|
+
for (const prop of node.getProperties()) {
|
|
138
|
+
if (!prop.isKind(SyntaxKind2.PropertyAssignment)) {
|
|
139
|
+
return "reactive";
|
|
140
|
+
}
|
|
141
|
+
const initializer = prop.getInitializer();
|
|
142
|
+
if (!initializer)
|
|
143
|
+
return "reactive";
|
|
144
|
+
if (!initializer.isKind(SyntaxKind2.ArrayLiteralExpression)) {
|
|
145
|
+
return "reactive";
|
|
146
|
+
}
|
|
147
|
+
for (const element of initializer.getElements()) {
|
|
148
|
+
if (element.isKind(SyntaxKind2.StringLiteral)) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (element.isKind(SyntaxKind2.ObjectLiteralExpression)) {
|
|
152
|
+
if (!this.isStaticNestedObject(element)) {
|
|
153
|
+
return "reactive";
|
|
154
|
+
}
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
return "reactive";
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return "static";
|
|
161
|
+
}
|
|
162
|
+
isStaticNestedObject(node) {
|
|
163
|
+
if (!node.isKind(SyntaxKind2.ObjectLiteralExpression))
|
|
164
|
+
return false;
|
|
165
|
+
for (const prop of node.getProperties()) {
|
|
166
|
+
if (!prop.isKind(SyntaxKind2.PropertyAssignment))
|
|
167
|
+
return false;
|
|
168
|
+
const init = prop.getInitializer();
|
|
169
|
+
if (!init || !init.isKind(SyntaxKind2.ArrayLiteralExpression))
|
|
170
|
+
return false;
|
|
171
|
+
for (const el of init.getElements()) {
|
|
172
|
+
if (el.isKind(SyntaxKind2.StringLiteral))
|
|
173
|
+
continue;
|
|
174
|
+
if (el.isKind(SyntaxKind2.ObjectLiteralExpression)) {
|
|
175
|
+
if (this.isStaticRawDeclaration(el))
|
|
176
|
+
continue;
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
isStaticRawDeclaration(node) {
|
|
185
|
+
if (!node.isKind(SyntaxKind2.ObjectLiteralExpression))
|
|
186
|
+
return false;
|
|
187
|
+
const props = node.getProperties();
|
|
188
|
+
if (props.length !== 2)
|
|
189
|
+
return false;
|
|
190
|
+
let hasProperty = false;
|
|
191
|
+
let hasValue = false;
|
|
192
|
+
for (const prop of props) {
|
|
193
|
+
if (!prop.isKind(SyntaxKind2.PropertyAssignment))
|
|
194
|
+
return false;
|
|
195
|
+
const init = prop.getInitializer();
|
|
196
|
+
if (!init || !init.isKind(SyntaxKind2.StringLiteral))
|
|
197
|
+
return false;
|
|
198
|
+
const name = prop.getName();
|
|
199
|
+
if (name === "property")
|
|
200
|
+
hasProperty = true;
|
|
201
|
+
else if (name === "value")
|
|
202
|
+
hasValue = true;
|
|
203
|
+
}
|
|
204
|
+
return hasProperty && hasValue;
|
|
205
|
+
}
|
|
206
|
+
extractBlockNames(node) {
|
|
207
|
+
if (!node.isKind(SyntaxKind2.ObjectLiteralExpression))
|
|
208
|
+
return [];
|
|
209
|
+
const names = [];
|
|
210
|
+
for (const prop of node.getProperties()) {
|
|
211
|
+
if (prop.isKind(SyntaxKind2.PropertyAssignment)) {
|
|
212
|
+
const name = prop.getName();
|
|
213
|
+
names.push(name);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return names;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// src/analyzers/jsx-analyzer.ts
|
|
220
|
+
import { SyntaxKind as SyntaxKind4 } from "ts-morph";
|
|
221
|
+
|
|
222
|
+
// src/utils.ts
|
|
223
|
+
import { SyntaxKind as SyntaxKind3 } from "ts-morph";
|
|
224
|
+
function findBodyNode(sourceFile, component) {
|
|
225
|
+
const allBlocks = sourceFile.getDescendantsOfKind(SyntaxKind3.Block);
|
|
226
|
+
for (const block of allBlocks) {
|
|
227
|
+
if (block.getStart() === component.bodyStart && block.getEnd() === component.bodyEnd) {
|
|
228
|
+
return block;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/analyzers/jsx-analyzer.ts
|
|
235
|
+
class JsxAnalyzer {
|
|
236
|
+
analyze(sourceFile, component, variables) {
|
|
237
|
+
const reactiveNames = new Set(variables.filter((v) => v.kind === "signal" || v.kind === "computed").map((v) => v.name));
|
|
238
|
+
const signalApiVars = new Map;
|
|
239
|
+
const plainPropVars = new Map;
|
|
240
|
+
const fieldSignalPropVars = new Map;
|
|
241
|
+
const reactiveSourceVars = new Set;
|
|
242
|
+
for (const v of variables) {
|
|
243
|
+
if (v.signalProperties && v.signalProperties.size > 0) {
|
|
244
|
+
signalApiVars.set(v.name, v.signalProperties);
|
|
245
|
+
}
|
|
246
|
+
if (v.plainProperties && v.plainProperties.size > 0) {
|
|
247
|
+
plainPropVars.set(v.name, v.plainProperties);
|
|
248
|
+
}
|
|
249
|
+
if (v.fieldSignalProperties && v.fieldSignalProperties.size > 0) {
|
|
250
|
+
fieldSignalPropVars.set(v.name, v.fieldSignalProperties);
|
|
251
|
+
}
|
|
252
|
+
if (v.isReactiveSource) {
|
|
253
|
+
reactiveSourceVars.add(v.name);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const bodyNode = findBodyNode(sourceFile, component);
|
|
257
|
+
if (!bodyNode)
|
|
258
|
+
return [];
|
|
259
|
+
const results = [];
|
|
260
|
+
const jsxExprs = bodyNode.getDescendantsOfKind(SyntaxKind4.JsxExpression);
|
|
261
|
+
for (const expr of jsxExprs) {
|
|
262
|
+
const identifiers = collectIdentifiers(expr);
|
|
263
|
+
const deps = identifiers.filter((id) => reactiveNames.has(id));
|
|
264
|
+
const uniqueDeps = [...new Set(deps)];
|
|
265
|
+
const hasSignalApiAccess = containsSignalApiPropertyAccess(expr, signalApiVars, plainPropVars, fieldSignalPropVars);
|
|
266
|
+
const hasReactiveSourceAccess = containsReactiveSourceAccess(expr, reactiveSourceVars);
|
|
267
|
+
results.push({
|
|
268
|
+
start: expr.getStart(),
|
|
269
|
+
end: expr.getEnd(),
|
|
270
|
+
reactive: uniqueDeps.length > 0 || hasSignalApiAccess || hasReactiveSourceAccess,
|
|
271
|
+
deps: uniqueDeps
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return results;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function containsSignalApiPropertyAccess(node, signalApiVars, plainPropVars, fieldSignalPropVars) {
|
|
278
|
+
if (signalApiVars.size === 0 && fieldSignalPropVars.size === 0)
|
|
279
|
+
return false;
|
|
280
|
+
const propAccesses = node.getDescendantsOfKind(SyntaxKind4.PropertyAccessExpression);
|
|
281
|
+
for (const pa of propAccesses) {
|
|
282
|
+
const obj = pa.getExpression();
|
|
283
|
+
const propName = pa.getName();
|
|
284
|
+
if (obj.isKind(SyntaxKind4.Identifier)) {
|
|
285
|
+
const varName = obj.getText();
|
|
286
|
+
const signalProps = signalApiVars.get(varName);
|
|
287
|
+
if (signalProps?.has(propName)) {
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (obj.isKind(SyntaxKind4.PropertyAccessExpression)) {
|
|
292
|
+
const innerExpr = obj.asKindOrThrow(SyntaxKind4.PropertyAccessExpression);
|
|
293
|
+
const rootExpr = innerExpr.getExpression();
|
|
294
|
+
const middleProp = innerExpr.getName();
|
|
295
|
+
if (rootExpr.isKind(SyntaxKind4.Identifier)) {
|
|
296
|
+
const rootName = rootExpr.getText();
|
|
297
|
+
const fieldSignalProps = fieldSignalPropVars.get(rootName);
|
|
298
|
+
if (!fieldSignalProps)
|
|
299
|
+
continue;
|
|
300
|
+
const signalProps = signalApiVars.get(rootName);
|
|
301
|
+
const plainProps = plainPropVars.get(rootName);
|
|
302
|
+
if (signalProps?.has(middleProp) || plainProps?.has(middleProp))
|
|
303
|
+
continue;
|
|
304
|
+
if (fieldSignalProps.has(propName)) {
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
function containsReactiveSourceAccess(node, reactiveSourceVars) {
|
|
313
|
+
if (reactiveSourceVars.size === 0)
|
|
314
|
+
return false;
|
|
315
|
+
const propAccesses = node.getDescendantsOfKind(SyntaxKind4.PropertyAccessExpression);
|
|
316
|
+
for (const pa of propAccesses) {
|
|
317
|
+
const obj = pa.getExpression();
|
|
318
|
+
if (obj.isKind(SyntaxKind4.Identifier) && reactiveSourceVars.has(obj.getText())) {
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const identifiers = node.getDescendantsOfKind(SyntaxKind4.Identifier);
|
|
323
|
+
for (const id of identifiers) {
|
|
324
|
+
if (reactiveSourceVars.has(id.getText())) {
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
function collectIdentifiers(node) {
|
|
331
|
+
const ids = [];
|
|
332
|
+
const walk = (n) => {
|
|
333
|
+
if (n.isKind(SyntaxKind4.Identifier)) {
|
|
334
|
+
ids.push(n.getText());
|
|
335
|
+
}
|
|
336
|
+
for (const c of n.getChildren()) {
|
|
337
|
+
walk(c);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
walk(node);
|
|
341
|
+
return ids;
|
|
342
|
+
}
|
|
343
|
+
// src/analyzers/mutation-analyzer.ts
|
|
344
|
+
import { SyntaxKind as SyntaxKind5 } from "ts-morph";
|
|
345
|
+
var MUTATION_METHODS = new Set([
|
|
346
|
+
"push",
|
|
347
|
+
"pop",
|
|
348
|
+
"shift",
|
|
349
|
+
"unshift",
|
|
350
|
+
"splice",
|
|
351
|
+
"sort",
|
|
352
|
+
"reverse",
|
|
353
|
+
"fill",
|
|
354
|
+
"copyWithin"
|
|
355
|
+
]);
|
|
356
|
+
|
|
357
|
+
class MutationAnalyzer {
|
|
358
|
+
analyze(sourceFile, component, variables) {
|
|
359
|
+
const signalNames = new Set(variables.filter((v) => v.kind === "signal").map((v) => v.name));
|
|
360
|
+
const bodyNode = findBodyNode(sourceFile, component);
|
|
361
|
+
if (!bodyNode)
|
|
362
|
+
return [];
|
|
363
|
+
const mutations = [];
|
|
364
|
+
bodyNode.forEachDescendant((node) => {
|
|
365
|
+
if (node.isKind(SyntaxKind5.CallExpression)) {
|
|
366
|
+
const expr = node.getExpression();
|
|
367
|
+
if (expr.isKind(SyntaxKind5.PropertyAccessExpression)) {
|
|
368
|
+
const objName = getRootIdentifier(expr.getExpression());
|
|
369
|
+
const methodName = expr.getName();
|
|
370
|
+
if (objName && signalNames.has(objName) && MUTATION_METHODS.has(methodName)) {
|
|
371
|
+
mutations.push({
|
|
372
|
+
variableName: objName,
|
|
373
|
+
kind: "method-call",
|
|
374
|
+
start: node.getStart(),
|
|
375
|
+
end: node.getEnd()
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
const obj = expr.getExpression();
|
|
379
|
+
const method = expr.getName();
|
|
380
|
+
if (obj.isKind(SyntaxKind5.Identifier) && obj.getText() === "Object" && method === "assign") {
|
|
381
|
+
const args = node.getArguments();
|
|
382
|
+
const firstArg = args[0];
|
|
383
|
+
if (firstArg) {
|
|
384
|
+
if (firstArg.isKind(SyntaxKind5.Identifier) && signalNames.has(firstArg.getText())) {
|
|
385
|
+
mutations.push({
|
|
386
|
+
variableName: firstArg.getText(),
|
|
387
|
+
kind: "object-assign",
|
|
388
|
+
start: node.getStart(),
|
|
389
|
+
end: node.getEnd()
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (node.isKind(SyntaxKind5.BinaryExpression)) {
|
|
397
|
+
const left = node.getLeft();
|
|
398
|
+
const opKind = node.getOperatorToken().getKind();
|
|
399
|
+
if (opKind === SyntaxKind5.EqualsToken || opKind === SyntaxKind5.PlusEqualsToken || opKind === SyntaxKind5.MinusEqualsToken) {
|
|
400
|
+
if (left.isKind(SyntaxKind5.PropertyAccessExpression)) {
|
|
401
|
+
const rootName = getRootIdentifier(left.getExpression());
|
|
402
|
+
if (rootName && signalNames.has(rootName)) {
|
|
403
|
+
mutations.push({
|
|
404
|
+
variableName: rootName,
|
|
405
|
+
kind: "property-assignment",
|
|
406
|
+
start: node.getStart(),
|
|
407
|
+
end: node.getEnd()
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (left.isKind(SyntaxKind5.ElementAccessExpression)) {
|
|
412
|
+
const rootName = getRootIdentifier(left.getExpression());
|
|
413
|
+
if (rootName && signalNames.has(rootName)) {
|
|
414
|
+
mutations.push({
|
|
415
|
+
variableName: rootName,
|
|
416
|
+
kind: "index-assignment",
|
|
417
|
+
start: node.getStart(),
|
|
418
|
+
end: node.getEnd()
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (node.isKind(SyntaxKind5.DeleteExpression)) {
|
|
425
|
+
const expr = node.getExpression();
|
|
426
|
+
if (expr.isKind(SyntaxKind5.PropertyAccessExpression)) {
|
|
427
|
+
const rootName = getRootIdentifier(expr.getExpression());
|
|
428
|
+
if (rootName && signalNames.has(rootName)) {
|
|
429
|
+
mutations.push({
|
|
430
|
+
variableName: rootName,
|
|
431
|
+
kind: "delete",
|
|
432
|
+
start: node.getStart(),
|
|
433
|
+
end: node.getEnd()
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
return mutations;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function getRootIdentifier(node) {
|
|
443
|
+
if (node.isKind(SyntaxKind5.Identifier))
|
|
444
|
+
return node.getText();
|
|
445
|
+
if (node.isKind(SyntaxKind5.PropertyAccessExpression)) {
|
|
446
|
+
return getRootIdentifier(node.getExpression());
|
|
447
|
+
}
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
// src/analyzers/reactivity-analyzer.ts
|
|
451
|
+
import { SyntaxKind as SyntaxKind6 } from "ts-morph";
|
|
452
|
+
|
|
453
|
+
// src/signal-api-registry.ts
|
|
454
|
+
var SIGNAL_API_REGISTRY = {
|
|
455
|
+
query: {
|
|
456
|
+
signalProperties: new Set(["data", "loading", "error", "revalidating"]),
|
|
457
|
+
plainProperties: new Set(["refetch", "revalidate", "dispose"])
|
|
458
|
+
},
|
|
459
|
+
form: {
|
|
460
|
+
signalProperties: new Set(["submitting", "dirty", "valid"]),
|
|
461
|
+
plainProperties: new Set(["action", "method", "onSubmit", "reset", "setFieldError", "submit"]),
|
|
462
|
+
fieldSignalProperties: new Set(["error", "dirty", "touched", "value"])
|
|
463
|
+
},
|
|
464
|
+
createLoader: {
|
|
465
|
+
signalProperties: new Set(["data", "loading", "error"]),
|
|
466
|
+
plainProperties: new Set(["refetch"])
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
var REACTIVE_SOURCE_APIS = new Set(["useContext"]);
|
|
470
|
+
function isSignalApi(functionName) {
|
|
471
|
+
return functionName in SIGNAL_API_REGISTRY;
|
|
472
|
+
}
|
|
473
|
+
function isReactiveSourceApi(functionName) {
|
|
474
|
+
return REACTIVE_SOURCE_APIS.has(functionName);
|
|
475
|
+
}
|
|
476
|
+
function getSignalApiConfig(functionName) {
|
|
477
|
+
return SIGNAL_API_REGISTRY[functionName];
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/analyzers/reactivity-analyzer.ts
|
|
481
|
+
class ReactivityAnalyzer {
|
|
482
|
+
analyze(sourceFile, component) {
|
|
483
|
+
const bodyNode = findBodyNode(sourceFile, component);
|
|
484
|
+
if (!bodyNode)
|
|
485
|
+
return [];
|
|
486
|
+
const { signalApiAliases: importAliases, reactiveSourceAliases } = buildImportAliasMap(sourceFile);
|
|
487
|
+
const declaredNames = collectDeclaredNames(bodyNode);
|
|
488
|
+
const lets = new Map;
|
|
489
|
+
const consts = new Map;
|
|
490
|
+
const signalApiVars = new Map;
|
|
491
|
+
const reactiveSourceVars = new Set;
|
|
492
|
+
const destructuredFromMap = new Map;
|
|
493
|
+
const syntheticCounters = new Map;
|
|
494
|
+
for (const stmt of bodyNode.getChildSyntaxList()?.getChildren() ?? []) {
|
|
495
|
+
if (!stmt.isKind(SyntaxKind6.VariableStatement))
|
|
496
|
+
continue;
|
|
497
|
+
const declList = stmt.getChildrenOfKind(SyntaxKind6.VariableDeclarationList)[0];
|
|
498
|
+
if (!declList)
|
|
499
|
+
continue;
|
|
500
|
+
const declText = declList.getText();
|
|
501
|
+
const isLet = declText.startsWith("let ");
|
|
502
|
+
const isConst = declText.startsWith("const ");
|
|
503
|
+
for (const decl of declList.getDeclarations()) {
|
|
504
|
+
const nameNode = decl.getNameNode();
|
|
505
|
+
const init = decl.getInitializer();
|
|
506
|
+
if (nameNode.isKind(SyntaxKind6.ObjectBindingPattern)) {
|
|
507
|
+
let signalApiConfig;
|
|
508
|
+
let syntheticName;
|
|
509
|
+
const hasUnsupportedBindings = nameNode.getElements().some((el) => el.getInitializer() || el.getNameNode().isKind(SyntaxKind6.ObjectBindingPattern));
|
|
510
|
+
if (isConst && !hasUnsupportedBindings && init?.isKind(SyntaxKind6.CallExpression)) {
|
|
511
|
+
const callExpr = init.asKindOrThrow(SyntaxKind6.CallExpression);
|
|
512
|
+
const callName = callExpr.getExpression();
|
|
513
|
+
if (callName.isKind(SyntaxKind6.Identifier)) {
|
|
514
|
+
const fnName = callName.getText();
|
|
515
|
+
const originalName = importAliases.get(fnName);
|
|
516
|
+
if (originalName) {
|
|
517
|
+
signalApiConfig = getSignalApiConfig(originalName);
|
|
518
|
+
if (signalApiConfig) {
|
|
519
|
+
let counter = syntheticCounters.get(originalName) ?? 0;
|
|
520
|
+
syntheticName = `__${originalName}_${counter}`;
|
|
521
|
+
while (declaredNames.has(syntheticName)) {
|
|
522
|
+
counter++;
|
|
523
|
+
syntheticName = `__${originalName}_${counter}`;
|
|
524
|
+
}
|
|
525
|
+
syntheticCounters.set(originalName, counter + 1);
|
|
526
|
+
signalApiVars.set(syntheticName, signalApiConfig);
|
|
527
|
+
consts.set(syntheticName, {
|
|
528
|
+
start: decl.getStart(),
|
|
529
|
+
end: decl.getEnd(),
|
|
530
|
+
deps: []
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
for (const element of nameNode.getElements()) {
|
|
537
|
+
const bindingName = element.getName();
|
|
538
|
+
const propName = element.getPropertyNameNode()?.getText() ?? bindingName;
|
|
539
|
+
if (signalApiConfig && syntheticName) {
|
|
540
|
+
const isSignalProp = signalApiConfig.signalProperties.has(propName);
|
|
541
|
+
const deps2 = isSignalProp ? [syntheticName] : [];
|
|
542
|
+
const entry2 = { start: decl.getStart(), end: decl.getEnd(), deps: deps2 };
|
|
543
|
+
consts.set(bindingName, entry2);
|
|
544
|
+
destructuredFromMap.set(bindingName, syntheticName);
|
|
545
|
+
} else {
|
|
546
|
+
const deps2 = init ? collectIdentifierRefs(init) : [];
|
|
547
|
+
const entry2 = { start: decl.getStart(), end: decl.getEnd(), deps: deps2 };
|
|
548
|
+
if (isLet) {
|
|
549
|
+
lets.set(bindingName, entry2);
|
|
550
|
+
} else if (isConst) {
|
|
551
|
+
consts.set(bindingName, entry2);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
const name = decl.getName();
|
|
558
|
+
const deps = init ? collectIdentifierRefs(init) : [];
|
|
559
|
+
const entry = { start: decl.getStart(), end: decl.getEnd(), deps };
|
|
560
|
+
if (init?.isKind(SyntaxKind6.CallExpression)) {
|
|
561
|
+
const callExpr = init.asKindOrThrow(SyntaxKind6.CallExpression);
|
|
562
|
+
const callName = callExpr.getExpression();
|
|
563
|
+
if (callName.isKind(SyntaxKind6.Identifier)) {
|
|
564
|
+
const fnName = callName.getText();
|
|
565
|
+
const originalName = importAliases.get(fnName);
|
|
566
|
+
if (originalName) {
|
|
567
|
+
const config = getSignalApiConfig(originalName);
|
|
568
|
+
if (config) {
|
|
569
|
+
signalApiVars.set(name, config);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (reactiveSourceAliases.has(fnName)) {
|
|
573
|
+
reactiveSourceVars.add(name);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (isLet) {
|
|
578
|
+
lets.set(name, entry);
|
|
579
|
+
} else if (isConst) {
|
|
580
|
+
consts.set(name, entry);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
const jsxRefs = collectJsxReferencedIdentifiers(bodyNode);
|
|
585
|
+
const jsxReachable = new Set(jsxRefs);
|
|
586
|
+
let changed = true;
|
|
587
|
+
while (changed) {
|
|
588
|
+
changed = false;
|
|
589
|
+
for (const [name, info] of consts) {
|
|
590
|
+
if (jsxReachable.has(name)) {
|
|
591
|
+
for (const dep of info.deps) {
|
|
592
|
+
if (!jsxReachable.has(dep)) {
|
|
593
|
+
jsxReachable.add(dep);
|
|
594
|
+
changed = true;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const signals = new Set;
|
|
601
|
+
for (const name of lets.keys()) {
|
|
602
|
+
if (jsxReachable.has(name)) {
|
|
603
|
+
signals.add(name);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
const computeds = new Set;
|
|
607
|
+
changed = true;
|
|
608
|
+
while (changed) {
|
|
609
|
+
changed = false;
|
|
610
|
+
for (const [name, info] of consts) {
|
|
611
|
+
if (computeds.has(name))
|
|
612
|
+
continue;
|
|
613
|
+
const dependsOnReactive = info.deps.some((dep) => signals.has(dep) || computeds.has(dep) || signalApiVars.has(dep) || reactiveSourceVars.has(dep));
|
|
614
|
+
if (dependsOnReactive) {
|
|
615
|
+
computeds.add(name);
|
|
616
|
+
changed = true;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const results = [];
|
|
621
|
+
for (const [name, info] of lets) {
|
|
622
|
+
const varInfo = {
|
|
623
|
+
name,
|
|
624
|
+
kind: signals.has(name) ? "signal" : "static",
|
|
625
|
+
start: info.start,
|
|
626
|
+
end: info.end
|
|
627
|
+
};
|
|
628
|
+
const apiConfig = signalApiVars.get(name);
|
|
629
|
+
if (apiConfig) {
|
|
630
|
+
varInfo.signalProperties = apiConfig.signalProperties;
|
|
631
|
+
varInfo.plainProperties = apiConfig.plainProperties;
|
|
632
|
+
varInfo.fieldSignalProperties = apiConfig.fieldSignalProperties;
|
|
633
|
+
}
|
|
634
|
+
results.push(varInfo);
|
|
635
|
+
}
|
|
636
|
+
for (const [name, info] of consts) {
|
|
637
|
+
const varInfo = {
|
|
638
|
+
name,
|
|
639
|
+
kind: computeds.has(name) ? "computed" : "static",
|
|
640
|
+
start: info.start,
|
|
641
|
+
end: info.end
|
|
642
|
+
};
|
|
643
|
+
const apiConfig = signalApiVars.get(name);
|
|
644
|
+
if (apiConfig) {
|
|
645
|
+
varInfo.signalProperties = apiConfig.signalProperties;
|
|
646
|
+
varInfo.plainProperties = apiConfig.plainProperties;
|
|
647
|
+
varInfo.fieldSignalProperties = apiConfig.fieldSignalProperties;
|
|
648
|
+
}
|
|
649
|
+
if (reactiveSourceVars.has(name)) {
|
|
650
|
+
varInfo.isReactiveSource = true;
|
|
651
|
+
}
|
|
652
|
+
const syntheticSource = destructuredFromMap.get(name);
|
|
653
|
+
if (syntheticSource) {
|
|
654
|
+
varInfo.destructuredFrom = syntheticSource;
|
|
655
|
+
}
|
|
656
|
+
results.push(varInfo);
|
|
657
|
+
}
|
|
658
|
+
return results;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
function collectJsxReferencedIdentifiers(bodyNode) {
|
|
662
|
+
const refs = new Set;
|
|
663
|
+
const jsxExprs = bodyNode.getDescendantsOfKind(SyntaxKind6.JsxExpression);
|
|
664
|
+
for (const expr of jsxExprs) {
|
|
665
|
+
addIdentifiers(expr, refs);
|
|
666
|
+
}
|
|
667
|
+
return refs;
|
|
668
|
+
}
|
|
669
|
+
function addIdentifiers(node, refs) {
|
|
670
|
+
if (node.isKind(SyntaxKind6.Identifier)) {
|
|
671
|
+
refs.add(node.getText());
|
|
672
|
+
}
|
|
673
|
+
for (const child of node.getChildren()) {
|
|
674
|
+
addIdentifiers(child, refs);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function collectIdentifierRefs(node) {
|
|
678
|
+
const refs = [];
|
|
679
|
+
const walk = (n) => {
|
|
680
|
+
if (n.isKind(SyntaxKind6.Identifier)) {
|
|
681
|
+
refs.push(n.getText());
|
|
682
|
+
}
|
|
683
|
+
for (const c of n.getChildren()) {
|
|
684
|
+
walk(c);
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
walk(node);
|
|
688
|
+
return refs;
|
|
689
|
+
}
|
|
690
|
+
function buildImportAliasMap(sourceFile) {
|
|
691
|
+
const signalApiAliases = new Map;
|
|
692
|
+
const reactiveSourceAliases = new Set;
|
|
693
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
694
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
695
|
+
if (moduleSpecifier !== "@vertz/ui")
|
|
696
|
+
continue;
|
|
697
|
+
const namedImports = importDecl.getNamedImports();
|
|
698
|
+
for (const namedImport of namedImports) {
|
|
699
|
+
const originalName = namedImport.getName();
|
|
700
|
+
const localName = namedImport.getAliasNode()?.getText() ?? originalName;
|
|
701
|
+
if (isSignalApi(originalName)) {
|
|
702
|
+
signalApiAliases.set(localName, originalName);
|
|
703
|
+
}
|
|
704
|
+
if (isReactiveSourceApi(originalName)) {
|
|
705
|
+
reactiveSourceAliases.add(localName);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return { signalApiAliases, reactiveSourceAliases };
|
|
710
|
+
}
|
|
711
|
+
function collectDeclaredNames(bodyNode) {
|
|
712
|
+
const names = new Set;
|
|
713
|
+
for (const stmt of bodyNode.getChildSyntaxList()?.getChildren() ?? []) {
|
|
714
|
+
if (!stmt.isKind(SyntaxKind6.VariableStatement))
|
|
715
|
+
continue;
|
|
716
|
+
const declList = stmt.getChildrenOfKind(SyntaxKind6.VariableDeclarationList)[0];
|
|
717
|
+
if (!declList)
|
|
718
|
+
continue;
|
|
719
|
+
for (const decl of declList.getDeclarations()) {
|
|
720
|
+
const nameNode = decl.getNameNode();
|
|
721
|
+
if (nameNode.isKind(SyntaxKind6.Identifier)) {
|
|
722
|
+
names.add(nameNode.getText());
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return names;
|
|
727
|
+
}
|
|
728
|
+
// src/compiler.ts
|
|
729
|
+
import MagicString from "magic-string";
|
|
730
|
+
import { Project, SyntaxKind as SyntaxKind12, ts } from "ts-morph";
|
|
731
|
+
|
|
732
|
+
// src/diagnostics/mutation-diagnostics.ts
|
|
733
|
+
import { SyntaxKind as SyntaxKind7 } from "ts-morph";
|
|
734
|
+
var MUTATION_METHODS2 = new Set([
|
|
735
|
+
"push",
|
|
736
|
+
"pop",
|
|
737
|
+
"shift",
|
|
738
|
+
"unshift",
|
|
739
|
+
"splice",
|
|
740
|
+
"sort",
|
|
741
|
+
"reverse",
|
|
742
|
+
"fill",
|
|
743
|
+
"copyWithin"
|
|
744
|
+
]);
|
|
745
|
+
|
|
746
|
+
class MutationDiagnostics {
|
|
747
|
+
analyze(sourceFile, component, variables) {
|
|
748
|
+
const staticConsts = new Set(variables.filter((v) => v.kind === "static").map((v) => v.name));
|
|
749
|
+
const bodyNode = findBodyNode(sourceFile, component);
|
|
750
|
+
if (!bodyNode)
|
|
751
|
+
return [];
|
|
752
|
+
const jsxRefs = collectJsxReferencedIdentifiers2(bodyNode);
|
|
753
|
+
const constsInJsx = new Set([...staticConsts].filter((name) => jsxRefs.has(name)));
|
|
754
|
+
const diagnostics = [];
|
|
755
|
+
bodyNode.forEachDescendant((node) => {
|
|
756
|
+
if (node.isKind(SyntaxKind7.CallExpression)) {
|
|
757
|
+
const expr = node.getExpression();
|
|
758
|
+
if (expr.isKind(SyntaxKind7.PropertyAccessExpression)) {
|
|
759
|
+
const objName = getRootIdentifier2(expr.getExpression());
|
|
760
|
+
const methodName = expr.getName();
|
|
761
|
+
if (objName && constsInJsx.has(objName) && MUTATION_METHODS2.has(methodName)) {
|
|
762
|
+
const pos = sourceFile.getLineAndColumnAtPos(node.getStart());
|
|
763
|
+
diagnostics.push({
|
|
764
|
+
code: "non-reactive-mutation",
|
|
765
|
+
message: `Mutation \`.${methodName}()\` on \`const ${objName}\` will not trigger UI updates. Change \`const\` to \`let\` to make it reactive.`,
|
|
766
|
+
severity: "warning",
|
|
767
|
+
line: pos.line,
|
|
768
|
+
column: pos.column - 1,
|
|
769
|
+
fix: `Change \`const ${objName}\` to \`let ${objName}\``
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (node.isKind(SyntaxKind7.BinaryExpression)) {
|
|
775
|
+
const left = node.getLeft();
|
|
776
|
+
const op = node.getOperatorToken();
|
|
777
|
+
if (op.isKind(SyntaxKind7.EqualsToken) && left.isKind(SyntaxKind7.PropertyAccessExpression)) {
|
|
778
|
+
const rootName = getRootIdentifier2(left.getExpression());
|
|
779
|
+
if (rootName && constsInJsx.has(rootName)) {
|
|
780
|
+
const pos = sourceFile.getLineAndColumnAtPos(node.getStart());
|
|
781
|
+
diagnostics.push({
|
|
782
|
+
code: "non-reactive-mutation",
|
|
783
|
+
message: `Property assignment on \`const ${rootName}\` will not trigger UI updates. Change \`const\` to \`let\` to make it reactive.`,
|
|
784
|
+
severity: "warning",
|
|
785
|
+
line: pos.line,
|
|
786
|
+
column: pos.column - 1,
|
|
787
|
+
fix: `Change \`const ${rootName}\` to \`let ${rootName}\``
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
return diagnostics;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function getRootIdentifier2(node) {
|
|
797
|
+
if (node.isKind(SyntaxKind7.Identifier))
|
|
798
|
+
return node.getText();
|
|
799
|
+
if (node.isKind(SyntaxKind7.PropertyAccessExpression)) {
|
|
800
|
+
return getRootIdentifier2(node.getExpression());
|
|
801
|
+
}
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
function collectJsxReferencedIdentifiers2(bodyNode) {
|
|
805
|
+
const refs = new Set;
|
|
806
|
+
const jsxExprs = bodyNode.getDescendantsOfKind(SyntaxKind7.JsxExpression);
|
|
807
|
+
for (const expr of jsxExprs) {
|
|
808
|
+
addIdentifiers2(expr, refs);
|
|
809
|
+
}
|
|
810
|
+
return refs;
|
|
811
|
+
}
|
|
812
|
+
function addIdentifiers2(node, refs) {
|
|
813
|
+
if (node.isKind(SyntaxKind7.Identifier))
|
|
814
|
+
refs.add(node.getText());
|
|
815
|
+
for (const child of node.getChildren()) {
|
|
816
|
+
addIdentifiers2(child, refs);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// src/diagnostics/props-destructuring.ts
|
|
821
|
+
class PropsDestructuringDiagnostics {
|
|
822
|
+
analyze(sourceFile, components) {
|
|
823
|
+
const diagnostics = [];
|
|
824
|
+
for (const comp of components) {
|
|
825
|
+
if (comp.hasDestructuredProps) {
|
|
826
|
+
const pos = sourceFile.getLineAndColumnAtPos(comp.bodyStart);
|
|
827
|
+
diagnostics.push({
|
|
828
|
+
code: "props-destructuring",
|
|
829
|
+
message: `Component \`${comp.name}\` destructures props in the parameter list. This breaks reactivity — use \`props.x\` access instead.`,
|
|
830
|
+
severity: "warning",
|
|
831
|
+
line: pos.line,
|
|
832
|
+
column: pos.column - 1,
|
|
833
|
+
fix: `Change \`function ${comp.name}({ ... })\` to \`function ${comp.name}(props)\` and access via \`props.x\``
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return diagnostics;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// src/diagnostics/ssr-safety-diagnostics.ts
|
|
842
|
+
import { SyntaxKind as SyntaxKind8 } from "ts-morph";
|
|
843
|
+
var BROWSER_ONLY_GLOBALS = new Set([
|
|
844
|
+
"localStorage",
|
|
845
|
+
"sessionStorage",
|
|
846
|
+
"navigator",
|
|
847
|
+
"IntersectionObserver",
|
|
848
|
+
"ResizeObserver",
|
|
849
|
+
"MutationObserver",
|
|
850
|
+
"requestAnimationFrame",
|
|
851
|
+
"cancelAnimationFrame",
|
|
852
|
+
"requestIdleCallback",
|
|
853
|
+
"cancelIdleCallback"
|
|
854
|
+
]);
|
|
855
|
+
var BROWSER_ONLY_DOCUMENT_PROPS = new Set([
|
|
856
|
+
"querySelector",
|
|
857
|
+
"querySelectorAll",
|
|
858
|
+
"getElementById",
|
|
859
|
+
"cookie"
|
|
860
|
+
]);
|
|
861
|
+
|
|
862
|
+
class SSRSafetyDiagnostics {
|
|
863
|
+
analyze(sourceFile, component) {
|
|
864
|
+
const bodyNode = findBodyNode(sourceFile, component);
|
|
865
|
+
if (!bodyNode)
|
|
866
|
+
return [];
|
|
867
|
+
const diagnostics = [];
|
|
868
|
+
bodyNode.forEachDescendant((node) => {
|
|
869
|
+
if (!node.isKind(SyntaxKind8.Identifier))
|
|
870
|
+
return;
|
|
871
|
+
const name = node.getText();
|
|
872
|
+
if (BROWSER_ONLY_GLOBALS.has(name)) {
|
|
873
|
+
if (isInNestedFunction(node, bodyNode))
|
|
874
|
+
return;
|
|
875
|
+
if (isInTypeofGuard(node))
|
|
876
|
+
return;
|
|
877
|
+
const pos = sourceFile.getLineAndColumnAtPos(node.getStart());
|
|
878
|
+
diagnostics.push({
|
|
879
|
+
code: "ssr-unsafe-api",
|
|
880
|
+
message: `\`${name}\` is a browser-only API that is not available during SSR. Move it inside \`onMount()\` or wrap in a \`typeof\` guard.`,
|
|
881
|
+
severity: "warning",
|
|
882
|
+
line: pos.line,
|
|
883
|
+
column: pos.column - 1,
|
|
884
|
+
fix: `Move the \`${name}\` usage inside \`onMount(() => { ... })\` or guard with \`typeof ${name} !== 'undefined'\`.`
|
|
885
|
+
});
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
if (name === "document") {
|
|
889
|
+
const parent = node.getParent();
|
|
890
|
+
if (parent?.isKind(SyntaxKind8.PropertyAccessExpression) && parent.getExpression() === node) {
|
|
891
|
+
const propName = parent.getName();
|
|
892
|
+
if (BROWSER_ONLY_DOCUMENT_PROPS.has(propName)) {
|
|
893
|
+
if (isInNestedFunction(node, bodyNode))
|
|
894
|
+
return;
|
|
895
|
+
if (isInTypeofGuard(node))
|
|
896
|
+
return;
|
|
897
|
+
const pos = sourceFile.getLineAndColumnAtPos(node.getStart());
|
|
898
|
+
diagnostics.push({
|
|
899
|
+
code: "ssr-unsafe-api",
|
|
900
|
+
message: `\`document.${propName}\` is a browser-only API that is not available during SSR. Move it inside \`onMount()\` or wrap in a \`typeof\` guard.`,
|
|
901
|
+
severity: "warning",
|
|
902
|
+
line: pos.line,
|
|
903
|
+
column: pos.column - 1,
|
|
904
|
+
fix: `Move the \`document.${propName}\` usage inside \`onMount(() => { ... })\`.`
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
return diagnostics;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
function isInNestedFunction(node, bodyNode) {
|
|
914
|
+
let current = node.getParent();
|
|
915
|
+
while (current && current !== bodyNode) {
|
|
916
|
+
if (current.isKind(SyntaxKind8.ArrowFunction) || current.isKind(SyntaxKind8.FunctionExpression) || current.isKind(SyntaxKind8.FunctionDeclaration) || current.isKind(SyntaxKind8.MethodDeclaration) || current.isKind(SyntaxKind8.Constructor) || current.isKind(SyntaxKind8.GetAccessor) || current.isKind(SyntaxKind8.SetAccessor)) {
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
current = current.getParent();
|
|
920
|
+
}
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
function isInTypeofGuard(node) {
|
|
924
|
+
const parent = node.getParent();
|
|
925
|
+
if (parent?.isKind(SyntaxKind8.TypeOfExpression))
|
|
926
|
+
return true;
|
|
927
|
+
const name = node.getText();
|
|
928
|
+
let current = node.getParent();
|
|
929
|
+
while (current) {
|
|
930
|
+
if (current.isKind(SyntaxKind8.IfStatement)) {
|
|
931
|
+
const condition = current.getExpression();
|
|
932
|
+
const thenStatement = current.getThenStatement();
|
|
933
|
+
if (conditionContainsTypeofFor(condition, name)) {
|
|
934
|
+
if (isDescendantOf(node, thenStatement))
|
|
935
|
+
return true;
|
|
936
|
+
}
|
|
937
|
+
if (conditionContainsTypeofFor(condition, "window")) {
|
|
938
|
+
if (isDescendantOf(node, thenStatement))
|
|
939
|
+
return true;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
if (current.isKind(SyntaxKind8.ConditionalExpression)) {
|
|
943
|
+
const condition = current.getCondition();
|
|
944
|
+
if (conditionContainsTypeofFor(condition, name)) {
|
|
945
|
+
const whenTrue = current.getWhenTrue();
|
|
946
|
+
if (isDescendantOf(node, whenTrue))
|
|
947
|
+
return true;
|
|
948
|
+
}
|
|
949
|
+
if (conditionContainsTypeofFor(condition, "window")) {
|
|
950
|
+
const whenTrue = current.getWhenTrue();
|
|
951
|
+
if (isDescendantOf(node, whenTrue))
|
|
952
|
+
return true;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (current.isKind(SyntaxKind8.BinaryExpression)) {
|
|
956
|
+
const op = current.getOperatorToken();
|
|
957
|
+
if (op.getKind() === SyntaxKind8.AmpersandAmpersandToken) {
|
|
958
|
+
const left = current.getLeft();
|
|
959
|
+
if (conditionContainsTypeofFor(left, name)) {
|
|
960
|
+
if (isDescendantOf(node, current.getRight()))
|
|
961
|
+
return true;
|
|
962
|
+
}
|
|
963
|
+
if (conditionContainsTypeofFor(left, "window")) {
|
|
964
|
+
if (isDescendantOf(node, current.getRight()))
|
|
965
|
+
return true;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
current = current.getParent();
|
|
970
|
+
}
|
|
971
|
+
return false;
|
|
972
|
+
}
|
|
973
|
+
function conditionContainsTypeofFor(condition, name) {
|
|
974
|
+
if (condition.isKind(SyntaxKind8.TypeOfExpression)) {
|
|
975
|
+
return condition.getExpression().getText() === name;
|
|
976
|
+
}
|
|
977
|
+
for (const desc of condition.getDescendantsOfKind(SyntaxKind8.TypeOfExpression)) {
|
|
978
|
+
if (desc.getExpression().getText() === name)
|
|
979
|
+
return true;
|
|
980
|
+
}
|
|
981
|
+
return false;
|
|
982
|
+
}
|
|
983
|
+
function isDescendantOf(node, ancestor) {
|
|
984
|
+
let current = node.getParent();
|
|
985
|
+
while (current) {
|
|
986
|
+
if (current === ancestor)
|
|
987
|
+
return true;
|
|
988
|
+
current = current.getParent();
|
|
989
|
+
}
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// src/transformers/computed-transformer.ts
|
|
994
|
+
import { SyntaxKind as SyntaxKind9 } from "ts-morph";
|
|
995
|
+
class ComputedTransformer {
|
|
996
|
+
transform(source, sourceFile, component, variables) {
|
|
997
|
+
const computeds = new Set(variables.filter((v) => v.kind === "computed").map((v) => v.name));
|
|
998
|
+
if (computeds.size === 0)
|
|
999
|
+
return;
|
|
1000
|
+
const bodyNode = findBodyNode(sourceFile, component);
|
|
1001
|
+
if (!bodyNode)
|
|
1002
|
+
return;
|
|
1003
|
+
const destructuredFromMap = new Map;
|
|
1004
|
+
const syntheticVarInfo = new Map;
|
|
1005
|
+
for (const v of variables) {
|
|
1006
|
+
if (v.destructuredFrom) {
|
|
1007
|
+
destructuredFromMap.set(v.name, v.destructuredFrom);
|
|
1008
|
+
}
|
|
1009
|
+
if (v.name.startsWith("__") && v.signalProperties) {
|
|
1010
|
+
syntheticVarInfo.set(v.name, v);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
transformComputedReads(source, bodyNode, computeds);
|
|
1014
|
+
for (const stmt of bodyNode.getChildSyntaxList()?.getChildren() ?? []) {
|
|
1015
|
+
if (!stmt.isKind(SyntaxKind9.VariableStatement))
|
|
1016
|
+
continue;
|
|
1017
|
+
const declList = stmt.getChildrenOfKind(SyntaxKind9.VariableDeclarationList)[0];
|
|
1018
|
+
if (!declList)
|
|
1019
|
+
continue;
|
|
1020
|
+
for (const decl of declList.getDeclarations()) {
|
|
1021
|
+
const nameNode = decl.getNameNode();
|
|
1022
|
+
const init = decl.getInitializer();
|
|
1023
|
+
if (!init)
|
|
1024
|
+
continue;
|
|
1025
|
+
if (nameNode.isKind(SyntaxKind9.ObjectBindingPattern)) {
|
|
1026
|
+
const elements = nameNode.getElements();
|
|
1027
|
+
const firstBindingName = elements[0]?.getName();
|
|
1028
|
+
const syntheticName = firstBindingName ? destructuredFromMap.get(firstBindingName) : undefined;
|
|
1029
|
+
if (syntheticName) {
|
|
1030
|
+
const initText = source.slice(init.getStart(), init.getEnd());
|
|
1031
|
+
const synthetic = syntheticVarInfo.get(syntheticName);
|
|
1032
|
+
const signalProps = synthetic?.signalProperties ?? new Set;
|
|
1033
|
+
const lines = [];
|
|
1034
|
+
lines.push(`const ${syntheticName} = ${initText}`);
|
|
1035
|
+
for (const el of elements) {
|
|
1036
|
+
if (el.getDotDotDotToken()) {
|
|
1037
|
+
const restName = el.getName();
|
|
1038
|
+
lines.push(`const { ...${restName} } = ${syntheticName}`);
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
const bindingName = el.getName();
|
|
1042
|
+
const propName = el.getPropertyNameNode()?.getText() ?? bindingName;
|
|
1043
|
+
if (computeds.has(bindingName) && signalProps.has(propName)) {
|
|
1044
|
+
lines.push(`const ${bindingName} = computed(() => ${syntheticName}.${propName}.value)`);
|
|
1045
|
+
} else {
|
|
1046
|
+
lines.push(`const ${bindingName} = ${syntheticName}.${propName}`);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
source.overwrite(stmt.getStart(), stmt.getEnd(), `${lines.join(`;
|
|
1050
|
+
`)};`);
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
const computedElements = elements.filter((el) => computeds.has(el.getName()));
|
|
1054
|
+
if (computedElements.length > 0) {
|
|
1055
|
+
const initText = source.slice(init.getStart(), init.getEnd());
|
|
1056
|
+
const replacements = elements.map((el) => {
|
|
1057
|
+
const bindingName = el.getName();
|
|
1058
|
+
const propName = el.getPropertyNameNode()?.getText() ?? bindingName;
|
|
1059
|
+
if (computeds.has(bindingName)) {
|
|
1060
|
+
return `const ${bindingName} = computed(() => ${initText}.${propName})`;
|
|
1061
|
+
}
|
|
1062
|
+
return `const ${bindingName} = ${initText}.${propName}`;
|
|
1063
|
+
});
|
|
1064
|
+
source.overwrite(stmt.getStart(), stmt.getEnd(), `${replacements.join(`;
|
|
1065
|
+
`)};`);
|
|
1066
|
+
}
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
const name = decl.getName();
|
|
1070
|
+
if (!computeds.has(name))
|
|
1071
|
+
continue;
|
|
1072
|
+
source.appendLeft(init.getStart(), "computed(() => ");
|
|
1073
|
+
source.appendRight(init.getEnd(), ")");
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
function transformComputedReads(source, bodyNode, computeds) {
|
|
1079
|
+
bodyNode.forEachDescendant((node) => {
|
|
1080
|
+
if (!node.isKind(SyntaxKind9.Identifier))
|
|
1081
|
+
return;
|
|
1082
|
+
const name = node.getText();
|
|
1083
|
+
if (!computeds.has(name))
|
|
1084
|
+
return;
|
|
1085
|
+
const parent = node.getParent();
|
|
1086
|
+
if (!parent)
|
|
1087
|
+
return;
|
|
1088
|
+
if (parent.isKind(SyntaxKind9.VariableDeclaration) && parent.getNameNode() === node) {
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
if (parent.isKind(SyntaxKind9.PropertyAccessExpression) && parent.getNameNode() === node) {
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
if (parent.isKind(SyntaxKind9.PropertyAssignment) && parent.getNameNode() === node) {
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (parent.isKind(SyntaxKind9.ShorthandPropertyAssignment)) {
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
if (parent.isKind(SyntaxKind9.BindingElement)) {
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
source.overwrite(node.getStart(), node.getEnd(), `${name}.value`);
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// src/transformers/jsx-transformer.ts
|
|
1108
|
+
import { SyntaxKind as SyntaxKind10 } from "ts-morph";
|
|
1109
|
+
var varCounter = 0;
|
|
1110
|
+
function genVar() {
|
|
1111
|
+
return `__el${varCounter++}`;
|
|
1112
|
+
}
|
|
1113
|
+
function resetVarCounter() {
|
|
1114
|
+
varCounter = 0;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
class JsxTransformer {
|
|
1118
|
+
transform(source, sourceFile, component, variables, jsxExpressions) {
|
|
1119
|
+
resetVarCounter();
|
|
1120
|
+
const bodyNode = findBodyNode(sourceFile, component);
|
|
1121
|
+
if (!bodyNode)
|
|
1122
|
+
return;
|
|
1123
|
+
const reactiveNames = new Set(variables.filter((v) => v.kind === "signal" || v.kind === "computed").map((v) => v.name));
|
|
1124
|
+
const jsxMap = new Map(jsxExpressions.map((e) => [e.start, e]));
|
|
1125
|
+
const formVarNames = new Set;
|
|
1126
|
+
for (const v of variables) {
|
|
1127
|
+
if (v.fieldSignalProperties && v.fieldSignalProperties.size > 0) {
|
|
1128
|
+
formVarNames.add(v.name);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
this.transformAllJsx(bodyNode, reactiveNames, jsxMap, source, formVarNames);
|
|
1132
|
+
}
|
|
1133
|
+
transformAllJsx(node, reactiveNames, jsxMap, source, formVarNames) {
|
|
1134
|
+
if (isJsxTopLevel(node)) {
|
|
1135
|
+
const transformed = transformJsxNode(node, reactiveNames, jsxMap, source, formVarNames);
|
|
1136
|
+
source.overwrite(node.getStart(), node.getEnd(), transformed);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
for (const child of node.getChildren()) {
|
|
1140
|
+
this.transformAllJsx(child, reactiveNames, jsxMap, source, formVarNames);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
function isJsxTopLevel(node) {
|
|
1145
|
+
return node.isKind(SyntaxKind10.JsxElement) || node.isKind(SyntaxKind10.JsxSelfClosingElement) || node.isKind(SyntaxKind10.JsxFragment);
|
|
1146
|
+
}
|
|
1147
|
+
function transformJsxNode(node, reactiveNames, jsxMap, source, formVarNames = new Set) {
|
|
1148
|
+
if (node.isKind(SyntaxKind10.ParenthesizedExpression)) {
|
|
1149
|
+
return transformJsxNode(node.getExpression(), reactiveNames, jsxMap, source, formVarNames);
|
|
1150
|
+
}
|
|
1151
|
+
if (node.isKind(SyntaxKind10.JsxElement)) {
|
|
1152
|
+
return transformJsxElement(node, reactiveNames, jsxMap, source, formVarNames);
|
|
1153
|
+
}
|
|
1154
|
+
if (node.isKind(SyntaxKind10.JsxSelfClosingElement)) {
|
|
1155
|
+
return transformSelfClosingElement(node, reactiveNames, jsxMap, source, formVarNames);
|
|
1156
|
+
}
|
|
1157
|
+
if (node.isKind(SyntaxKind10.JsxFragment)) {
|
|
1158
|
+
return transformFragment(node, reactiveNames, jsxMap, source, formVarNames);
|
|
1159
|
+
}
|
|
1160
|
+
if (node.isKind(SyntaxKind10.JsxText)) {
|
|
1161
|
+
const text = node.getText().trim();
|
|
1162
|
+
if (!text)
|
|
1163
|
+
return "";
|
|
1164
|
+
return `__staticText(${JSON.stringify(text)})`;
|
|
1165
|
+
}
|
|
1166
|
+
return node.getText();
|
|
1167
|
+
}
|
|
1168
|
+
function transformJsxElement(node, reactiveNames, jsxMap, source, formVarNames = new Set) {
|
|
1169
|
+
const openingElement = node.getFirstChildByKind(SyntaxKind10.JsxOpeningElement);
|
|
1170
|
+
if (!openingElement)
|
|
1171
|
+
return node.getText();
|
|
1172
|
+
const tagName = openingElement.getTagNameNode().getText();
|
|
1173
|
+
const isComponent = /^[A-Z]/.test(tagName);
|
|
1174
|
+
if (isComponent) {
|
|
1175
|
+
const hasExplicitChildren = openingElement.getAttributes().filter((a) => a.isKind(SyntaxKind10.JsxAttribute)).some((attr) => attr.getNameNode().getText() === "children");
|
|
1176
|
+
let extraEntries;
|
|
1177
|
+
if (!hasExplicitChildren) {
|
|
1178
|
+
const children2 = getJsxChildren(node);
|
|
1179
|
+
const nonEmptyChildren = children2.filter((child) => {
|
|
1180
|
+
if (child.isKind(SyntaxKind10.JsxText))
|
|
1181
|
+
return !!child.getText().trim();
|
|
1182
|
+
return true;
|
|
1183
|
+
});
|
|
1184
|
+
if (nonEmptyChildren.length > 0) {
|
|
1185
|
+
const thunkCode = buildComponentChildrenThunk(nonEmptyChildren, reactiveNames, jsxMap, source, formVarNames);
|
|
1186
|
+
if (thunkCode) {
|
|
1187
|
+
extraEntries = new Map([["children", thunkCode]]);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
const propsObj = buildPropsObject(openingElement, jsxMap, source, reactiveNames, formVarNames, extraEntries);
|
|
1192
|
+
return `${tagName}(${propsObj})`;
|
|
1193
|
+
}
|
|
1194
|
+
const elVar = genVar();
|
|
1195
|
+
const statements = [];
|
|
1196
|
+
statements.push(`const ${elVar} = __element(${JSON.stringify(tagName)})`);
|
|
1197
|
+
const attrs = openingElement.getAttributes();
|
|
1198
|
+
for (const attr of attrs) {
|
|
1199
|
+
if (!attr.isKind(SyntaxKind10.JsxAttribute))
|
|
1200
|
+
continue;
|
|
1201
|
+
const attrStmt = processAttribute(attr, elVar, jsxMap, source);
|
|
1202
|
+
if (attrStmt)
|
|
1203
|
+
statements.push(attrStmt);
|
|
1204
|
+
}
|
|
1205
|
+
const bindStmt = tryBindElement(tagName, openingElement, elVar, formVarNames);
|
|
1206
|
+
if (bindStmt)
|
|
1207
|
+
statements.push(bindStmt);
|
|
1208
|
+
const children = getJsxChildren(node);
|
|
1209
|
+
const hasChildren = children.some((child) => {
|
|
1210
|
+
if (child.isKind(SyntaxKind10.JsxText))
|
|
1211
|
+
return !!child.getText().trim();
|
|
1212
|
+
return true;
|
|
1213
|
+
});
|
|
1214
|
+
if (hasChildren) {
|
|
1215
|
+
statements.push(`__enterChildren(${elVar})`);
|
|
1216
|
+
}
|
|
1217
|
+
for (const child of children) {
|
|
1218
|
+
const childCode = transformChild(child, reactiveNames, jsxMap, elVar, source, formVarNames);
|
|
1219
|
+
if (childCode)
|
|
1220
|
+
statements.push(childCode);
|
|
1221
|
+
}
|
|
1222
|
+
if (hasChildren) {
|
|
1223
|
+
statements.push(`__exitChildren()`);
|
|
1224
|
+
}
|
|
1225
|
+
return `(() => {
|
|
1226
|
+
${statements.map((s) => ` ${s};`).join(`
|
|
1227
|
+
`)}
|
|
1228
|
+
return ${elVar};
|
|
1229
|
+
})()`;
|
|
1230
|
+
}
|
|
1231
|
+
function transformSelfClosingElement(node, reactiveNames, jsxMap, source, formVarNames = new Set) {
|
|
1232
|
+
if (!node.isKind(SyntaxKind10.JsxSelfClosingElement))
|
|
1233
|
+
return node.getText();
|
|
1234
|
+
const tagName = node.getTagNameNode().getText();
|
|
1235
|
+
const isComponent = /^[A-Z]/.test(tagName);
|
|
1236
|
+
if (isComponent) {
|
|
1237
|
+
const propsObj = buildPropsObject(node, jsxMap, source, reactiveNames, formVarNames);
|
|
1238
|
+
return `${tagName}(${propsObj})`;
|
|
1239
|
+
}
|
|
1240
|
+
const elVar = genVar();
|
|
1241
|
+
const statements = [];
|
|
1242
|
+
statements.push(`const ${elVar} = __element(${JSON.stringify(tagName)})`);
|
|
1243
|
+
const attrs = node.getAttributes();
|
|
1244
|
+
for (const attr of attrs) {
|
|
1245
|
+
if (!attr.isKind(SyntaxKind10.JsxAttribute))
|
|
1246
|
+
continue;
|
|
1247
|
+
const attrStmt = processAttribute(attr, elVar, jsxMap, source);
|
|
1248
|
+
if (attrStmt)
|
|
1249
|
+
statements.push(attrStmt);
|
|
1250
|
+
}
|
|
1251
|
+
const bindStmt = tryBindElement(tagName, node, elVar, formVarNames);
|
|
1252
|
+
if (bindStmt)
|
|
1253
|
+
statements.push(bindStmt);
|
|
1254
|
+
return `(() => {
|
|
1255
|
+
${statements.map((s) => ` ${s};`).join(`
|
|
1256
|
+
`)}
|
|
1257
|
+
return ${elVar};
|
|
1258
|
+
})()`;
|
|
1259
|
+
}
|
|
1260
|
+
function transformFragment(node, reactiveNames, jsxMap, source, formVarNames = new Set) {
|
|
1261
|
+
const fragVar = genVar();
|
|
1262
|
+
const statements = [];
|
|
1263
|
+
statements.push(`const ${fragVar} = document.createDocumentFragment()`);
|
|
1264
|
+
const children = getJsxChildren(node);
|
|
1265
|
+
for (const child of children) {
|
|
1266
|
+
const childCode = transformChild(child, reactiveNames, jsxMap, fragVar, source, formVarNames);
|
|
1267
|
+
if (childCode)
|
|
1268
|
+
statements.push(childCode);
|
|
1269
|
+
}
|
|
1270
|
+
return `(() => {
|
|
1271
|
+
${statements.map((s) => ` ${s};`).join(`
|
|
1272
|
+
`)}
|
|
1273
|
+
return ${fragVar};
|
|
1274
|
+
})()`;
|
|
1275
|
+
}
|
|
1276
|
+
function processAttribute(attr, elVar, jsxMap, source) {
|
|
1277
|
+
if (!attr.isKind(SyntaxKind10.JsxAttribute))
|
|
1278
|
+
return null;
|
|
1279
|
+
const attrName = attr.getNameNode().getText();
|
|
1280
|
+
const init = attr.getInitializer();
|
|
1281
|
+
if (!init)
|
|
1282
|
+
return null;
|
|
1283
|
+
if (attrName.startsWith("on") && attrName.length > 2) {
|
|
1284
|
+
const eventName = attrName[2]?.toLowerCase() + attrName.slice(3);
|
|
1285
|
+
if (init.isKind(SyntaxKind10.JsxExpression)) {
|
|
1286
|
+
const exprNode = init.getExpression();
|
|
1287
|
+
const handlerText = exprNode ? source.slice(exprNode.getStart(), exprNode.getEnd()) : "";
|
|
1288
|
+
return `__on(${elVar}, ${JSON.stringify(eventName)}, ${handlerText})`;
|
|
1289
|
+
}
|
|
1290
|
+
return null;
|
|
1291
|
+
}
|
|
1292
|
+
if (init.isKind(SyntaxKind10.StringLiteral)) {
|
|
1293
|
+
return `${elVar}.setAttribute(${JSON.stringify(attrName)}, ${init.getText()})`;
|
|
1294
|
+
}
|
|
1295
|
+
if (init.isKind(SyntaxKind10.JsxExpression)) {
|
|
1296
|
+
const exprInfo = jsxMap.get(init.getStart());
|
|
1297
|
+
const exprNode = init.getExpression();
|
|
1298
|
+
const exprText = exprNode ? source.slice(exprNode.getStart(), exprNode.getEnd()) : "";
|
|
1299
|
+
if (exprInfo?.reactive) {
|
|
1300
|
+
return `__attr(${elVar}, ${JSON.stringify(attrName)}, () => ${exprText})`;
|
|
1301
|
+
}
|
|
1302
|
+
return `{ const __v = ${exprText}; if (__v != null && __v !== false) ${elVar}.setAttribute(${JSON.stringify(attrName)}, __v === true ? "" : __v); }`;
|
|
1303
|
+
}
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
function transformChild(child, reactiveNames, jsxMap, parentVar, source, formVarNames = new Set) {
|
|
1307
|
+
if (child.isKind(SyntaxKind10.JsxText)) {
|
|
1308
|
+
const text = child.getText().trim();
|
|
1309
|
+
if (!text)
|
|
1310
|
+
return null;
|
|
1311
|
+
return `__append(${parentVar}, __staticText(${JSON.stringify(text)}))`;
|
|
1312
|
+
}
|
|
1313
|
+
if (child.isKind(SyntaxKind10.JsxExpression)) {
|
|
1314
|
+
const exprInfo = jsxMap.get(child.getStart());
|
|
1315
|
+
const exprNode = child.getExpression();
|
|
1316
|
+
if (!exprNode)
|
|
1317
|
+
return null;
|
|
1318
|
+
if (exprInfo?.reactive) {
|
|
1319
|
+
const conditionalCode = tryTransformConditional(exprNode, reactiveNames, jsxMap, source);
|
|
1320
|
+
if (conditionalCode) {
|
|
1321
|
+
return `__append(${parentVar}, ${conditionalCode})`;
|
|
1322
|
+
}
|
|
1323
|
+
const listCode = tryTransformList(exprNode, reactiveNames, jsxMap, parentVar, source);
|
|
1324
|
+
if (listCode) {
|
|
1325
|
+
return listCode;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
const exprText = source.slice(exprNode.getStart(), exprNode.getEnd());
|
|
1329
|
+
if (exprInfo?.reactive) {
|
|
1330
|
+
return `__append(${parentVar}, __child(() => ${exprText}))`;
|
|
1331
|
+
}
|
|
1332
|
+
return `__insert(${parentVar}, ${exprText})`;
|
|
1333
|
+
}
|
|
1334
|
+
if (child.isKind(SyntaxKind10.JsxElement) || child.isKind(SyntaxKind10.JsxSelfClosingElement) || child.isKind(SyntaxKind10.JsxFragment)) {
|
|
1335
|
+
const childCode = transformJsxNode(child, reactiveNames, jsxMap, source, formVarNames);
|
|
1336
|
+
return `__append(${parentVar}, ${childCode})`;
|
|
1337
|
+
}
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1340
|
+
function transformChildAsValue(child, reactiveNames, jsxMap, source, formVarNames = new Set) {
|
|
1341
|
+
if (child.isKind(SyntaxKind10.JsxText)) {
|
|
1342
|
+
const text = child.getText().trim();
|
|
1343
|
+
if (!text)
|
|
1344
|
+
return null;
|
|
1345
|
+
return `__staticText(${JSON.stringify(text)})`;
|
|
1346
|
+
}
|
|
1347
|
+
if (child.isKind(SyntaxKind10.JsxExpression)) {
|
|
1348
|
+
const exprInfo = jsxMap.get(child.getStart());
|
|
1349
|
+
const exprNode = child.getExpression();
|
|
1350
|
+
if (!exprNode)
|
|
1351
|
+
return null;
|
|
1352
|
+
if (exprInfo?.reactive) {
|
|
1353
|
+
const conditionalCode = tryTransformConditional(exprNode, reactiveNames, jsxMap, source);
|
|
1354
|
+
if (conditionalCode) {
|
|
1355
|
+
return conditionalCode;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
const exprText = source.slice(exprNode.getStart(), exprNode.getEnd());
|
|
1359
|
+
if (exprInfo?.reactive) {
|
|
1360
|
+
return `__child(() => ${exprText})`;
|
|
1361
|
+
}
|
|
1362
|
+
return exprText;
|
|
1363
|
+
}
|
|
1364
|
+
if (child.isKind(SyntaxKind10.JsxElement) || child.isKind(SyntaxKind10.JsxSelfClosingElement) || child.isKind(SyntaxKind10.JsxFragment)) {
|
|
1365
|
+
return transformJsxNode(child, reactiveNames, jsxMap, source, formVarNames);
|
|
1366
|
+
}
|
|
1367
|
+
return null;
|
|
1368
|
+
}
|
|
1369
|
+
function buildComponentChildrenThunk(children, reactiveNames, jsxMap, source, formVarNames) {
|
|
1370
|
+
const values = [];
|
|
1371
|
+
for (const child of children) {
|
|
1372
|
+
const value = transformChildAsValue(child, reactiveNames, jsxMap, source, formVarNames);
|
|
1373
|
+
if (value)
|
|
1374
|
+
values.push(value);
|
|
1375
|
+
}
|
|
1376
|
+
if (values.length === 0)
|
|
1377
|
+
return "";
|
|
1378
|
+
if (values.length === 1)
|
|
1379
|
+
return `() => ${values[0]}`;
|
|
1380
|
+
return `() => [${values.join(", ")}]`;
|
|
1381
|
+
}
|
|
1382
|
+
function tryTransformConditional(exprNode, reactiveNames, jsxMap, source) {
|
|
1383
|
+
if (exprNode.isKind(SyntaxKind10.ConditionalExpression)) {
|
|
1384
|
+
const condition = exprNode.getCondition();
|
|
1385
|
+
const whenTrue = exprNode.getWhenTrue();
|
|
1386
|
+
const whenFalse = exprNode.getWhenFalse();
|
|
1387
|
+
const condText = source.slice(condition.getStart(), condition.getEnd());
|
|
1388
|
+
const trueBranch = transformBranch(whenTrue, reactiveNames, jsxMap, source);
|
|
1389
|
+
const falseBranch = transformBranch(whenFalse, reactiveNames, jsxMap, source);
|
|
1390
|
+
return `__conditional(() => ${condText}, () => ${trueBranch}, () => ${falseBranch})`;
|
|
1391
|
+
}
|
|
1392
|
+
if (exprNode.isKind(SyntaxKind10.BinaryExpression) && exprNode.getOperatorToken().getKind() === SyntaxKind10.AmpersandAmpersandToken) {
|
|
1393
|
+
const left = exprNode.getLeft();
|
|
1394
|
+
const right = exprNode.getRight();
|
|
1395
|
+
const condText = source.slice(left.getStart(), left.getEnd());
|
|
1396
|
+
const trueBranch = transformBranch(right, reactiveNames, jsxMap, source);
|
|
1397
|
+
return `__conditional(() => ${condText}, () => ${trueBranch}, () => null)`;
|
|
1398
|
+
}
|
|
1399
|
+
return null;
|
|
1400
|
+
}
|
|
1401
|
+
function transformBranch(node, reactiveNames, jsxMap, source) {
|
|
1402
|
+
if (node.isKind(SyntaxKind10.ParenthesizedExpression)) {
|
|
1403
|
+
return transformBranch(node.getExpression(), reactiveNames, jsxMap, source);
|
|
1404
|
+
}
|
|
1405
|
+
if (node.isKind(SyntaxKind10.JsxElement) || node.isKind(SyntaxKind10.JsxSelfClosingElement) || node.isKind(SyntaxKind10.JsxFragment)) {
|
|
1406
|
+
return transformJsxNode(node, reactiveNames, jsxMap, source);
|
|
1407
|
+
}
|
|
1408
|
+
if (node.isKind(SyntaxKind10.ConditionalExpression)) {
|
|
1409
|
+
const nested = tryTransformConditional(node, reactiveNames, jsxMap, source);
|
|
1410
|
+
if (nested)
|
|
1411
|
+
return nested;
|
|
1412
|
+
}
|
|
1413
|
+
return source.slice(node.getStart(), node.getEnd());
|
|
1414
|
+
}
|
|
1415
|
+
function tryTransformList(exprNode, reactiveNames, jsxMap, parentVar, source) {
|
|
1416
|
+
if (!exprNode.isKind(SyntaxKind10.CallExpression))
|
|
1417
|
+
return null;
|
|
1418
|
+
const propAccess = exprNode.getExpression();
|
|
1419
|
+
if (!propAccess.isKind(SyntaxKind10.PropertyAccessExpression))
|
|
1420
|
+
return null;
|
|
1421
|
+
const methodName = propAccess.getNameNode().getText();
|
|
1422
|
+
if (methodName !== "map")
|
|
1423
|
+
return null;
|
|
1424
|
+
const args = exprNode.getArguments();
|
|
1425
|
+
if (args.length === 0)
|
|
1426
|
+
return null;
|
|
1427
|
+
const callbackArg = args[0];
|
|
1428
|
+
if (!callbackArg)
|
|
1429
|
+
return null;
|
|
1430
|
+
const sourceObj = propAccess.getExpression();
|
|
1431
|
+
const sourceObjText = source.slice(sourceObj.getStart(), sourceObj.getEnd());
|
|
1432
|
+
let itemParam = null;
|
|
1433
|
+
let indexParam = null;
|
|
1434
|
+
let callbackBody = null;
|
|
1435
|
+
if (callbackArg.isKind(SyntaxKind10.ArrowFunction)) {
|
|
1436
|
+
const params = callbackArg.getParameters();
|
|
1437
|
+
itemParam = params[0]?.getName() ?? null;
|
|
1438
|
+
indexParam = params[1]?.getName() ?? null;
|
|
1439
|
+
const body = callbackArg.getBody();
|
|
1440
|
+
callbackBody = body;
|
|
1441
|
+
}
|
|
1442
|
+
if (!itemParam || !callbackBody)
|
|
1443
|
+
return null;
|
|
1444
|
+
const keyFn = extractKeyFunction(callbackBody, itemParam, indexParam);
|
|
1445
|
+
const renderFn = buildListRenderFunction(callbackBody, itemParam, reactiveNames, jsxMap, source);
|
|
1446
|
+
return `__list(${parentVar}, () => ${sourceObjText}, ${keyFn}, ${renderFn})`;
|
|
1447
|
+
}
|
|
1448
|
+
function extractKeyFunction(callbackBody, itemParam, indexParam) {
|
|
1449
|
+
const jsxNode = findJsxInBody(callbackBody);
|
|
1450
|
+
if (jsxNode) {
|
|
1451
|
+
const keyValue = extractKeyPropValue(jsxNode);
|
|
1452
|
+
if (keyValue) {
|
|
1453
|
+
return `(${itemParam}) => ${keyValue}`;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
if (indexParam) {
|
|
1457
|
+
return `(_item, ${indexParam}) => ${indexParam}`;
|
|
1458
|
+
}
|
|
1459
|
+
return `(_item, __i) => __i`;
|
|
1460
|
+
}
|
|
1461
|
+
function findJsxInBody(node) {
|
|
1462
|
+
if (node.isKind(SyntaxKind10.JsxElement) || node.isKind(SyntaxKind10.JsxSelfClosingElement)) {
|
|
1463
|
+
return node;
|
|
1464
|
+
}
|
|
1465
|
+
if (node.isKind(SyntaxKind10.Block)) {
|
|
1466
|
+
const returnStmt = node.getFirstDescendantByKind(SyntaxKind10.ReturnStatement);
|
|
1467
|
+
if (returnStmt) {
|
|
1468
|
+
const expr = returnStmt.getExpression();
|
|
1469
|
+
if (expr)
|
|
1470
|
+
return findJsxInBody(expr);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
if (node.isKind(SyntaxKind10.ParenthesizedExpression)) {
|
|
1474
|
+
return findJsxInBody(node.getExpression());
|
|
1475
|
+
}
|
|
1476
|
+
return null;
|
|
1477
|
+
}
|
|
1478
|
+
function extractKeyPropValue(jsxNode) {
|
|
1479
|
+
const attrs = jsxNode.getDescendantsOfKind(SyntaxKind10.JsxAttribute);
|
|
1480
|
+
for (const attr of attrs) {
|
|
1481
|
+
if (attr.getNameNode().getText() !== "key")
|
|
1482
|
+
continue;
|
|
1483
|
+
const init = attr.getInitializer();
|
|
1484
|
+
if (!init)
|
|
1485
|
+
continue;
|
|
1486
|
+
if (init.isKind(SyntaxKind10.JsxExpression)) {
|
|
1487
|
+
const expr = init.getExpression();
|
|
1488
|
+
if (expr)
|
|
1489
|
+
return expr.getText();
|
|
1490
|
+
}
|
|
1491
|
+
if (init.isKind(SyntaxKind10.StringLiteral)) {
|
|
1492
|
+
return init.getText();
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
return null;
|
|
1496
|
+
}
|
|
1497
|
+
function buildListRenderFunction(callbackBody, itemParam, reactiveNames, jsxMap, source) {
|
|
1498
|
+
const jsxNode = findJsxInBody(callbackBody);
|
|
1499
|
+
if (jsxNode) {
|
|
1500
|
+
const transformed = transformJsxNode(jsxNode, reactiveNames, jsxMap, source);
|
|
1501
|
+
return `(${itemParam}) => ${transformed}`;
|
|
1502
|
+
}
|
|
1503
|
+
const bodyText = source.slice(callbackBody.getStart(), callbackBody.getEnd());
|
|
1504
|
+
return `(${itemParam}) => ${bodyText}`;
|
|
1505
|
+
}
|
|
1506
|
+
function sliceWithTransformedJsx(node, reactiveNames, jsxMap, source, formVarNames) {
|
|
1507
|
+
const start = node.getStart();
|
|
1508
|
+
const end = node.getEnd();
|
|
1509
|
+
const jsxNodes = [];
|
|
1510
|
+
collectJsxInExpression(node, reactiveNames, jsxMap, source, formVarNames, jsxNodes);
|
|
1511
|
+
if (jsxNodes.length === 0) {
|
|
1512
|
+
return source.slice(start, end);
|
|
1513
|
+
}
|
|
1514
|
+
jsxNodes.sort((a, b) => a.start - b.start);
|
|
1515
|
+
let result = "";
|
|
1516
|
+
let cursor = start;
|
|
1517
|
+
for (const jsx of jsxNodes) {
|
|
1518
|
+
result += source.slice(cursor, jsx.start);
|
|
1519
|
+
result += jsx.transformed;
|
|
1520
|
+
cursor = jsx.end;
|
|
1521
|
+
}
|
|
1522
|
+
result += source.slice(cursor, end);
|
|
1523
|
+
return result;
|
|
1524
|
+
}
|
|
1525
|
+
function collectJsxInExpression(node, reactiveNames, jsxMap, source, formVarNames, results) {
|
|
1526
|
+
if (isJsxTopLevel(node)) {
|
|
1527
|
+
const transformed = transformJsxNode(node, reactiveNames, jsxMap, source, formVarNames);
|
|
1528
|
+
results.push({ start: node.getStart(), end: node.getEnd(), transformed });
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
for (const child of node.getChildren()) {
|
|
1532
|
+
collectJsxInExpression(child, reactiveNames, jsxMap, source, formVarNames, results);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
function buildPropsObject(element, jsxMap, source, reactiveNames, formVarNames, extraEntries) {
|
|
1536
|
+
const attrs = element.isKind(SyntaxKind10.JsxOpeningElement) || element.isKind(SyntaxKind10.JsxSelfClosingElement) ? element.getAttributes().filter((a) => a.isKind(SyntaxKind10.JsxAttribute)) : element.getDescendantsOfKind(SyntaxKind10.JsxAttribute);
|
|
1537
|
+
if (attrs.length === 0 && (!extraEntries || extraEntries.size === 0))
|
|
1538
|
+
return "{}";
|
|
1539
|
+
const props = [];
|
|
1540
|
+
for (const attr of attrs) {
|
|
1541
|
+
const name = attr.getNameNode().getText();
|
|
1542
|
+
const init = attr.getInitializer();
|
|
1543
|
+
if (!init) {
|
|
1544
|
+
props.push(`${name}: true`);
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
if (init.isKind(SyntaxKind10.StringLiteral)) {
|
|
1548
|
+
props.push(`${name}: ${init.getText()}`);
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
if (init.isKind(SyntaxKind10.JsxExpression)) {
|
|
1552
|
+
const exprInfo = jsxMap.get(init.getStart());
|
|
1553
|
+
const exprNode = init.getExpression();
|
|
1554
|
+
const exprText = exprNode ? sliceWithTransformedJsx(exprNode, reactiveNames, jsxMap, source, formVarNames) : "";
|
|
1555
|
+
if (exprInfo?.reactive) {
|
|
1556
|
+
props.push(`get ${name}() { return ${exprText}; }`);
|
|
1557
|
+
} else {
|
|
1558
|
+
props.push(`${name}: ${exprText}`);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
if (extraEntries) {
|
|
1563
|
+
for (const [key, value] of extraEntries) {
|
|
1564
|
+
props.push(`${key}: ${value}`);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
return `{ ${props.join(", ")} }`;
|
|
1568
|
+
}
|
|
1569
|
+
function getJsxChildren(node) {
|
|
1570
|
+
const children = [];
|
|
1571
|
+
for (const child of node.getChildren()) {
|
|
1572
|
+
if (isJsxChild(child)) {
|
|
1573
|
+
children.push(child);
|
|
1574
|
+
} else if (child.isKind(SyntaxKind10.SyntaxList)) {
|
|
1575
|
+
for (const grandchild of child.getChildren()) {
|
|
1576
|
+
if (isJsxChild(grandchild)) {
|
|
1577
|
+
children.push(grandchild);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
return children;
|
|
1583
|
+
}
|
|
1584
|
+
function isJsxChild(node) {
|
|
1585
|
+
return node.isKind(SyntaxKind10.JsxText) || node.isKind(SyntaxKind10.JsxExpression) || node.isKind(SyntaxKind10.JsxElement) || node.isKind(SyntaxKind10.JsxSelfClosingElement) || node.isKind(SyntaxKind10.JsxFragment);
|
|
1586
|
+
}
|
|
1587
|
+
function tryBindElement(tagName, element, elVar, formVarNames) {
|
|
1588
|
+
if (tagName !== "form" || formVarNames.size === 0)
|
|
1589
|
+
return null;
|
|
1590
|
+
const attrs = element.getDescendantsOfKind(SyntaxKind10.JsxAttribute);
|
|
1591
|
+
for (const attr of attrs) {
|
|
1592
|
+
if (attr.getNameNode().getText() !== "onSubmit")
|
|
1593
|
+
continue;
|
|
1594
|
+
const init = attr.getInitializer();
|
|
1595
|
+
if (!init?.isKind(SyntaxKind10.JsxExpression))
|
|
1596
|
+
continue;
|
|
1597
|
+
const exprNode = init.getExpression();
|
|
1598
|
+
if (!exprNode?.isKind(SyntaxKind10.PropertyAccessExpression))
|
|
1599
|
+
continue;
|
|
1600
|
+
const obj = exprNode.getExpression();
|
|
1601
|
+
if (!obj.isKind(SyntaxKind10.Identifier))
|
|
1602
|
+
continue;
|
|
1603
|
+
const varName = obj.getText();
|
|
1604
|
+
if (formVarNames.has(varName)) {
|
|
1605
|
+
return `${varName}.__bindElement(${elVar})`;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
return null;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// src/transformers/mutation-transformer.ts
|
|
1612
|
+
function escapeRegExp(s) {
|
|
1613
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1614
|
+
}
|
|
1615
|
+
function replaceWithBoundary(text, variableName, suffix, replacement) {
|
|
1616
|
+
const pattern = new RegExp(`(?<![a-zA-Z0-9_$])${escapeRegExp(variableName)}${escapeRegExp(suffix)}`, "g");
|
|
1617
|
+
return text.replace(pattern, replacement);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
class MutationTransformer {
|
|
1621
|
+
transform(source, _component, mutations) {
|
|
1622
|
+
if (mutations.length === 0)
|
|
1623
|
+
return;
|
|
1624
|
+
const sorted = [...mutations].sort((a, b) => b.start - a.start);
|
|
1625
|
+
for (const mutation of sorted) {
|
|
1626
|
+
const originalText = source.slice(mutation.start, mutation.end);
|
|
1627
|
+
switch (mutation.kind) {
|
|
1628
|
+
case "method-call":
|
|
1629
|
+
this._transformMethodCall(source, mutation, originalText);
|
|
1630
|
+
break;
|
|
1631
|
+
case "property-assignment":
|
|
1632
|
+
this._transformPropertyAssignment(source, mutation, originalText);
|
|
1633
|
+
break;
|
|
1634
|
+
case "index-assignment":
|
|
1635
|
+
this._transformIndexAssignment(source, mutation, originalText);
|
|
1636
|
+
break;
|
|
1637
|
+
case "delete":
|
|
1638
|
+
this._transformDelete(source, mutation, originalText);
|
|
1639
|
+
break;
|
|
1640
|
+
case "object-assign":
|
|
1641
|
+
this._transformObjectAssign(source, mutation, originalText);
|
|
1642
|
+
break;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
_transformMethodCall(source, mutation, originalText) {
|
|
1647
|
+
const { variableName } = mutation;
|
|
1648
|
+
const peekText = replaceWithBoundary(originalText, variableName, ".", `${variableName}.peek().`);
|
|
1649
|
+
source.overwrite(mutation.start, mutation.end, `(${peekText}, ${variableName}.notify())`);
|
|
1650
|
+
}
|
|
1651
|
+
_transformPropertyAssignment(source, mutation, originalText) {
|
|
1652
|
+
const { variableName } = mutation;
|
|
1653
|
+
const peekText = replaceWithBoundary(originalText, variableName, ".", `${variableName}.peek().`);
|
|
1654
|
+
source.overwrite(mutation.start, mutation.end, `(${peekText}, ${variableName}.notify())`);
|
|
1655
|
+
}
|
|
1656
|
+
_transformIndexAssignment(source, mutation, originalText) {
|
|
1657
|
+
const { variableName } = mutation;
|
|
1658
|
+
const peekText = replaceWithBoundary(originalText, variableName, "[", `${variableName}.peek()[`);
|
|
1659
|
+
source.overwrite(mutation.start, mutation.end, `(${peekText}, ${variableName}.notify())`);
|
|
1660
|
+
}
|
|
1661
|
+
_transformDelete(source, mutation, originalText) {
|
|
1662
|
+
const { variableName } = mutation;
|
|
1663
|
+
const peekText = replaceWithBoundary(originalText, variableName, ".", `${variableName}.peek().`);
|
|
1664
|
+
source.overwrite(mutation.start, mutation.end, `(${peekText}, ${variableName}.notify())`);
|
|
1665
|
+
}
|
|
1666
|
+
_transformObjectAssign(source, mutation, originalText) {
|
|
1667
|
+
const { variableName } = mutation;
|
|
1668
|
+
const peekText = originalText.replace(`Object.assign(${variableName}`, `Object.assign(${variableName}.peek()`);
|
|
1669
|
+
source.overwrite(mutation.start, mutation.end, `(${peekText}, ${variableName}.notify())`);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// src/transformers/signal-transformer.ts
|
|
1674
|
+
import { SyntaxKind as SyntaxKind11 } from "ts-morph";
|
|
1675
|
+
class SignalTransformer {
|
|
1676
|
+
transform(source, sourceFile, component, variables, mutationRanges = []) {
|
|
1677
|
+
const signals = new Set(variables.filter((v) => v.kind === "signal").map((v) => v.name));
|
|
1678
|
+
const signalApiVars = new Map;
|
|
1679
|
+
const plainPropVars = new Map;
|
|
1680
|
+
const fieldSignalPropVars = new Map;
|
|
1681
|
+
for (const v of variables) {
|
|
1682
|
+
if (v.signalProperties && v.signalProperties.size > 0) {
|
|
1683
|
+
signalApiVars.set(v.name, v.signalProperties);
|
|
1684
|
+
}
|
|
1685
|
+
if (v.plainProperties && v.plainProperties.size > 0) {
|
|
1686
|
+
plainPropVars.set(v.name, v.plainProperties);
|
|
1687
|
+
}
|
|
1688
|
+
if (v.fieldSignalProperties && v.fieldSignalProperties.size > 0) {
|
|
1689
|
+
fieldSignalPropVars.set(v.name, v.fieldSignalProperties);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
const bodyNode = findBodyNode(sourceFile, component);
|
|
1693
|
+
if (!bodyNode)
|
|
1694
|
+
return;
|
|
1695
|
+
if (signals.size > 0) {
|
|
1696
|
+
transformDeclarations(source, bodyNode, signals);
|
|
1697
|
+
transformReferences(source, bodyNode, signals, mutationRanges);
|
|
1698
|
+
}
|
|
1699
|
+
if (signalApiVars.size > 0 || fieldSignalPropVars.size > 0) {
|
|
1700
|
+
transformSignalApiProperties(source, bodyNode, signalApiVars, plainPropVars, fieldSignalPropVars);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
function transformDeclarations(source, bodyNode, signals) {
|
|
1705
|
+
for (const stmt of bodyNode.getChildSyntaxList()?.getChildren() ?? []) {
|
|
1706
|
+
if (!stmt.isKind(SyntaxKind11.VariableStatement))
|
|
1707
|
+
continue;
|
|
1708
|
+
const declList = stmt.getChildrenOfKind(SyntaxKind11.VariableDeclarationList)[0];
|
|
1709
|
+
if (!declList)
|
|
1710
|
+
continue;
|
|
1711
|
+
for (const decl of declList.getDeclarations()) {
|
|
1712
|
+
const name = decl.getName();
|
|
1713
|
+
if (!signals.has(name))
|
|
1714
|
+
continue;
|
|
1715
|
+
const init = decl.getInitializer();
|
|
1716
|
+
if (!init)
|
|
1717
|
+
continue;
|
|
1718
|
+
const letKeyword = declList.getFirstChildByKind(SyntaxKind11.LetKeyword);
|
|
1719
|
+
if (letKeyword) {
|
|
1720
|
+
source.overwrite(letKeyword.getStart(), letKeyword.getEnd(), "const");
|
|
1721
|
+
}
|
|
1722
|
+
source.appendLeft(init.getStart(), "signal(");
|
|
1723
|
+
source.appendRight(init.getEnd(), ")");
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
function isInsideMutationRange(pos, ranges) {
|
|
1728
|
+
return ranges.some((r) => pos >= r.start && pos < r.end);
|
|
1729
|
+
}
|
|
1730
|
+
function transformReferences(source, bodyNode, signals, mutationRanges) {
|
|
1731
|
+
bodyNode.forEachDescendant((node) => {
|
|
1732
|
+
if (!node.isKind(SyntaxKind11.Identifier))
|
|
1733
|
+
return;
|
|
1734
|
+
const name = node.getText();
|
|
1735
|
+
if (!signals.has(name))
|
|
1736
|
+
return;
|
|
1737
|
+
const parent = node.getParent();
|
|
1738
|
+
if (!parent)
|
|
1739
|
+
return;
|
|
1740
|
+
if (parent.isKind(SyntaxKind11.VariableDeclaration) && parent.getNameNode() === node) {
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
if (parent.isKind(SyntaxKind11.PropertyAccessExpression) && parent.getNameNode() === node) {
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
if (parent.isKind(SyntaxKind11.PropertyAssignment) && parent.getNameNode() === node) {
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
if (parent.isKind(SyntaxKind11.ShorthandPropertyAssignment)) {
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
if (parent.isKind(SyntaxKind11.BindingElement)) {
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
if (isInsideMutationRange(node.getStart(), mutationRanges)) {
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
source.overwrite(node.getStart(), node.getEnd(), `${name}.value`);
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
function transformSignalApiProperties(source, bodyNode, signalApiVars, plainPropVars, fieldSignalPropVars) {
|
|
1762
|
+
const threeLevelRanges = [];
|
|
1763
|
+
bodyNode.forEachDescendant((node) => {
|
|
1764
|
+
if (!node.isKind(SyntaxKind11.PropertyAccessExpression))
|
|
1765
|
+
return;
|
|
1766
|
+
const outerExpr = node.asKindOrThrow(SyntaxKind11.PropertyAccessExpression);
|
|
1767
|
+
const middleExpr = outerExpr.getExpression();
|
|
1768
|
+
const leafProp = outerExpr.getName();
|
|
1769
|
+
if (!middleExpr.isKind(SyntaxKind11.PropertyAccessExpression))
|
|
1770
|
+
return;
|
|
1771
|
+
const innerExpr = middleExpr.asKindOrThrow(SyntaxKind11.PropertyAccessExpression);
|
|
1772
|
+
const rootExpr = innerExpr.getExpression();
|
|
1773
|
+
const middleProp = innerExpr.getName();
|
|
1774
|
+
if (!rootExpr.isKind(SyntaxKind11.Identifier))
|
|
1775
|
+
return;
|
|
1776
|
+
const rootName = rootExpr.getText();
|
|
1777
|
+
const fieldSignalProps = fieldSignalPropVars.get(rootName);
|
|
1778
|
+
if (!fieldSignalProps)
|
|
1779
|
+
return;
|
|
1780
|
+
const signalProps = signalApiVars.get(rootName);
|
|
1781
|
+
const plainProps = plainPropVars.get(rootName);
|
|
1782
|
+
if (signalProps?.has(middleProp) || plainProps?.has(middleProp))
|
|
1783
|
+
return;
|
|
1784
|
+
if (!fieldSignalProps.has(leafProp))
|
|
1785
|
+
return;
|
|
1786
|
+
const parent = outerExpr.getParent();
|
|
1787
|
+
if (parent?.isKind(SyntaxKind11.PropertyAccessExpression)) {
|
|
1788
|
+
const parentProp = parent.asKindOrThrow(SyntaxKind11.PropertyAccessExpression);
|
|
1789
|
+
if (parentProp.getExpression() === outerExpr && parentProp.getName() === "value") {
|
|
1790
|
+
threeLevelRanges.push({ start: outerExpr.getStart(), end: outerExpr.getEnd() });
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
source.appendLeft(outerExpr.getEnd(), ".value");
|
|
1795
|
+
threeLevelRanges.push({ start: outerExpr.getStart(), end: outerExpr.getEnd() });
|
|
1796
|
+
});
|
|
1797
|
+
bodyNode.forEachDescendant((node) => {
|
|
1798
|
+
if (!node.isKind(SyntaxKind11.PropertyAccessExpression))
|
|
1799
|
+
return;
|
|
1800
|
+
const expr = node.asKindOrThrow(SyntaxKind11.PropertyAccessExpression);
|
|
1801
|
+
const nodeStart = expr.getStart();
|
|
1802
|
+
if (threeLevelRanges.some((r) => nodeStart >= r.start && nodeStart < r.end))
|
|
1803
|
+
return;
|
|
1804
|
+
const objExpr = expr.getExpression();
|
|
1805
|
+
const propName = expr.getName();
|
|
1806
|
+
if (!objExpr.isKind(SyntaxKind11.Identifier))
|
|
1807
|
+
return;
|
|
1808
|
+
const varName = objExpr.getText();
|
|
1809
|
+
const signalProps = signalApiVars.get(varName);
|
|
1810
|
+
if (!signalProps || !signalProps.has(propName))
|
|
1811
|
+
return;
|
|
1812
|
+
const parent = expr.getParent();
|
|
1813
|
+
if (parent?.isKind(SyntaxKind11.PropertyAccessExpression)) {
|
|
1814
|
+
const parentProp = parent.asKindOrThrow(SyntaxKind11.PropertyAccessExpression);
|
|
1815
|
+
if (parentProp.getExpression() === expr && parentProp.getName() === "value") {
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
source.appendLeft(expr.getEnd(), ".value");
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// src/compiler.ts
|
|
1824
|
+
function compile(source, optionsOrFilename) {
|
|
1825
|
+
const options = typeof optionsOrFilename === "string" ? { filename: optionsOrFilename } : optionsOrFilename ?? {};
|
|
1826
|
+
const filename = options.filename ?? "input.tsx";
|
|
1827
|
+
const project = new Project({
|
|
1828
|
+
useInMemoryFileSystem: true,
|
|
1829
|
+
compilerOptions: {
|
|
1830
|
+
jsx: ts.JsxEmit.Preserve,
|
|
1831
|
+
strict: true
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1834
|
+
const sourceFile = project.createSourceFile(filename, source);
|
|
1835
|
+
const s = new MagicString(source);
|
|
1836
|
+
const allDiagnostics = [];
|
|
1837
|
+
const componentAnalyzer = new ComponentAnalyzer;
|
|
1838
|
+
const components = componentAnalyzer.analyze(sourceFile);
|
|
1839
|
+
if (components.length === 0) {
|
|
1840
|
+
return {
|
|
1841
|
+
code: source,
|
|
1842
|
+
map: s.generateMap({ source: filename, includeContent: true }),
|
|
1843
|
+
diagnostics: []
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
const usedFeatures = new Set;
|
|
1847
|
+
for (const component of components) {
|
|
1848
|
+
const reactivityAnalyzer = new ReactivityAnalyzer;
|
|
1849
|
+
const variables = reactivityAnalyzer.analyze(sourceFile, component);
|
|
1850
|
+
const hasSignals = variables.some((v) => v.kind === "signal");
|
|
1851
|
+
const hasComputeds = variables.some((v) => v.kind === "computed");
|
|
1852
|
+
if (hasSignals)
|
|
1853
|
+
usedFeatures.add("signal");
|
|
1854
|
+
if (hasComputeds)
|
|
1855
|
+
usedFeatures.add("computed");
|
|
1856
|
+
const mutationAnalyzer = new MutationAnalyzer;
|
|
1857
|
+
const mutations = mutationAnalyzer.analyze(sourceFile, component, variables);
|
|
1858
|
+
const mutationRanges = mutations.map((m) => ({ start: m.start, end: m.end }));
|
|
1859
|
+
if (mutations.length > 0) {
|
|
1860
|
+
const mutationTransformer = new MutationTransformer;
|
|
1861
|
+
mutationTransformer.transform(s, component, mutations);
|
|
1862
|
+
}
|
|
1863
|
+
const signalTransformer = new SignalTransformer;
|
|
1864
|
+
signalTransformer.transform(s, sourceFile, component, variables, mutationRanges);
|
|
1865
|
+
const computedTransformer = new ComputedTransformer;
|
|
1866
|
+
computedTransformer.transform(s, sourceFile, component, variables);
|
|
1867
|
+
const jsxAnalyzer = new JsxAnalyzer;
|
|
1868
|
+
const jsxExpressions = jsxAnalyzer.analyze(sourceFile, component, variables);
|
|
1869
|
+
if (jsxExpressions.length > 0) {
|
|
1870
|
+
usedFeatures.add("__element");
|
|
1871
|
+
usedFeatures.add("__child");
|
|
1872
|
+
}
|
|
1873
|
+
if (jsxExpressions.some((e) => e.reactive)) {
|
|
1874
|
+
usedFeatures.add("__text");
|
|
1875
|
+
usedFeatures.add("__attr");
|
|
1876
|
+
}
|
|
1877
|
+
const hasEvents = sourceFile.getDescendantsOfKind(SyntaxKind12.JsxAttribute).some((attr) => {
|
|
1878
|
+
const name = attr.getNameNode().getText();
|
|
1879
|
+
return name.startsWith("on") && name.length > 2;
|
|
1880
|
+
});
|
|
1881
|
+
if (hasEvents) {
|
|
1882
|
+
usedFeatures.add("__on");
|
|
1883
|
+
}
|
|
1884
|
+
const jsxTransformer = new JsxTransformer;
|
|
1885
|
+
jsxTransformer.transform(s, sourceFile, component, variables, jsxExpressions);
|
|
1886
|
+
const mutationDiags = new MutationDiagnostics;
|
|
1887
|
+
allDiagnostics.push(...mutationDiags.analyze(sourceFile, component, variables));
|
|
1888
|
+
const ssrDiags = new SSRSafetyDiagnostics;
|
|
1889
|
+
allDiagnostics.push(...ssrDiags.analyze(sourceFile, component));
|
|
1890
|
+
}
|
|
1891
|
+
const propsDiags = new PropsDestructuringDiagnostics;
|
|
1892
|
+
allDiagnostics.push(...propsDiags.analyze(sourceFile, components));
|
|
1893
|
+
detectDomHelpers(s.toString(), usedFeatures);
|
|
1894
|
+
const target = options.target ?? "dom";
|
|
1895
|
+
const imports = buildImportStatement(usedFeatures, target);
|
|
1896
|
+
if (imports) {
|
|
1897
|
+
s.prepend(`${imports}
|
|
1898
|
+
`);
|
|
1899
|
+
}
|
|
1900
|
+
const map = s.generateMap({
|
|
1901
|
+
source: filename,
|
|
1902
|
+
includeContent: true
|
|
1903
|
+
});
|
|
1904
|
+
return {
|
|
1905
|
+
code: s.toString(),
|
|
1906
|
+
map,
|
|
1907
|
+
diagnostics: allDiagnostics
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
var DOM_HELPERS = [
|
|
1911
|
+
"__append",
|
|
1912
|
+
"__child",
|
|
1913
|
+
"__enterChildren",
|
|
1914
|
+
"__exitChildren",
|
|
1915
|
+
"__insert",
|
|
1916
|
+
"__conditional",
|
|
1917
|
+
"__list",
|
|
1918
|
+
"__show",
|
|
1919
|
+
"__classList",
|
|
1920
|
+
"__staticText"
|
|
1921
|
+
];
|
|
1922
|
+
function detectDomHelpers(output, usedFeatures) {
|
|
1923
|
+
for (const helper of DOM_HELPERS) {
|
|
1924
|
+
if (output.includes(`${helper}(`)) {
|
|
1925
|
+
usedFeatures.add(helper);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
function buildImportStatement(features, target) {
|
|
1930
|
+
const runtimeImports = [];
|
|
1931
|
+
const domImports = [];
|
|
1932
|
+
for (const feature of features) {
|
|
1933
|
+
if (feature === "signal" || feature === "computed" || feature === "effect" || feature === "batch" || feature === "untrack") {
|
|
1934
|
+
runtimeImports.push(feature);
|
|
1935
|
+
} else if (feature.startsWith("__")) {
|
|
1936
|
+
domImports.push(feature);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
const internalsSource = target === "tui" ? "@vertz/tui/internals" : "@vertz/ui/internals";
|
|
1940
|
+
const parts = [];
|
|
1941
|
+
if (runtimeImports.length > 0) {
|
|
1942
|
+
parts.push(`import { ${runtimeImports.sort().join(", ")} } from '@vertz/ui';`);
|
|
1943
|
+
}
|
|
1944
|
+
if (domImports.length > 0) {
|
|
1945
|
+
parts.push(`import { ${domImports.sort().join(", ")} } from '${internalsSource}';`);
|
|
1946
|
+
}
|
|
1947
|
+
return parts.length > 0 ? parts.join(`
|
|
1948
|
+
`) : null;
|
|
1949
|
+
}
|
|
1950
|
+
// src/css-extraction/code-splitting.ts
|
|
1951
|
+
class CSSCodeSplitter {
|
|
1952
|
+
split(manifest, fileExtractions) {
|
|
1953
|
+
const fileRouteCount = new Map;
|
|
1954
|
+
for (const [_route, files] of manifest) {
|
|
1955
|
+
for (const filePath of files) {
|
|
1956
|
+
fileRouteCount.set(filePath, (fileRouteCount.get(filePath) ?? 0) + 1);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
const sharedFiles = new Set;
|
|
1960
|
+
for (const [filePath, count] of fileRouteCount) {
|
|
1961
|
+
if (count > 1) {
|
|
1962
|
+
sharedFiles.add(filePath);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
const result = {};
|
|
1966
|
+
const commonCSS = [];
|
|
1967
|
+
for (const filePath of sharedFiles) {
|
|
1968
|
+
const extraction = fileExtractions.get(filePath);
|
|
1969
|
+
if (extraction && extraction.css.length > 0) {
|
|
1970
|
+
commonCSS.push(extraction.css);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
if (commonCSS.length > 0) {
|
|
1974
|
+
result.__common = commonCSS.join(`
|
|
1975
|
+
`);
|
|
1976
|
+
}
|
|
1977
|
+
for (const [route, files] of manifest) {
|
|
1978
|
+
const routeCSS = [];
|
|
1979
|
+
for (const filePath of files) {
|
|
1980
|
+
if (sharedFiles.has(filePath))
|
|
1981
|
+
continue;
|
|
1982
|
+
const extraction = fileExtractions.get(filePath);
|
|
1983
|
+
if (extraction && extraction.css.length > 0) {
|
|
1984
|
+
routeCSS.push(extraction.css);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
result[route] = routeCSS.join(`
|
|
1988
|
+
`);
|
|
1989
|
+
}
|
|
1990
|
+
return result;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
// src/css-extraction/dead-css.ts
|
|
1994
|
+
class DeadCSSEliminator {
|
|
1995
|
+
eliminate(extractions, usedFiles) {
|
|
1996
|
+
const liveCSS = [];
|
|
1997
|
+
for (const [filePath, extraction] of extractions) {
|
|
1998
|
+
if (usedFiles.has(filePath) && extraction.css.length > 0) {
|
|
1999
|
+
liveCSS.push(extraction.css);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
return liveCSS.join(`
|
|
2003
|
+
`);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
// src/css-extraction/extractor.ts
|
|
2007
|
+
import {
|
|
2008
|
+
ALIGNMENT_MAP,
|
|
2009
|
+
COLOR_NAMESPACES,
|
|
2010
|
+
CONTENT_MAP,
|
|
2011
|
+
CSS_COLOR_KEYWORDS,
|
|
2012
|
+
DISPLAY_MAP,
|
|
2013
|
+
FONT_SIZE_SCALE,
|
|
2014
|
+
FONT_WEIGHT_SCALE,
|
|
2015
|
+
HEIGHT_AXIS_PROPERTIES,
|
|
2016
|
+
KEYWORD_MAP,
|
|
2017
|
+
LINE_HEIGHT_SCALE,
|
|
2018
|
+
PROPERTY_MAP,
|
|
2019
|
+
PSEUDO_MAP,
|
|
2020
|
+
PSEUDO_PREFIXES,
|
|
2021
|
+
RADIUS_SCALE,
|
|
2022
|
+
SHADOW_SCALE,
|
|
2023
|
+
SIZE_KEYWORDS,
|
|
2024
|
+
SPACING_SCALE
|
|
2025
|
+
} from "@vertz/ui/internals";
|
|
2026
|
+
import { Project as Project2, SyntaxKind as SyntaxKind13, ts as ts2 } from "ts-morph";
|
|
2027
|
+
|
|
2028
|
+
class CSSExtractor {
|
|
2029
|
+
extract(source, filePath) {
|
|
2030
|
+
const project = new Project2({
|
|
2031
|
+
useInMemoryFileSystem: true,
|
|
2032
|
+
compilerOptions: { jsx: ts2.JsxEmit.Preserve, strict: true }
|
|
2033
|
+
});
|
|
2034
|
+
const sourceFile = project.createSourceFile(filePath, source);
|
|
2035
|
+
const allCssRules = [];
|
|
2036
|
+
const allBlockNames = [];
|
|
2037
|
+
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind13.CallExpression);
|
|
2038
|
+
for (const call of callExpressions) {
|
|
2039
|
+
const expression = call.getExpression();
|
|
2040
|
+
if (!expression.isKind(SyntaxKind13.Identifier) || expression.getText() !== "css") {
|
|
2041
|
+
continue;
|
|
2042
|
+
}
|
|
2043
|
+
const args = call.getArguments();
|
|
2044
|
+
if (args.length === 0)
|
|
2045
|
+
continue;
|
|
2046
|
+
const firstArg = args[0];
|
|
2047
|
+
if (!firstArg.isKind(SyntaxKind13.ObjectLiteralExpression))
|
|
2048
|
+
continue;
|
|
2049
|
+
if (!isStaticCSSCall(firstArg))
|
|
2050
|
+
continue;
|
|
2051
|
+
for (const prop of firstArg.getProperties()) {
|
|
2052
|
+
if (!prop.isKind(SyntaxKind13.PropertyAssignment))
|
|
2053
|
+
continue;
|
|
2054
|
+
const blockName = prop.getName();
|
|
2055
|
+
allBlockNames.push(blockName);
|
|
2056
|
+
const className = generateClassName(filePath, blockName);
|
|
2057
|
+
const init = prop.getInitializer();
|
|
2058
|
+
if (!init || !init.isKind(SyntaxKind13.ArrayLiteralExpression))
|
|
2059
|
+
continue;
|
|
2060
|
+
const entries = extractEntries(init);
|
|
2061
|
+
const rules = buildCSSRules(className, entries);
|
|
2062
|
+
allCssRules.push(...rules);
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
return {
|
|
2066
|
+
css: allCssRules.join(`
|
|
2067
|
+
`),
|
|
2068
|
+
blockNames: allBlockNames
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
function isStaticCSSCall(node) {
|
|
2073
|
+
if (!node.isKind(SyntaxKind13.ObjectLiteralExpression))
|
|
2074
|
+
return false;
|
|
2075
|
+
for (const prop of node.getProperties()) {
|
|
2076
|
+
if (!prop.isKind(SyntaxKind13.PropertyAssignment))
|
|
2077
|
+
return false;
|
|
2078
|
+
const initializer = prop.getInitializer();
|
|
2079
|
+
if (!initializer || !initializer.isKind(SyntaxKind13.ArrayLiteralExpression))
|
|
2080
|
+
return false;
|
|
2081
|
+
for (const element of initializer.getElements()) {
|
|
2082
|
+
if (element.isKind(SyntaxKind13.StringLiteral))
|
|
2083
|
+
continue;
|
|
2084
|
+
if (element.isKind(SyntaxKind13.ObjectLiteralExpression)) {
|
|
2085
|
+
if (!isStaticNestedObject(element))
|
|
2086
|
+
return false;
|
|
2087
|
+
continue;
|
|
2088
|
+
}
|
|
2089
|
+
return false;
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
return true;
|
|
2093
|
+
}
|
|
2094
|
+
function isStaticNestedObject(node) {
|
|
2095
|
+
if (!node.isKind(SyntaxKind13.ObjectLiteralExpression))
|
|
2096
|
+
return false;
|
|
2097
|
+
for (const prop of node.getProperties()) {
|
|
2098
|
+
if (!prop.isKind(SyntaxKind13.PropertyAssignment))
|
|
2099
|
+
return false;
|
|
2100
|
+
const init = prop.getInitializer();
|
|
2101
|
+
if (!init || !init.isKind(SyntaxKind13.ArrayLiteralExpression))
|
|
2102
|
+
return false;
|
|
2103
|
+
for (const el of init.getElements()) {
|
|
2104
|
+
if (el.isKind(SyntaxKind13.StringLiteral))
|
|
2105
|
+
continue;
|
|
2106
|
+
if (el.isKind(SyntaxKind13.ObjectLiteralExpression)) {
|
|
2107
|
+
if (isStaticRawDeclaration(el))
|
|
2108
|
+
continue;
|
|
2109
|
+
return false;
|
|
2110
|
+
}
|
|
2111
|
+
return false;
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
return true;
|
|
2115
|
+
}
|
|
2116
|
+
function isStaticRawDeclaration(node) {
|
|
2117
|
+
if (!node.isKind(SyntaxKind13.ObjectLiteralExpression))
|
|
2118
|
+
return false;
|
|
2119
|
+
const props = node.getProperties();
|
|
2120
|
+
if (props.length !== 2)
|
|
2121
|
+
return false;
|
|
2122
|
+
let hasProperty = false;
|
|
2123
|
+
let hasValue = false;
|
|
2124
|
+
for (const prop of props) {
|
|
2125
|
+
if (!prop.isKind(SyntaxKind13.PropertyAssignment))
|
|
2126
|
+
return false;
|
|
2127
|
+
const init = prop.getInitializer();
|
|
2128
|
+
if (!init || !init.isKind(SyntaxKind13.StringLiteral))
|
|
2129
|
+
return false;
|
|
2130
|
+
const name = prop.getName();
|
|
2131
|
+
if (name === "property")
|
|
2132
|
+
hasProperty = true;
|
|
2133
|
+
else if (name === "value")
|
|
2134
|
+
hasValue = true;
|
|
2135
|
+
}
|
|
2136
|
+
return hasProperty && hasValue;
|
|
2137
|
+
}
|
|
2138
|
+
function extractEntries(arrayNode) {
|
|
2139
|
+
if (!arrayNode.isKind(SyntaxKind13.ArrayLiteralExpression))
|
|
2140
|
+
return [];
|
|
2141
|
+
const results = [];
|
|
2142
|
+
for (const element of arrayNode.getElements()) {
|
|
2143
|
+
if (element.isKind(SyntaxKind13.StringLiteral)) {
|
|
2144
|
+
results.push({ kind: "shorthand", value: element.getLiteralValue() });
|
|
2145
|
+
} else if (element.isKind(SyntaxKind13.ObjectLiteralExpression)) {
|
|
2146
|
+
for (const prop of element.getProperties()) {
|
|
2147
|
+
if (!prop.isKind(SyntaxKind13.PropertyAssignment))
|
|
2148
|
+
continue;
|
|
2149
|
+
const selector = prop.getName();
|
|
2150
|
+
const nameNode = prop.getNameNode();
|
|
2151
|
+
const actualSelector = nameNode.isKind(SyntaxKind13.StringLiteral) ? nameNode.getLiteralValue() : selector;
|
|
2152
|
+
const init = prop.getInitializer();
|
|
2153
|
+
if (!init || !init.isKind(SyntaxKind13.ArrayLiteralExpression))
|
|
2154
|
+
continue;
|
|
2155
|
+
const nestedEntries = [];
|
|
2156
|
+
const rawDeclarations = [];
|
|
2157
|
+
for (const el of init.getElements()) {
|
|
2158
|
+
if (el.isKind(SyntaxKind13.StringLiteral)) {
|
|
2159
|
+
nestedEntries.push(el.getLiteralValue());
|
|
2160
|
+
} else if (el.isKind(SyntaxKind13.ObjectLiteralExpression)) {
|
|
2161
|
+
const rawDecl = extractRawDeclaration(el);
|
|
2162
|
+
if (rawDecl)
|
|
2163
|
+
rawDeclarations.push(rawDecl);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
results.push({
|
|
2167
|
+
kind: "nested",
|
|
2168
|
+
value: "",
|
|
2169
|
+
selector: actualSelector,
|
|
2170
|
+
entries: nestedEntries,
|
|
2171
|
+
rawDeclarations
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
return results;
|
|
2177
|
+
}
|
|
2178
|
+
function extractRawDeclaration(node) {
|
|
2179
|
+
if (!node.isKind(SyntaxKind13.ObjectLiteralExpression))
|
|
2180
|
+
return null;
|
|
2181
|
+
let property = null;
|
|
2182
|
+
let value = null;
|
|
2183
|
+
for (const prop of node.getProperties()) {
|
|
2184
|
+
if (!prop.isKind(SyntaxKind13.PropertyAssignment))
|
|
2185
|
+
return null;
|
|
2186
|
+
const name = prop.getName();
|
|
2187
|
+
const init = prop.getInitializer();
|
|
2188
|
+
if (!init || !init.isKind(SyntaxKind13.StringLiteral))
|
|
2189
|
+
return null;
|
|
2190
|
+
if (name === "property")
|
|
2191
|
+
property = init.getLiteralValue();
|
|
2192
|
+
else if (name === "value")
|
|
2193
|
+
value = init.getLiteralValue();
|
|
2194
|
+
}
|
|
2195
|
+
if (property && value)
|
|
2196
|
+
return { property, value };
|
|
2197
|
+
return null;
|
|
2198
|
+
}
|
|
2199
|
+
function buildCSSRules(className, entries) {
|
|
2200
|
+
const rules = [];
|
|
2201
|
+
const baseDecls = [];
|
|
2202
|
+
const pseudoDecls = new Map;
|
|
2203
|
+
for (const entry of entries) {
|
|
2204
|
+
if (entry.kind === "shorthand") {
|
|
2205
|
+
const parsed = parseShorthand(entry.value);
|
|
2206
|
+
if (!parsed)
|
|
2207
|
+
continue;
|
|
2208
|
+
const resolved = resolveDeclarations(parsed);
|
|
2209
|
+
if (!resolved)
|
|
2210
|
+
continue;
|
|
2211
|
+
if (parsed.pseudo) {
|
|
2212
|
+
const existing = pseudoDecls.get(parsed.pseudo) ?? [];
|
|
2213
|
+
existing.push(...resolved);
|
|
2214
|
+
pseudoDecls.set(parsed.pseudo, existing);
|
|
2215
|
+
} else {
|
|
2216
|
+
baseDecls.push(...resolved);
|
|
2217
|
+
}
|
|
2218
|
+
} else if (entry.kind === "nested" && entry.selector && entry.entries) {
|
|
2219
|
+
const nestedDecls = [];
|
|
2220
|
+
for (const nestedEntry of entry.entries) {
|
|
2221
|
+
const parsed = parseShorthand(nestedEntry);
|
|
2222
|
+
if (!parsed)
|
|
2223
|
+
continue;
|
|
2224
|
+
const resolved = resolveDeclarations(parsed);
|
|
2225
|
+
if (!resolved)
|
|
2226
|
+
continue;
|
|
2227
|
+
nestedDecls.push(...resolved);
|
|
2228
|
+
}
|
|
2229
|
+
if (entry.rawDeclarations) {
|
|
2230
|
+
for (const raw of entry.rawDeclarations) {
|
|
2231
|
+
nestedDecls.push(`${raw.property}: ${raw.value};`);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
const resolvedSelector = entry.selector.replaceAll("&", `.${className}`);
|
|
2235
|
+
if (nestedDecls.length > 0) {
|
|
2236
|
+
rules.push(formatCSSRule(resolvedSelector, nestedDecls));
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
if (baseDecls.length > 0) {
|
|
2241
|
+
rules.unshift(formatCSSRule(`.${className}`, baseDecls));
|
|
2242
|
+
}
|
|
2243
|
+
for (const [pseudo, decls] of pseudoDecls) {
|
|
2244
|
+
rules.push(formatCSSRule(`.${className}${pseudo}`, decls));
|
|
2245
|
+
}
|
|
2246
|
+
return rules;
|
|
2247
|
+
}
|
|
2248
|
+
function formatCSSRule(selector, declarations) {
|
|
2249
|
+
const props = declarations.map((d) => ` ${d}`).join(`
|
|
2250
|
+
`);
|
|
2251
|
+
return `${selector} {
|
|
2252
|
+
${props}
|
|
2253
|
+
}`;
|
|
2254
|
+
}
|
|
2255
|
+
function parseShorthand(input) {
|
|
2256
|
+
const parts = input.split(":");
|
|
2257
|
+
if (parts.length === 1) {
|
|
2258
|
+
const [property] = parts;
|
|
2259
|
+
return { property, value: null, pseudo: null };
|
|
2260
|
+
}
|
|
2261
|
+
if (parts.length === 2) {
|
|
2262
|
+
const [a, b] = parts;
|
|
2263
|
+
if (PSEUDO_PREFIXES.has(a)) {
|
|
2264
|
+
return { property: b, value: null, pseudo: PSEUDO_MAP[a] ?? a };
|
|
2265
|
+
}
|
|
2266
|
+
return { property: a, value: b, pseudo: null };
|
|
2267
|
+
}
|
|
2268
|
+
if (parts.length === 3) {
|
|
2269
|
+
const [a, b, c] = parts;
|
|
2270
|
+
if (PSEUDO_PREFIXES.has(a)) {
|
|
2271
|
+
return { property: b, value: c, pseudo: PSEUDO_MAP[a] ?? a };
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
return null;
|
|
2275
|
+
}
|
|
2276
|
+
function resolveDeclarations(parsed) {
|
|
2277
|
+
const { property, value } = parsed;
|
|
2278
|
+
if (DISPLAY_MAP[property] !== undefined && value === null) {
|
|
2279
|
+
return [`display: ${DISPLAY_MAP[property]};`];
|
|
2280
|
+
}
|
|
2281
|
+
const keyword = KEYWORD_MAP[property];
|
|
2282
|
+
if (keyword !== undefined && value === null) {
|
|
2283
|
+
return keyword.map((d) => `${d.property}: ${d.value};`);
|
|
2284
|
+
}
|
|
2285
|
+
const mapping = PROPERTY_MAP[property];
|
|
2286
|
+
if (!mapping || value === null)
|
|
2287
|
+
return null;
|
|
2288
|
+
const resolvedValue = resolveValue(value, mapping.valueType, property);
|
|
2289
|
+
if (resolvedValue === null)
|
|
2290
|
+
return null;
|
|
2291
|
+
return mapping.properties.map((prop) => `${prop}: ${resolvedValue};`);
|
|
2292
|
+
}
|
|
2293
|
+
function resolveValue(value, valueType, property) {
|
|
2294
|
+
switch (valueType) {
|
|
2295
|
+
case "spacing":
|
|
2296
|
+
return SPACING_SCALE[value] ?? null;
|
|
2297
|
+
case "color":
|
|
2298
|
+
return resolveColor(value);
|
|
2299
|
+
case "radius":
|
|
2300
|
+
return RADIUS_SCALE[value] ?? null;
|
|
2301
|
+
case "shadow":
|
|
2302
|
+
return SHADOW_SCALE[value] ?? null;
|
|
2303
|
+
case "size": {
|
|
2304
|
+
if (value === "screen") {
|
|
2305
|
+
return HEIGHT_AXIS_PROPERTIES.has(property) ? "100vh" : "100vw";
|
|
2306
|
+
}
|
|
2307
|
+
return SPACING_SCALE[value] ?? SIZE_KEYWORDS[value] ?? null;
|
|
2308
|
+
}
|
|
2309
|
+
case "alignment":
|
|
2310
|
+
return ALIGNMENT_MAP[value] ?? null;
|
|
2311
|
+
case "font-size":
|
|
2312
|
+
return FONT_SIZE_SCALE[value] ?? null;
|
|
2313
|
+
case "font-weight":
|
|
2314
|
+
return FONT_WEIGHT_SCALE[value] ?? null;
|
|
2315
|
+
case "line-height":
|
|
2316
|
+
return LINE_HEIGHT_SCALE[value] ?? null;
|
|
2317
|
+
case "ring": {
|
|
2318
|
+
const num = Number(value);
|
|
2319
|
+
if (Number.isNaN(num) || num < 0)
|
|
2320
|
+
return null;
|
|
2321
|
+
return `${num}px solid var(--color-ring)`;
|
|
2322
|
+
}
|
|
2323
|
+
case "content":
|
|
2324
|
+
return CONTENT_MAP[value] ?? null;
|
|
2325
|
+
default:
|
|
2326
|
+
return value;
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
function resolveColor(value) {
|
|
2330
|
+
const dotIndex = value.indexOf(".");
|
|
2331
|
+
if (dotIndex !== -1) {
|
|
2332
|
+
const namespace = value.substring(0, dotIndex);
|
|
2333
|
+
const shade = value.substring(dotIndex + 1);
|
|
2334
|
+
if (COLOR_NAMESPACES.has(namespace)) {
|
|
2335
|
+
return `var(--color-${namespace}-${shade})`;
|
|
2336
|
+
}
|
|
2337
|
+
return null;
|
|
2338
|
+
}
|
|
2339
|
+
if (COLOR_NAMESPACES.has(value)) {
|
|
2340
|
+
return `var(--color-${value})`;
|
|
2341
|
+
}
|
|
2342
|
+
if (CSS_COLOR_KEYWORDS.has(value))
|
|
2343
|
+
return value;
|
|
2344
|
+
return null;
|
|
2345
|
+
}
|
|
2346
|
+
function generateClassName(filePath, blockName) {
|
|
2347
|
+
const input = `${filePath}::${blockName}`;
|
|
2348
|
+
const hash = djb2Hash(input);
|
|
2349
|
+
return `_${hash}`;
|
|
2350
|
+
}
|
|
2351
|
+
function djb2Hash(str) {
|
|
2352
|
+
let hash = 5381;
|
|
2353
|
+
for (let i = 0;i < str.length; i++) {
|
|
2354
|
+
hash = (hash << 5) + hash + str.charCodeAt(i) | 0;
|
|
2355
|
+
}
|
|
2356
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
2357
|
+
}
|
|
2358
|
+
// src/css-extraction/hmr.ts
|
|
2359
|
+
class CSSHMRHandler {
|
|
2360
|
+
cssCache = new Map;
|
|
2361
|
+
register(filePath, css) {
|
|
2362
|
+
this.cssCache.set(filePath, css);
|
|
2363
|
+
}
|
|
2364
|
+
update(filePath, newCSS) {
|
|
2365
|
+
const previousCSS = this.cssCache.get(filePath);
|
|
2366
|
+
if (previousCSS === newCSS) {
|
|
2367
|
+
return {
|
|
2368
|
+
hasChanged: false,
|
|
2369
|
+
css: newCSS,
|
|
2370
|
+
affectedFiles: []
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
2373
|
+
this.cssCache.set(filePath, newCSS);
|
|
2374
|
+
return {
|
|
2375
|
+
hasChanged: true,
|
|
2376
|
+
css: newCSS,
|
|
2377
|
+
affectedFiles: [filePath]
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2380
|
+
remove(filePath) {
|
|
2381
|
+
this.cssCache.delete(filePath);
|
|
2382
|
+
}
|
|
2383
|
+
getSnapshot() {
|
|
2384
|
+
const parts = [];
|
|
2385
|
+
for (const [_filePath, css] of this.cssCache) {
|
|
2386
|
+
if (css.length > 0) {
|
|
2387
|
+
parts.push(css);
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
return parts.join(`
|
|
2391
|
+
`);
|
|
2392
|
+
}
|
|
2393
|
+
get size() {
|
|
2394
|
+
return this.cssCache.size;
|
|
2395
|
+
}
|
|
2396
|
+
clear() {
|
|
2397
|
+
this.cssCache.clear();
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
// src/css-extraction/route-css-manifest.ts
|
|
2401
|
+
class RouteCSSManifest {
|
|
2402
|
+
build(routeToFiles, fileExtractions) {
|
|
2403
|
+
const manifest = new Map;
|
|
2404
|
+
for (const [route, files] of routeToFiles) {
|
|
2405
|
+
const cssFiles = [];
|
|
2406
|
+
for (const filePath of files) {
|
|
2407
|
+
const extraction = fileExtractions.get(filePath);
|
|
2408
|
+
if (extraction && extraction.css.length > 0) {
|
|
2409
|
+
cssFiles.push(filePath);
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
manifest.set(route, cssFiles);
|
|
2413
|
+
}
|
|
2414
|
+
return manifest;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
// src/diagnostics/css-diagnostics.ts
|
|
2418
|
+
import {
|
|
2419
|
+
COLOR_NAMESPACES as COLOR_NAMESPACES2,
|
|
2420
|
+
CSS_COLOR_KEYWORDS as CSS_COLOR_KEYWORDS2,
|
|
2421
|
+
KEYWORD_MAP as KEYWORD_MAP2,
|
|
2422
|
+
PROPERTY_MAP as PROPERTY_MAP2,
|
|
2423
|
+
PSEUDO_PREFIXES as PSEUDO_PREFIXES2,
|
|
2424
|
+
SPACING_SCALE as SPACING_SCALE2
|
|
2425
|
+
} from "@vertz/ui/internals";
|
|
2426
|
+
import { SyntaxKind as SyntaxKind14 } from "ts-morph";
|
|
2427
|
+
var KNOWN_PROPERTIES = new Set([...Object.keys(PROPERTY_MAP2), ...Object.keys(KEYWORD_MAP2)]);
|
|
2428
|
+
var SPACING_VALUES = new Set(Object.keys(SPACING_SCALE2));
|
|
2429
|
+
var SPACING_PROPERTIES = new Set(Object.entries(PROPERTY_MAP2).filter(([_, mapping]) => mapping.valueType === "spacing").map(([key]) => key));
|
|
2430
|
+
|
|
2431
|
+
class CSSDiagnostics {
|
|
2432
|
+
analyze(sourceFile) {
|
|
2433
|
+
const diagnostics = [];
|
|
2434
|
+
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind14.CallExpression);
|
|
2435
|
+
for (const call of callExpressions) {
|
|
2436
|
+
const expression = call.getExpression();
|
|
2437
|
+
if (!expression.isKind(SyntaxKind14.Identifier) || expression.getText() !== "css") {
|
|
2438
|
+
continue;
|
|
2439
|
+
}
|
|
2440
|
+
const args = call.getArguments();
|
|
2441
|
+
if (args.length === 0)
|
|
2442
|
+
continue;
|
|
2443
|
+
const firstArg = args[0];
|
|
2444
|
+
if (!firstArg || !firstArg.isKind(SyntaxKind14.ObjectLiteralExpression))
|
|
2445
|
+
continue;
|
|
2446
|
+
for (const prop of firstArg.getProperties()) {
|
|
2447
|
+
if (!prop.isKind(SyntaxKind14.PropertyAssignment))
|
|
2448
|
+
continue;
|
|
2449
|
+
const init = prop.getInitializer();
|
|
2450
|
+
if (!init || !init.isKind(SyntaxKind14.ArrayLiteralExpression))
|
|
2451
|
+
continue;
|
|
2452
|
+
for (const element of init.getElements()) {
|
|
2453
|
+
if (!element.isKind(SyntaxKind14.StringLiteral))
|
|
2454
|
+
continue;
|
|
2455
|
+
const value = element.getLiteralValue();
|
|
2456
|
+
const pos = sourceFile.getLineAndColumnAtPos(element.getStart());
|
|
2457
|
+
diagnostics.push(...this.validateShorthand(value, pos.line, pos.column - 1));
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
return diagnostics;
|
|
2462
|
+
}
|
|
2463
|
+
validateShorthand(input, line, column) {
|
|
2464
|
+
const diagnostics = [];
|
|
2465
|
+
const parts = input.split(":");
|
|
2466
|
+
if (parts.length === 0 || parts.length === 1 && parts[0] === "") {
|
|
2467
|
+
diagnostics.push({
|
|
2468
|
+
code: "css-empty-shorthand",
|
|
2469
|
+
message: "Empty shorthand string",
|
|
2470
|
+
severity: "error",
|
|
2471
|
+
line,
|
|
2472
|
+
column
|
|
2473
|
+
});
|
|
2474
|
+
return diagnostics;
|
|
2475
|
+
}
|
|
2476
|
+
let property;
|
|
2477
|
+
let value;
|
|
2478
|
+
let pseudo;
|
|
2479
|
+
if (parts.length === 1) {
|
|
2480
|
+
const [p] = parts;
|
|
2481
|
+
property = p;
|
|
2482
|
+
} else if (parts.length === 2) {
|
|
2483
|
+
const [a, b] = parts;
|
|
2484
|
+
if (PSEUDO_PREFIXES2.has(a)) {
|
|
2485
|
+
pseudo = a;
|
|
2486
|
+
property = b;
|
|
2487
|
+
} else {
|
|
2488
|
+
property = a;
|
|
2489
|
+
value = b;
|
|
2490
|
+
}
|
|
2491
|
+
} else if (parts.length === 3) {
|
|
2492
|
+
const [a, b, c] = parts;
|
|
2493
|
+
pseudo = a;
|
|
2494
|
+
property = b;
|
|
2495
|
+
value = c;
|
|
2496
|
+
} else {
|
|
2497
|
+
diagnostics.push({
|
|
2498
|
+
code: "css-malformed-shorthand",
|
|
2499
|
+
message: `Malformed shorthand '${input}': too many segments. Expected 'property:value' or 'pseudo:property:value'.`,
|
|
2500
|
+
severity: "error",
|
|
2501
|
+
line,
|
|
2502
|
+
column
|
|
2503
|
+
});
|
|
2504
|
+
return diagnostics;
|
|
2505
|
+
}
|
|
2506
|
+
if (pseudo && !PSEUDO_PREFIXES2.has(pseudo)) {
|
|
2507
|
+
diagnostics.push({
|
|
2508
|
+
code: "css-unknown-pseudo",
|
|
2509
|
+
message: `Unknown pseudo prefix '${pseudo}'. Supported: ${[...PSEUDO_PREFIXES2].join(", ")}`,
|
|
2510
|
+
severity: "error",
|
|
2511
|
+
line,
|
|
2512
|
+
column,
|
|
2513
|
+
fix: `Use one of: ${[...PSEUDO_PREFIXES2].join(", ")}`
|
|
2514
|
+
});
|
|
2515
|
+
}
|
|
2516
|
+
if (!KNOWN_PROPERTIES.has(property)) {
|
|
2517
|
+
diagnostics.push({
|
|
2518
|
+
code: "css-unknown-property",
|
|
2519
|
+
message: `Unknown CSS shorthand property '${property}'.`,
|
|
2520
|
+
severity: "error",
|
|
2521
|
+
line,
|
|
2522
|
+
column,
|
|
2523
|
+
fix: `Available properties: ${[...KNOWN_PROPERTIES].join(", ")}`
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
if (value && SPACING_PROPERTIES.has(property) && !SPACING_VALUES.has(value)) {
|
|
2527
|
+
diagnostics.push({
|
|
2528
|
+
code: "css-invalid-spacing",
|
|
2529
|
+
message: `Invalid spacing value '${value}' for '${property}'. Use the spacing scale (0, 1, 2, 4, 8, etc.).`,
|
|
2530
|
+
severity: "error",
|
|
2531
|
+
line,
|
|
2532
|
+
column,
|
|
2533
|
+
fix: `Use a spacing scale value: ${[...SPACING_VALUES].join(", ")}`
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
if (value && (property === "bg" || property === "text" || property === "border")) {
|
|
2537
|
+
this.validateColorToken(value, property, line, column, diagnostics);
|
|
2538
|
+
}
|
|
2539
|
+
return diagnostics;
|
|
2540
|
+
}
|
|
2541
|
+
validateColorToken(value, property, line, column, diagnostics) {
|
|
2542
|
+
if (CSS_COLOR_KEYWORDS2.has(value))
|
|
2543
|
+
return;
|
|
2544
|
+
const dotIndex = value.indexOf(".");
|
|
2545
|
+
if (dotIndex !== -1) {
|
|
2546
|
+
const namespace = value.substring(0, dotIndex);
|
|
2547
|
+
if (!COLOR_NAMESPACES2.has(namespace)) {
|
|
2548
|
+
diagnostics.push({
|
|
2549
|
+
code: "css-unknown-color-token",
|
|
2550
|
+
message: `Unknown color token namespace '${namespace}' in '${property}:${value}'. Known: ${[...COLOR_NAMESPACES2].join(", ")}`,
|
|
2551
|
+
severity: "error",
|
|
2552
|
+
line,
|
|
2553
|
+
column,
|
|
2554
|
+
fix: `Use a known color namespace: ${[...COLOR_NAMESPACES2].join(", ")}`
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
return;
|
|
2558
|
+
}
|
|
2559
|
+
if (!COLOR_NAMESPACES2.has(value)) {
|
|
2560
|
+
diagnostics.push({
|
|
2561
|
+
code: "css-unknown-color-token",
|
|
2562
|
+
message: `Unknown color token '${value}' for '${property}'. Use a design token (e.g. 'primary', 'background') or shade notation (e.g. 'primary.700').`,
|
|
2563
|
+
severity: "error",
|
|
2564
|
+
line,
|
|
2565
|
+
column,
|
|
2566
|
+
fix: `Use a known color token: ${[...COLOR_NAMESPACES2].join(", ")}`
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
// src/library-plugin.ts
|
|
2572
|
+
import { readFile } from "node:fs/promises";
|
|
2573
|
+
import remapping from "@ampproject/remapping";
|
|
2574
|
+
import MagicString2 from "magic-string";
|
|
2575
|
+
import { Project as Project3, ts as ts3 } from "ts-morph";
|
|
2576
|
+
|
|
2577
|
+
// src/transformers/hydration-transformer.ts
|
|
2578
|
+
import { SyntaxKind as SyntaxKind15 } from "ts-morph";
|
|
2579
|
+
class HydrationTransformer {
|
|
2580
|
+
transform(s, sourceFile) {
|
|
2581
|
+
const componentAnalyzer = new ComponentAnalyzer;
|
|
2582
|
+
const components = componentAnalyzer.analyze(sourceFile);
|
|
2583
|
+
for (const component of components) {
|
|
2584
|
+
if (this._isInteractive(sourceFile, component)) {
|
|
2585
|
+
this._addHydrationMarker(s, sourceFile, component);
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
_isInteractive(sourceFile, component) {
|
|
2590
|
+
const bodyNode = findBodyNode(sourceFile, component);
|
|
2591
|
+
if (!bodyNode)
|
|
2592
|
+
return false;
|
|
2593
|
+
for (const stmt of bodyNode.getChildSyntaxList()?.getChildren() ?? []) {
|
|
2594
|
+
if (!stmt.isKind(SyntaxKind15.VariableStatement))
|
|
2595
|
+
continue;
|
|
2596
|
+
const declList = stmt.getChildrenOfKind(SyntaxKind15.VariableDeclarationList)[0];
|
|
2597
|
+
if (!declList)
|
|
2598
|
+
continue;
|
|
2599
|
+
if (declList.getText().startsWith("let ")) {
|
|
2600
|
+
return true;
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
return false;
|
|
2604
|
+
}
|
|
2605
|
+
_addHydrationMarker(s, sourceFile, component) {
|
|
2606
|
+
const bodyNode = findBodyNode(sourceFile, component);
|
|
2607
|
+
if (!bodyNode)
|
|
2608
|
+
return;
|
|
2609
|
+
const returnStmts = bodyNode.getDescendantsOfKind(SyntaxKind15.ReturnStatement);
|
|
2610
|
+
for (const ret of returnStmts) {
|
|
2611
|
+
const expr = ret.getExpression();
|
|
2612
|
+
if (!expr)
|
|
2613
|
+
continue;
|
|
2614
|
+
const rootJsx = this._findRootJsx(expr);
|
|
2615
|
+
if (rootJsx) {
|
|
2616
|
+
this._injectAttribute(s, rootJsx, component.name);
|
|
2617
|
+
return;
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
const arrowBodies = sourceFile.getDescendantsOfKind(SyntaxKind15.ArrowFunction);
|
|
2621
|
+
for (const arrow of arrowBodies) {
|
|
2622
|
+
const body = arrow.getBody();
|
|
2623
|
+
if (body.getStart() === component.bodyStart && body.getEnd() === component.bodyEnd) {
|
|
2624
|
+
const rootJsx = this._findRootJsx(body);
|
|
2625
|
+
if (rootJsx) {
|
|
2626
|
+
this._injectAttribute(s, rootJsx, component.name);
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
_findRootJsx(node) {
|
|
2633
|
+
if (node.isKind(SyntaxKind15.JsxElement) || node.isKind(SyntaxKind15.JsxSelfClosingElement)) {
|
|
2634
|
+
return node;
|
|
2635
|
+
}
|
|
2636
|
+
if (node.isKind(SyntaxKind15.ParenthesizedExpression)) {
|
|
2637
|
+
const inner = node.getExpression();
|
|
2638
|
+
return this._findRootJsx(inner);
|
|
2639
|
+
}
|
|
2640
|
+
for (const child of node.getChildren()) {
|
|
2641
|
+
const found = this._findRootJsx(child);
|
|
2642
|
+
if (found)
|
|
2643
|
+
return found;
|
|
2644
|
+
}
|
|
2645
|
+
return null;
|
|
2646
|
+
}
|
|
2647
|
+
_injectAttribute(s, jsxNode, componentName) {
|
|
2648
|
+
if (jsxNode.isKind(SyntaxKind15.JsxSelfClosingElement)) {
|
|
2649
|
+
const tagName = jsxNode.getChildrenOfKind(SyntaxKind15.Identifier)[0];
|
|
2650
|
+
if (tagName) {
|
|
2651
|
+
const insertPos = tagName.getEnd();
|
|
2652
|
+
s.appendLeft(insertPos, ` data-v-id="${componentName}"`);
|
|
2653
|
+
}
|
|
2654
|
+
} else if (jsxNode.isKind(SyntaxKind15.JsxElement)) {
|
|
2655
|
+
const openingElement = jsxNode.getChildrenOfKind(SyntaxKind15.JsxOpeningElement)[0];
|
|
2656
|
+
if (openingElement) {
|
|
2657
|
+
const tagName = openingElement.getChildrenOfKind(SyntaxKind15.Identifier)[0];
|
|
2658
|
+
if (tagName) {
|
|
2659
|
+
const insertPos = tagName.getEnd();
|
|
2660
|
+
s.appendLeft(insertPos, ` data-v-id="${componentName}"`);
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
// src/library-plugin.ts
|
|
2668
|
+
function createVertzLibraryPlugin(options) {
|
|
2669
|
+
const filter = options?.filter ?? /\.tsx$/;
|
|
2670
|
+
return {
|
|
2671
|
+
name: "vertz-library-plugin",
|
|
2672
|
+
setup(build) {
|
|
2673
|
+
build.onLoad({ filter }, async (args) => {
|
|
2674
|
+
const source = await readFile(args.path, "utf-8");
|
|
2675
|
+
const hydrationS = new MagicString2(source);
|
|
2676
|
+
const hydrationProject = new Project3({
|
|
2677
|
+
useInMemoryFileSystem: true,
|
|
2678
|
+
compilerOptions: {
|
|
2679
|
+
jsx: ts3.JsxEmit.Preserve,
|
|
2680
|
+
strict: true
|
|
2681
|
+
}
|
|
2682
|
+
});
|
|
2683
|
+
const hydrationSourceFile = hydrationProject.createSourceFile(args.path, source);
|
|
2684
|
+
const hydrationTransformer = new HydrationTransformer;
|
|
2685
|
+
hydrationTransformer.transform(hydrationS, hydrationSourceFile);
|
|
2686
|
+
const hydratedCode = hydrationS.toString();
|
|
2687
|
+
const hydrationMap = hydrationS.generateMap({
|
|
2688
|
+
source: args.path,
|
|
2689
|
+
includeContent: true
|
|
2690
|
+
});
|
|
2691
|
+
const compileResult = compile(hydratedCode, {
|
|
2692
|
+
filename: args.path,
|
|
2693
|
+
target: options?.target
|
|
2694
|
+
});
|
|
2695
|
+
const errors = compileResult.diagnostics.filter((d) => d.severity === "error");
|
|
2696
|
+
if (errors.length > 0) {
|
|
2697
|
+
const messages = errors.map((d) => `${d.code}: ${d.message} (line ${d.line})`);
|
|
2698
|
+
throw new Error(`Vertz compilation errors in ${args.path}:
|
|
2699
|
+
${messages.join(`
|
|
2700
|
+
`)}`);
|
|
2701
|
+
}
|
|
2702
|
+
for (const d of compileResult.diagnostics) {
|
|
2703
|
+
if (d.severity === "warning") {
|
|
2704
|
+
console.warn(`[vertz-library-plugin] ${args.path}:${d.line} ${d.code}: ${d.message}`);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
const remapped = remapping([compileResult.map, hydrationMap], () => null);
|
|
2708
|
+
const mapBase64 = Buffer.from(remapped.toString()).toString("base64");
|
|
2709
|
+
const sourceMapComment = `
|
|
2710
|
+
//# sourceMappingURL=data:application/json;base64,${mapBase64}`;
|
|
2711
|
+
const contents = compileResult.code + sourceMapComment;
|
|
2712
|
+
return { contents, loader: "tsx" };
|
|
2713
|
+
});
|
|
2714
|
+
}
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
// src/transformers/css-transformer.ts
|
|
2718
|
+
import {
|
|
2719
|
+
ALIGNMENT_MAP as ALIGNMENT_MAP2,
|
|
2720
|
+
COLOR_NAMESPACES as COLOR_NAMESPACES3,
|
|
2721
|
+
CONTENT_MAP as CONTENT_MAP2,
|
|
2722
|
+
CSS_COLOR_KEYWORDS as CSS_COLOR_KEYWORDS3,
|
|
2723
|
+
DISPLAY_MAP as DISPLAY_MAP2,
|
|
2724
|
+
FONT_SIZE_SCALE as FONT_SIZE_SCALE2,
|
|
2725
|
+
FONT_WEIGHT_SCALE as FONT_WEIGHT_SCALE2,
|
|
2726
|
+
HEIGHT_AXIS_PROPERTIES as HEIGHT_AXIS_PROPERTIES2,
|
|
2727
|
+
KEYWORD_MAP as KEYWORD_MAP3,
|
|
2728
|
+
LINE_HEIGHT_SCALE as LINE_HEIGHT_SCALE2,
|
|
2729
|
+
PROPERTY_MAP as PROPERTY_MAP3,
|
|
2730
|
+
PSEUDO_MAP as PSEUDO_MAP2,
|
|
2731
|
+
PSEUDO_PREFIXES as PSEUDO_PREFIXES3,
|
|
2732
|
+
RADIUS_SCALE as RADIUS_SCALE2,
|
|
2733
|
+
SHADOW_SCALE as SHADOW_SCALE2,
|
|
2734
|
+
SIZE_KEYWORDS as SIZE_KEYWORDS2,
|
|
2735
|
+
SPACING_SCALE as SPACING_SCALE3
|
|
2736
|
+
} from "@vertz/ui/internals";
|
|
2737
|
+
import { SyntaxKind as SyntaxKind16 } from "ts-morph";
|
|
2738
|
+
|
|
2739
|
+
class CSSTransformer {
|
|
2740
|
+
transform(s, sourceFile, cssCalls, filePath) {
|
|
2741
|
+
const allCssRules = [];
|
|
2742
|
+
const classNameMaps = new Map;
|
|
2743
|
+
const sortedCalls = [...cssCalls].sort((a, b) => b.start - a.start);
|
|
2744
|
+
for (const call of sortedCalls) {
|
|
2745
|
+
if (call.kind !== "static")
|
|
2746
|
+
continue;
|
|
2747
|
+
const callIndex = cssCalls.indexOf(call);
|
|
2748
|
+
const { classNames, cssRules } = this.processStaticCall(sourceFile, call, filePath);
|
|
2749
|
+
classNameMaps.set(callIndex, classNames);
|
|
2750
|
+
allCssRules.push(...cssRules);
|
|
2751
|
+
const replacement = this.buildReplacement(classNames);
|
|
2752
|
+
s.overwrite(call.start, call.end, replacement);
|
|
2753
|
+
}
|
|
2754
|
+
return {
|
|
2755
|
+
css: allCssRules.join(`
|
|
2756
|
+
`),
|
|
2757
|
+
classNameMaps
|
|
2758
|
+
};
|
|
2759
|
+
}
|
|
2760
|
+
processStaticCall(sourceFile, call, filePath) {
|
|
2761
|
+
const classNames = {};
|
|
2762
|
+
const cssRules = [];
|
|
2763
|
+
const callNode = findCallAtPosition(sourceFile, call.start);
|
|
2764
|
+
if (!callNode)
|
|
2765
|
+
return { classNames, cssRules };
|
|
2766
|
+
const args = callNode.getArguments();
|
|
2767
|
+
if (args.length === 0)
|
|
2768
|
+
return { classNames, cssRules };
|
|
2769
|
+
const firstArg = args[0];
|
|
2770
|
+
if (!firstArg || !firstArg.isKind(SyntaxKind16.ObjectLiteralExpression)) {
|
|
2771
|
+
return { classNames, cssRules };
|
|
2772
|
+
}
|
|
2773
|
+
for (const prop of firstArg.getProperties()) {
|
|
2774
|
+
if (!prop.isKind(SyntaxKind16.PropertyAssignment))
|
|
2775
|
+
continue;
|
|
2776
|
+
const blockName = prop.getName();
|
|
2777
|
+
const className = generateClassName2(filePath, blockName);
|
|
2778
|
+
classNames[blockName] = className;
|
|
2779
|
+
const init = prop.getInitializer();
|
|
2780
|
+
if (!init || !init.isKind(SyntaxKind16.ArrayLiteralExpression))
|
|
2781
|
+
continue;
|
|
2782
|
+
const entries = extractEntries2(init);
|
|
2783
|
+
const rules = buildCSSRules2(className, entries);
|
|
2784
|
+
cssRules.push(...rules);
|
|
2785
|
+
}
|
|
2786
|
+
return { classNames, cssRules };
|
|
2787
|
+
}
|
|
2788
|
+
buildReplacement(classNames) {
|
|
2789
|
+
const entries = Object.entries(classNames).map(([name, className]) => `${name}: '${className}'`).join(", ");
|
|
2790
|
+
return `{ ${entries} }`;
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
function findCallAtPosition(sourceFile, start) {
|
|
2794
|
+
const calls = sourceFile.getDescendantsOfKind(SyntaxKind16.CallExpression);
|
|
2795
|
+
for (const call of calls) {
|
|
2796
|
+
if (call.getStart() === start)
|
|
2797
|
+
return call;
|
|
2798
|
+
}
|
|
2799
|
+
return null;
|
|
2800
|
+
}
|
|
2801
|
+
function extractEntries2(arrayNode) {
|
|
2802
|
+
if (!arrayNode.isKind(SyntaxKind16.ArrayLiteralExpression))
|
|
2803
|
+
return [];
|
|
2804
|
+
const results = [];
|
|
2805
|
+
for (const element of arrayNode.getElements()) {
|
|
2806
|
+
if (element.isKind(SyntaxKind16.StringLiteral)) {
|
|
2807
|
+
results.push({ kind: "shorthand", value: element.getLiteralValue() });
|
|
2808
|
+
} else if (element.isKind(SyntaxKind16.ObjectLiteralExpression)) {
|
|
2809
|
+
for (const prop of element.getProperties()) {
|
|
2810
|
+
if (!prop.isKind(SyntaxKind16.PropertyAssignment))
|
|
2811
|
+
continue;
|
|
2812
|
+
const selector = prop.getName();
|
|
2813
|
+
const nameNode = prop.getNameNode();
|
|
2814
|
+
const actualSelector = nameNode.isKind(SyntaxKind16.StringLiteral) ? nameNode.getLiteralValue() : selector;
|
|
2815
|
+
const init = prop.getInitializer();
|
|
2816
|
+
if (!init || !init.isKind(SyntaxKind16.ArrayLiteralExpression))
|
|
2817
|
+
continue;
|
|
2818
|
+
const nestedEntries = [];
|
|
2819
|
+
const rawDeclarations = [];
|
|
2820
|
+
for (const el of init.getElements()) {
|
|
2821
|
+
if (el.isKind(SyntaxKind16.StringLiteral)) {
|
|
2822
|
+
nestedEntries.push(el.getLiteralValue());
|
|
2823
|
+
} else if (el.isKind(SyntaxKind16.ObjectLiteralExpression)) {
|
|
2824
|
+
const rawDecl = extractRawDeclaration2(el);
|
|
2825
|
+
if (rawDecl)
|
|
2826
|
+
rawDeclarations.push(rawDecl);
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
results.push({
|
|
2830
|
+
kind: "nested",
|
|
2831
|
+
value: "",
|
|
2832
|
+
selector: actualSelector,
|
|
2833
|
+
entries: nestedEntries,
|
|
2834
|
+
rawDeclarations
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
return results;
|
|
2840
|
+
}
|
|
2841
|
+
function extractRawDeclaration2(node) {
|
|
2842
|
+
if (!node.isKind(SyntaxKind16.ObjectLiteralExpression))
|
|
2843
|
+
return null;
|
|
2844
|
+
let property = null;
|
|
2845
|
+
let value = null;
|
|
2846
|
+
for (const prop of node.getProperties()) {
|
|
2847
|
+
if (!prop.isKind(SyntaxKind16.PropertyAssignment))
|
|
2848
|
+
return null;
|
|
2849
|
+
const name = prop.getName();
|
|
2850
|
+
const init = prop.getInitializer();
|
|
2851
|
+
if (!init || !init.isKind(SyntaxKind16.StringLiteral))
|
|
2852
|
+
return null;
|
|
2853
|
+
if (name === "property")
|
|
2854
|
+
property = init.getLiteralValue();
|
|
2855
|
+
else if (name === "value")
|
|
2856
|
+
value = init.getLiteralValue();
|
|
2857
|
+
}
|
|
2858
|
+
if (property && value)
|
|
2859
|
+
return { property, value };
|
|
2860
|
+
return null;
|
|
2861
|
+
}
|
|
2862
|
+
function generateClassName2(filePath, blockName) {
|
|
2863
|
+
const input = `${filePath}::${blockName}`;
|
|
2864
|
+
const hash = djb2Hash2(input);
|
|
2865
|
+
return `_${hash}`;
|
|
2866
|
+
}
|
|
2867
|
+
function djb2Hash2(str) {
|
|
2868
|
+
let hash = 5381;
|
|
2869
|
+
for (let i = 0;i < str.length; i++) {
|
|
2870
|
+
hash = (hash << 5) + hash + str.charCodeAt(i) | 0;
|
|
2871
|
+
}
|
|
2872
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
2873
|
+
}
|
|
2874
|
+
function buildCSSRules2(className, entries) {
|
|
2875
|
+
const rules = [];
|
|
2876
|
+
const baseDecls = [];
|
|
2877
|
+
const pseudoDecls = new Map;
|
|
2878
|
+
for (const entry of entries) {
|
|
2879
|
+
if (entry.kind === "shorthand") {
|
|
2880
|
+
const parsed = parseShorthandInline(entry.value);
|
|
2881
|
+
if (!parsed)
|
|
2882
|
+
continue;
|
|
2883
|
+
const resolved = resolveInline(parsed);
|
|
2884
|
+
if (!resolved)
|
|
2885
|
+
continue;
|
|
2886
|
+
if (parsed.pseudo) {
|
|
2887
|
+
const existing = pseudoDecls.get(parsed.pseudo) ?? [];
|
|
2888
|
+
existing.push(...resolved);
|
|
2889
|
+
pseudoDecls.set(parsed.pseudo, existing);
|
|
2890
|
+
} else {
|
|
2891
|
+
baseDecls.push(...resolved);
|
|
2892
|
+
}
|
|
2893
|
+
} else if (entry.kind === "nested" && entry.selector && entry.entries) {
|
|
2894
|
+
const nestedDecls = [];
|
|
2895
|
+
for (const nestedEntry of entry.entries) {
|
|
2896
|
+
const parsed = parseShorthandInline(nestedEntry);
|
|
2897
|
+
if (!parsed)
|
|
2898
|
+
continue;
|
|
2899
|
+
const resolved = resolveInline(parsed);
|
|
2900
|
+
if (!resolved)
|
|
2901
|
+
continue;
|
|
2902
|
+
nestedDecls.push(...resolved);
|
|
2903
|
+
}
|
|
2904
|
+
if (entry.rawDeclarations) {
|
|
2905
|
+
for (const raw of entry.rawDeclarations) {
|
|
2906
|
+
nestedDecls.push(`${raw.property}: ${raw.value};`);
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
const resolvedSelector = entry.selector.replaceAll("&", `.${className}`);
|
|
2910
|
+
if (nestedDecls.length > 0) {
|
|
2911
|
+
rules.push(formatCSSRule2(resolvedSelector, nestedDecls));
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
if (baseDecls.length > 0) {
|
|
2916
|
+
rules.unshift(formatCSSRule2(`.${className}`, baseDecls));
|
|
2917
|
+
}
|
|
2918
|
+
for (const [pseudo, decls] of pseudoDecls) {
|
|
2919
|
+
rules.push(formatCSSRule2(`.${className}${pseudo}`, decls));
|
|
2920
|
+
}
|
|
2921
|
+
return rules;
|
|
2922
|
+
}
|
|
2923
|
+
function formatCSSRule2(selector, declarations) {
|
|
2924
|
+
const props = declarations.map((d) => ` ${d}`).join(`
|
|
2925
|
+
`);
|
|
2926
|
+
return `${selector} {
|
|
2927
|
+
${props}
|
|
2928
|
+
}`;
|
|
2929
|
+
}
|
|
2930
|
+
function parseShorthandInline(input) {
|
|
2931
|
+
const parts = input.split(":");
|
|
2932
|
+
if (parts.length === 1) {
|
|
2933
|
+
const [property] = parts;
|
|
2934
|
+
return { property, value: null, pseudo: null };
|
|
2935
|
+
}
|
|
2936
|
+
if (parts.length === 2) {
|
|
2937
|
+
const [a, b] = parts;
|
|
2938
|
+
if (PSEUDO_PREFIXES3.has(a)) {
|
|
2939
|
+
return { property: b, value: null, pseudo: PSEUDO_MAP2[a] ?? a };
|
|
2940
|
+
}
|
|
2941
|
+
return { property: a, value: b, pseudo: null };
|
|
2942
|
+
}
|
|
2943
|
+
if (parts.length === 3) {
|
|
2944
|
+
const [a, b, c] = parts;
|
|
2945
|
+
if (PSEUDO_PREFIXES3.has(a)) {
|
|
2946
|
+
return { property: b, value: c, pseudo: PSEUDO_MAP2[a] ?? a };
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
return null;
|
|
2950
|
+
}
|
|
2951
|
+
function resolveInline(parsed) {
|
|
2952
|
+
const { property, value } = parsed;
|
|
2953
|
+
if (DISPLAY_MAP2[property] !== undefined && value === null) {
|
|
2954
|
+
return [`display: ${DISPLAY_MAP2[property]};`];
|
|
2955
|
+
}
|
|
2956
|
+
const keyword = KEYWORD_MAP3[property];
|
|
2957
|
+
if (keyword !== undefined && value === null) {
|
|
2958
|
+
return keyword.map((d) => `${d.property}: ${d.value};`);
|
|
2959
|
+
}
|
|
2960
|
+
const mapping = PROPERTY_MAP3[property];
|
|
2961
|
+
if (!mapping || value === null)
|
|
2962
|
+
return null;
|
|
2963
|
+
const resolvedValue = resolveValueInline(value, mapping.valueType, property);
|
|
2964
|
+
if (resolvedValue === null)
|
|
2965
|
+
return null;
|
|
2966
|
+
return mapping.properties.map((prop) => `${prop}: ${resolvedValue};`);
|
|
2967
|
+
}
|
|
2968
|
+
function resolveValueInline(value, valueType, property) {
|
|
2969
|
+
switch (valueType) {
|
|
2970
|
+
case "spacing":
|
|
2971
|
+
return SPACING_SCALE3[value] ?? null;
|
|
2972
|
+
case "color":
|
|
2973
|
+
return resolveColorInline(value);
|
|
2974
|
+
case "radius":
|
|
2975
|
+
return RADIUS_SCALE2[value] ?? null;
|
|
2976
|
+
case "shadow":
|
|
2977
|
+
return SHADOW_SCALE2[value] ?? null;
|
|
2978
|
+
case "size": {
|
|
2979
|
+
if (value === "screen") {
|
|
2980
|
+
return HEIGHT_AXIS_PROPERTIES2.has(property) ? "100vh" : "100vw";
|
|
2981
|
+
}
|
|
2982
|
+
return SPACING_SCALE3[value] ?? SIZE_KEYWORDS2[value] ?? null;
|
|
2983
|
+
}
|
|
2984
|
+
case "alignment":
|
|
2985
|
+
return ALIGNMENT_MAP2[value] ?? null;
|
|
2986
|
+
case "font-size":
|
|
2987
|
+
return FONT_SIZE_SCALE2[value] ?? null;
|
|
2988
|
+
case "font-weight":
|
|
2989
|
+
return FONT_WEIGHT_SCALE2[value] ?? null;
|
|
2990
|
+
case "line-height":
|
|
2991
|
+
return LINE_HEIGHT_SCALE2[value] ?? null;
|
|
2992
|
+
case "ring": {
|
|
2993
|
+
const num = Number(value);
|
|
2994
|
+
if (Number.isNaN(num) || num < 0)
|
|
2995
|
+
return null;
|
|
2996
|
+
return `${num}px solid var(--color-ring)`;
|
|
2997
|
+
}
|
|
2998
|
+
case "content":
|
|
2999
|
+
return CONTENT_MAP2[value] ?? null;
|
|
3000
|
+
default:
|
|
3001
|
+
return value;
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
function resolveColorInline(value) {
|
|
3005
|
+
const dotIndex = value.indexOf(".");
|
|
3006
|
+
if (dotIndex !== -1) {
|
|
3007
|
+
const namespace = value.substring(0, dotIndex);
|
|
3008
|
+
const shade = value.substring(dotIndex + 1);
|
|
3009
|
+
if (COLOR_NAMESPACES3.has(namespace)) {
|
|
3010
|
+
return `var(--color-${namespace}-${shade})`;
|
|
3011
|
+
}
|
|
3012
|
+
return null;
|
|
3013
|
+
}
|
|
3014
|
+
if (COLOR_NAMESPACES3.has(value)) {
|
|
3015
|
+
return `var(--color-${value})`;
|
|
3016
|
+
}
|
|
3017
|
+
if (CSS_COLOR_KEYWORDS3.has(value))
|
|
3018
|
+
return value;
|
|
3019
|
+
return null;
|
|
3020
|
+
}
|
|
3021
|
+
// src/transformers/prop-transformer.ts
|
|
3022
|
+
import { SyntaxKind as SyntaxKind17 } from "ts-morph";
|
|
3023
|
+
|
|
3024
|
+
class PropTransformer {
|
|
3025
|
+
transform(_source, _sourceFile, _component, _variables, _jsxExpressions) {}
|
|
3026
|
+
buildPropsObject(attrs, jsxMap) {
|
|
3027
|
+
if (attrs.length === 0)
|
|
3028
|
+
return "{}";
|
|
3029
|
+
const props = [];
|
|
3030
|
+
for (const attr of attrs) {
|
|
3031
|
+
if (!attr.isKind(SyntaxKind17.JsxAttribute))
|
|
3032
|
+
continue;
|
|
3033
|
+
const name = attr.getNameNode().getText();
|
|
3034
|
+
const init = attr.getInitializer();
|
|
3035
|
+
if (!init) {
|
|
3036
|
+
props.push(`${name}: true`);
|
|
3037
|
+
continue;
|
|
3038
|
+
}
|
|
3039
|
+
if (init.isKind(SyntaxKind17.StringLiteral)) {
|
|
3040
|
+
props.push(`${name}: ${init.getText()}`);
|
|
3041
|
+
continue;
|
|
3042
|
+
}
|
|
3043
|
+
if (init.isKind(SyntaxKind17.JsxExpression)) {
|
|
3044
|
+
const exprInfo = jsxMap.get(init.getStart());
|
|
3045
|
+
const exprNode = init.getExpression();
|
|
3046
|
+
const exprText = exprNode?.getText() ?? "";
|
|
3047
|
+
if (exprInfo?.reactive) {
|
|
3048
|
+
props.push(`get ${name}() { return ${exprText}; }`);
|
|
3049
|
+
} else {
|
|
3050
|
+
props.push(`${name}: ${exprText}`);
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
return `{ ${props.join(", ")} }`;
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
// src/type-generation/css-properties.ts
|
|
3058
|
+
function generateCSSProperties(input) {
|
|
3059
|
+
const varRefs = [];
|
|
3060
|
+
for (const [name, values] of Object.entries(input.colors)) {
|
|
3061
|
+
for (const key of Object.keys(values)) {
|
|
3062
|
+
if (key === "DEFAULT") {
|
|
3063
|
+
varRefs.push(`'var(--color-${name})'`);
|
|
3064
|
+
} else if (!key.startsWith("_")) {
|
|
3065
|
+
varRefs.push(`'var(--color-${name}-${key})'`);
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
if (input.spacing) {
|
|
3070
|
+
for (const name of Object.keys(input.spacing)) {
|
|
3071
|
+
varRefs.push(`'var(--spacing-${name})'`);
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
const unionMembers = varRefs.length > 0 ? varRefs.join(" | ") : "never";
|
|
3075
|
+
const lines = [];
|
|
3076
|
+
lines.push(`type ThemeTokenVar = ${unionMembers};`);
|
|
3077
|
+
lines.push("");
|
|
3078
|
+
lines.push("export interface CSSProperties {");
|
|
3079
|
+
lines.push(" color?: ThemeTokenVar | string;");
|
|
3080
|
+
lines.push(" backgroundColor?: ThemeTokenVar | string;");
|
|
3081
|
+
lines.push(" borderColor?: ThemeTokenVar | string;");
|
|
3082
|
+
lines.push(" padding?: ThemeTokenVar | string;");
|
|
3083
|
+
lines.push(" margin?: ThemeTokenVar | string;");
|
|
3084
|
+
lines.push(" gap?: ThemeTokenVar | string;");
|
|
3085
|
+
lines.push(" width?: ThemeTokenVar | string;");
|
|
3086
|
+
lines.push(" height?: ThemeTokenVar | string;");
|
|
3087
|
+
lines.push("}");
|
|
3088
|
+
return lines.join(`
|
|
3089
|
+
`);
|
|
3090
|
+
}
|
|
3091
|
+
// src/type-generation/theme-types.ts
|
|
3092
|
+
function generateThemeTypes(input) {
|
|
3093
|
+
const entries = [];
|
|
3094
|
+
for (const [name, values] of Object.entries(input.colors)) {
|
|
3095
|
+
for (const key of Object.keys(values)) {
|
|
3096
|
+
if (key === "DEFAULT") {
|
|
3097
|
+
entries.push(` '${name}': string;`);
|
|
3098
|
+
} else if (!key.startsWith("_")) {
|
|
3099
|
+
entries.push(` '${name}.${key}': string;`);
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
if (input.spacing) {
|
|
3104
|
+
for (const name of Object.keys(input.spacing)) {
|
|
3105
|
+
entries.push(` 'spacing.${name}': string;`);
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
return `export type ThemeTokens = {
|
|
3109
|
+
${entries.join(`
|
|
3110
|
+
`)}
|
|
3111
|
+
};`;
|
|
3112
|
+
}
|
|
3113
|
+
export {
|
|
3114
|
+
generateThemeTypes,
|
|
3115
|
+
generateCSSProperties,
|
|
3116
|
+
createVertzLibraryPlugin,
|
|
3117
|
+
compile,
|
|
3118
|
+
SignalTransformer,
|
|
3119
|
+
SSRSafetyDiagnostics,
|
|
3120
|
+
RouteCSSManifest,
|
|
3121
|
+
ReactivityAnalyzer,
|
|
3122
|
+
PropsDestructuringDiagnostics,
|
|
3123
|
+
PropTransformer,
|
|
3124
|
+
MutationTransformer,
|
|
3125
|
+
MutationDiagnostics,
|
|
3126
|
+
MutationAnalyzer,
|
|
3127
|
+
JsxTransformer,
|
|
3128
|
+
JsxAnalyzer,
|
|
3129
|
+
HydrationTransformer,
|
|
3130
|
+
DeadCSSEliminator,
|
|
3131
|
+
ComputedTransformer,
|
|
3132
|
+
ComponentAnalyzer,
|
|
3133
|
+
CSSTransformer,
|
|
3134
|
+
CSSHMRHandler,
|
|
3135
|
+
CSSExtractor,
|
|
3136
|
+
CSSDiagnostics,
|
|
3137
|
+
CSSCodeSplitter,
|
|
3138
|
+
CSSAnalyzer
|
|
3139
|
+
};
|