@vune-ui/compiler 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ast.d.ts +85 -0
- package/dist/ast.d.ts.map +1 -0
- package/dist/ast.js +531 -0
- package/dist/ast.js.map +1 -0
- package/dist/diagnostics.d.ts +3 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +178 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/pipeline.d.ts +3 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +1316 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/scanner.d.ts +54 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +678 -0
- package/dist/scanner.js.map +1 -0
- package/dist/semantic.d.ts +100 -0
- package/dist/semantic.d.ts.map +1 -0
- package/dist/semantic.js +692 -0
- package/dist/semantic.js.map +1 -0
- package/dist/source-map.d.ts +26 -0
- package/dist/source-map.d.ts.map +1 -0
- package/dist/source-map.js +196 -0
- package/dist/source-map.js.map +1 -0
- package/dist/specialization.d.ts +4 -0
- package/dist/specialization.d.ts.map +1 -0
- package/dist/specialization.js +233 -0
- package/dist/specialization.js.map +1 -0
- package/dist/types.d.ts +50 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/vite.d.ts +17 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +96 -0
- package/dist/vite.js.map +1 -0
- package/package.json +29 -0
package/dist/semantic.js
ADDED
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import * as Core from "@vune-ui/core";
|
|
3
|
+
import { SemanticModel, resolveSemanticCall, semanticHtmlAttributeSpec, semanticHtmlTagSpec, } from "@vune-ui/core";
|
|
4
|
+
import { parseVuneBuilder, parseVuneStructs, } from "./ast.js";
|
|
5
|
+
import { createVuneSourceMap, mapGeneratedPosition } from "./source-map.js";
|
|
6
|
+
function splitParameterSource(source) {
|
|
7
|
+
const parts = [];
|
|
8
|
+
let start = 0;
|
|
9
|
+
let angle = 0;
|
|
10
|
+
let square = 0;
|
|
11
|
+
let parens = 0;
|
|
12
|
+
let braces = 0;
|
|
13
|
+
let quote;
|
|
14
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
15
|
+
const character = source[index];
|
|
16
|
+
if (quote) {
|
|
17
|
+
if (character === "\\") {
|
|
18
|
+
index += 1;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (character === quote)
|
|
22
|
+
quote = undefined;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (character === "\"" || character === "'" || character === "`") {
|
|
26
|
+
quote = character;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (character === "<")
|
|
30
|
+
angle += 1;
|
|
31
|
+
else if (character === ">")
|
|
32
|
+
angle = Math.max(0, angle - 1);
|
|
33
|
+
else if (character === "[")
|
|
34
|
+
square += 1;
|
|
35
|
+
else if (character === "]")
|
|
36
|
+
square = Math.max(0, square - 1);
|
|
37
|
+
else if (character === "(")
|
|
38
|
+
parens += 1;
|
|
39
|
+
else if (character === ")")
|
|
40
|
+
parens = Math.max(0, parens - 1);
|
|
41
|
+
else if (character === "{")
|
|
42
|
+
braces += 1;
|
|
43
|
+
else if (character === "}")
|
|
44
|
+
braces = Math.max(0, braces - 1);
|
|
45
|
+
else if (character === "," && angle === 0 && square === 0 && parens === 0 && braces === 0) {
|
|
46
|
+
parts.push(source.slice(start, index).trim());
|
|
47
|
+
start = index + 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
parts.push(source.slice(start).trim());
|
|
51
|
+
return parts.filter(Boolean);
|
|
52
|
+
}
|
|
53
|
+
function topLevelCharacter(source, expected) {
|
|
54
|
+
let angle = 0;
|
|
55
|
+
let square = 0;
|
|
56
|
+
let parens = 0;
|
|
57
|
+
let braces = 0;
|
|
58
|
+
let quote;
|
|
59
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
60
|
+
const character = source[index];
|
|
61
|
+
if (quote) {
|
|
62
|
+
if (character === "\\") {
|
|
63
|
+
index += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (character === quote)
|
|
67
|
+
quote = undefined;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (character === "\"" || character === "'" || character === "`") {
|
|
71
|
+
quote = character;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (character === "<")
|
|
75
|
+
angle += 1;
|
|
76
|
+
else if (character === ">")
|
|
77
|
+
angle = Math.max(0, angle - 1);
|
|
78
|
+
else if (character === "[")
|
|
79
|
+
square += 1;
|
|
80
|
+
else if (character === "]")
|
|
81
|
+
square = Math.max(0, square - 1);
|
|
82
|
+
else if (character === "(")
|
|
83
|
+
parens += 1;
|
|
84
|
+
else if (character === ")")
|
|
85
|
+
parens = Math.max(0, parens - 1);
|
|
86
|
+
else if (character === "{")
|
|
87
|
+
braces += 1;
|
|
88
|
+
else if (character === "}")
|
|
89
|
+
braces = Math.max(0, braces - 1);
|
|
90
|
+
else if (character === expected && angle === 0 && square === 0 && parens === 0 && braces === 0)
|
|
91
|
+
return index;
|
|
92
|
+
}
|
|
93
|
+
return -1;
|
|
94
|
+
}
|
|
95
|
+
function semanticInitializerParameters(source) {
|
|
96
|
+
const parsed = splitParameterSource(source).map(parameterSource => {
|
|
97
|
+
const kind = parameterSource.includes("@ViewBuilder")
|
|
98
|
+
? "viewBuilder"
|
|
99
|
+
: parameterSource.includes("@Action")
|
|
100
|
+
? "action"
|
|
101
|
+
: parameterSource.includes("@Binding")
|
|
102
|
+
? "binding"
|
|
103
|
+
: "value";
|
|
104
|
+
const clean = parameterSource.replace(/@(?:ViewBuilder|Action|Binding)\s*/g, "").trim();
|
|
105
|
+
const equals = topLevelCharacter(clean, "=");
|
|
106
|
+
const declaration = equals < 0 ? clean : clean.slice(0, equals).trim();
|
|
107
|
+
const defaultValue = equals < 0 ? undefined : clean.slice(equals + 1).trim();
|
|
108
|
+
const colon = topLevelCharacter(declaration, ":");
|
|
109
|
+
const head = (colon < 0 ? declaration : declaration.slice(0, colon)).trim();
|
|
110
|
+
const words = head.split(/\s+/).filter(Boolean);
|
|
111
|
+
const name = (words.at(-1) ?? "value").replace(/^_+/, "");
|
|
112
|
+
return {
|
|
113
|
+
name,
|
|
114
|
+
label: words[0] === "_" ? undefined : words[0],
|
|
115
|
+
kind,
|
|
116
|
+
required: defaultValue === undefined,
|
|
117
|
+
type: colon < 0 ? undefined : declaration.slice(colon + 1).trim(),
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
return parsed.map((parameter, index) => {
|
|
121
|
+
const trailing = index === parsed.length - 1 && (parameter.kind === "viewBuilder" || parameter.kind === "action");
|
|
122
|
+
return {
|
|
123
|
+
...parameter,
|
|
124
|
+
trailing,
|
|
125
|
+
labelRequired: parameter.label !== undefined && !trailing,
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function flattenStructs(structs, prefix = "") {
|
|
130
|
+
const result = [];
|
|
131
|
+
for (const declaration of structs) {
|
|
132
|
+
const qualifiedName = prefix ? `${prefix}.${declaration.name}` : declaration.name;
|
|
133
|
+
const initializers = declaration.initializers.map((initializer, index) => {
|
|
134
|
+
const parameters = semanticInitializerParameters(initializer.parametersSource);
|
|
135
|
+
const symbol = {
|
|
136
|
+
kind: "initializer",
|
|
137
|
+
index,
|
|
138
|
+
signature: `${declaration.name}(${initializer.parametersSource.trim()})`,
|
|
139
|
+
parameters,
|
|
140
|
+
};
|
|
141
|
+
return {
|
|
142
|
+
index,
|
|
143
|
+
signature: symbol.signature,
|
|
144
|
+
parametersSource: initializer.parametersSource,
|
|
145
|
+
parameters,
|
|
146
|
+
symbol,
|
|
147
|
+
range: initializer.range,
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
const fields = declaration.fields.map(field => ({
|
|
151
|
+
name: field.name,
|
|
152
|
+
kind: field.kind,
|
|
153
|
+
type: field.type,
|
|
154
|
+
initializer: field.initializer,
|
|
155
|
+
range: field.range,
|
|
156
|
+
}));
|
|
157
|
+
const symbol = {
|
|
158
|
+
kind: "view",
|
|
159
|
+
name: declaration.name,
|
|
160
|
+
qualifiedName,
|
|
161
|
+
genericParameters: declaration.genericParameters,
|
|
162
|
+
fields: fields.map(field => ({ name: field.name, kind: field.kind, type: field.type, defaultValue: field.initializer })),
|
|
163
|
+
initializers: initializers.map(initializer => initializer.symbol),
|
|
164
|
+
};
|
|
165
|
+
result.push({
|
|
166
|
+
name: declaration.name,
|
|
167
|
+
qualifiedName,
|
|
168
|
+
genericParameters: declaration.genericParameters,
|
|
169
|
+
fields,
|
|
170
|
+
initializers,
|
|
171
|
+
symbol,
|
|
172
|
+
bodyRange: declaration.bodyExpressionRange,
|
|
173
|
+
range: declaration.range,
|
|
174
|
+
});
|
|
175
|
+
result.push(...flattenStructs(declaration.nested ?? [], qualifiedName));
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
function collectCalls(program, output) {
|
|
180
|
+
const visit = (node) => {
|
|
181
|
+
if (node.kind === "call") {
|
|
182
|
+
output.push({
|
|
183
|
+
callee: node.callee,
|
|
184
|
+
arguments: node.arguments.map(argument => ({
|
|
185
|
+
label: argument.label,
|
|
186
|
+
kind: argument.value.kind === "closure" ? "closure" : "expression",
|
|
187
|
+
source: argument.value.kind === "closure" ? "" : argument.value.source,
|
|
188
|
+
range: argument.range,
|
|
189
|
+
})),
|
|
190
|
+
trailingClosure: node.trailing !== undefined,
|
|
191
|
+
range: node.range,
|
|
192
|
+
resolution: resolveSemanticCall(undefined, []),
|
|
193
|
+
});
|
|
194
|
+
for (const argument of node.arguments) {
|
|
195
|
+
if (argument.value.kind === "closure")
|
|
196
|
+
collectCalls(argument.value.body, output);
|
|
197
|
+
}
|
|
198
|
+
if (node.trailing)
|
|
199
|
+
collectCalls(node.trailing.body, output);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (node.kind === "conditional") {
|
|
203
|
+
for (const child of node.then.statements)
|
|
204
|
+
visit(child);
|
|
205
|
+
if (node.otherwise) {
|
|
206
|
+
if (node.otherwise.kind === "conditional")
|
|
207
|
+
visit(node.otherwise);
|
|
208
|
+
else
|
|
209
|
+
for (const child of node.otherwise.statements)
|
|
210
|
+
visit(child);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
for (const node of program.statements)
|
|
215
|
+
visit(node);
|
|
216
|
+
}
|
|
217
|
+
function canonicalViewSymbols() {
|
|
218
|
+
const result = new Map();
|
|
219
|
+
for (const [name, value] of Object.entries(Core)) {
|
|
220
|
+
if (typeof value !== "function")
|
|
221
|
+
continue;
|
|
222
|
+
const symbol = value.viewType?.semanticSymbol;
|
|
223
|
+
if (symbol)
|
|
224
|
+
result.set(name, symbol);
|
|
225
|
+
}
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
function checkerTypeForExpression(source, checker, sourceFile) {
|
|
229
|
+
const wanted = source.trim();
|
|
230
|
+
if (!wanted)
|
|
231
|
+
return undefined;
|
|
232
|
+
let candidate;
|
|
233
|
+
const visit = (node) => {
|
|
234
|
+
if (candidate || !ts.isExpression(node)) {
|
|
235
|
+
if (!candidate)
|
|
236
|
+
ts.forEachChild(node, visit);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (node.getText(sourceFile).trim() === wanted)
|
|
240
|
+
candidate = node;
|
|
241
|
+
if (!candidate)
|
|
242
|
+
ts.forEachChild(node, visit);
|
|
243
|
+
};
|
|
244
|
+
visit(sourceFile);
|
|
245
|
+
if (!candidate)
|
|
246
|
+
return undefined;
|
|
247
|
+
const type = checker.getTypeAtLocation(candidate);
|
|
248
|
+
if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never))
|
|
249
|
+
return undefined;
|
|
250
|
+
// `const label = "x"` has the literal type `"x"`, but a normal Vune
|
|
251
|
+
// initializer accepting `string` must still accept it. Preserve literal
|
|
252
|
+
// precision in TypeScript itself while normalizing the compiler-facing
|
|
253
|
+
// semantic category used for overload matching.
|
|
254
|
+
const primitiveCategory = (value) => {
|
|
255
|
+
if (value.flags & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral))
|
|
256
|
+
return "string";
|
|
257
|
+
if (value.flags & (ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral))
|
|
258
|
+
return "number";
|
|
259
|
+
if (value.flags & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral))
|
|
260
|
+
return "boolean";
|
|
261
|
+
if (value.isUnion()) {
|
|
262
|
+
const categories = new Set(value.types.map(primitiveCategory));
|
|
263
|
+
if (categories.size === 1 && !categories.has(undefined))
|
|
264
|
+
return [...categories][0];
|
|
265
|
+
}
|
|
266
|
+
return undefined;
|
|
267
|
+
};
|
|
268
|
+
return primitiveCategory(type) ?? checker.typeToString(type);
|
|
269
|
+
}
|
|
270
|
+
function compilerSemanticArgument(source, label, checker, sourceFile, declaredTypes = new Map()) {
|
|
271
|
+
const value = source.trim();
|
|
272
|
+
if (/^(?:\$[A-Za-z_$][A-Za-z0-9_$]*|Binding\s*\()/.test(value))
|
|
273
|
+
return { label, kind: "binding", type: "binding" };
|
|
274
|
+
if (/^(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)$/.test(value))
|
|
275
|
+
return { label, type: "string" };
|
|
276
|
+
if (/^-?(?:\d+(?:\.\d*)?|\.\d+)$/.test(value))
|
|
277
|
+
return { label, type: "number" };
|
|
278
|
+
if (/^(?:true|false)$/.test(value))
|
|
279
|
+
return { label, type: "boolean" };
|
|
280
|
+
if (/^null$/.test(value))
|
|
281
|
+
return { label, type: "null" };
|
|
282
|
+
if (/^undefined$/.test(value))
|
|
283
|
+
return { label, type: "undefined" };
|
|
284
|
+
if (/^(?:\[|Array\s*\()/.test(value))
|
|
285
|
+
return { label, type: "array" };
|
|
286
|
+
if (/=>|^function\b/.test(value))
|
|
287
|
+
return { label, type: "function" };
|
|
288
|
+
const declared = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value) ? declaredTypes.get(value) : undefined;
|
|
289
|
+
return { label, type: checkerTypeForExpression(value, checker, sourceFile) ?? declared ?? "unknown" };
|
|
290
|
+
}
|
|
291
|
+
function resolvedCalls(calls, views, checker, sourceFile) {
|
|
292
|
+
const symbols = canonicalViewSymbols();
|
|
293
|
+
for (const view of views)
|
|
294
|
+
symbols.set(view.name, view.symbol);
|
|
295
|
+
const declaredTypes = new Map();
|
|
296
|
+
for (const view of views)
|
|
297
|
+
for (const field of view.fields)
|
|
298
|
+
if (field.type)
|
|
299
|
+
declaredTypes.set(field.name, field.type);
|
|
300
|
+
return calls.map(call => {
|
|
301
|
+
const arguments_ = call.arguments.map((argument, argumentIndex) => argument.kind === "closure"
|
|
302
|
+
? { label: argument.label, type: "function" }
|
|
303
|
+
: call.callee === "ForEach" && argumentIndex === 1 && /^\{\s*(?:id|key)\s*:/.test(argument.source) && /=>/.test(argument.source)
|
|
304
|
+
? { label: "key", type: "function" }
|
|
305
|
+
: compilerSemanticArgument(argument.source, argument.label, checker, sourceFile, declaredTypes));
|
|
306
|
+
if (call.trailingClosure)
|
|
307
|
+
arguments_.push({ type: "function", trailing: true });
|
|
308
|
+
return {
|
|
309
|
+
...call,
|
|
310
|
+
resolution: resolveSemanticCall(symbols.get(call.callee), arguments_),
|
|
311
|
+
};
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
function builderProgramsFor(source, structs) {
|
|
315
|
+
const programs = [];
|
|
316
|
+
const seen = new Set();
|
|
317
|
+
const add = (value) => {
|
|
318
|
+
const key = `${value.range.start}:${value.range.end}`;
|
|
319
|
+
if (seen.has(key))
|
|
320
|
+
return;
|
|
321
|
+
seen.add(key);
|
|
322
|
+
programs.push(value);
|
|
323
|
+
};
|
|
324
|
+
const visit = (declarations) => {
|
|
325
|
+
for (const declaration of declarations) {
|
|
326
|
+
add(parseVuneBuilder(declaration.bodyExpressionSource, declaration.bodyExpressionRange.start));
|
|
327
|
+
visit(declaration.nested ?? []);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
if (structs.length > 0) {
|
|
331
|
+
// Struct bodies are indexed separately above. Mask declarations while
|
|
332
|
+
// preserving offsets so top-level builder calls are not lost when a file
|
|
333
|
+
// also contains custom Views.
|
|
334
|
+
let masked = source;
|
|
335
|
+
for (const declaration of [...structs].sort((left, right) => right.range.start - left.range.start)) {
|
|
336
|
+
masked = masked.slice(0, declaration.range.start) + " ".repeat(declaration.range.end - declaration.range.start) + masked.slice(declaration.range.end);
|
|
337
|
+
}
|
|
338
|
+
add(parseVuneBuilder(masked));
|
|
339
|
+
}
|
|
340
|
+
visit(structs);
|
|
341
|
+
if (programs.length === 0 && /\b[A-Z][A-Za-z0-9_$]*\s*\(/.test(source))
|
|
342
|
+
add(parseVuneBuilder(source));
|
|
343
|
+
return programs;
|
|
344
|
+
}
|
|
345
|
+
function importsOf(source, generatedSource, sourceFile, sourceMap) {
|
|
346
|
+
return sourceFile.statements.flatMap(statement => {
|
|
347
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
|
|
348
|
+
return [];
|
|
349
|
+
const generatedRange = { start: statement.getStart(sourceFile), end: statement.end };
|
|
350
|
+
return [{
|
|
351
|
+
module: statement.moduleSpecifier.text,
|
|
352
|
+
range: mapRange(source, generatedSource, sourceMap, generatedRange),
|
|
353
|
+
}];
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
function propertyName(property) {
|
|
357
|
+
if (!property)
|
|
358
|
+
return undefined;
|
|
359
|
+
if (ts.isIdentifier(property) || ts.isStringLiteral(property) || ts.isNumericLiteral(property))
|
|
360
|
+
return property.text;
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
function objectPropertyName(property) {
|
|
364
|
+
if (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property) || ts.isMethodDeclaration(property)) {
|
|
365
|
+
return propertyName(property.name);
|
|
366
|
+
}
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
function expressionValueType(expression, checker) {
|
|
370
|
+
if (ts.isStringLiteralLike(expression))
|
|
371
|
+
return "string";
|
|
372
|
+
if (ts.isNumericLiteral(expression))
|
|
373
|
+
return "number";
|
|
374
|
+
if (expression.kind === ts.SyntaxKind.TrueKeyword || expression.kind === ts.SyntaxKind.FalseKeyword)
|
|
375
|
+
return "boolean";
|
|
376
|
+
if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression))
|
|
377
|
+
return "event";
|
|
378
|
+
const type = checker.getTypeAtLocation(expression);
|
|
379
|
+
if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never))
|
|
380
|
+
return undefined;
|
|
381
|
+
if (type.flags & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral))
|
|
382
|
+
return "string";
|
|
383
|
+
if (type.flags & (ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral))
|
|
384
|
+
return "number";
|
|
385
|
+
if (type.flags & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral))
|
|
386
|
+
return "boolean";
|
|
387
|
+
if (type.getCallSignatures().length > 0)
|
|
388
|
+
return "event";
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
function acceptsHtmlValue(spec, valueType, expression) {
|
|
392
|
+
if (!spec || !valueType || spec.type === "unknown")
|
|
393
|
+
return true;
|
|
394
|
+
if (spec.type === "event")
|
|
395
|
+
return valueType === "event";
|
|
396
|
+
if (spec.type === "string | number")
|
|
397
|
+
return valueType === "string" || valueType === "number";
|
|
398
|
+
if (spec.type === "string | number | boolean")
|
|
399
|
+
return ["string", "number", "boolean"].includes(valueType);
|
|
400
|
+
if (spec.values && expression && ts.isStringLiteralLike(expression))
|
|
401
|
+
return spec.values.includes(expression.text);
|
|
402
|
+
if (spec.type === valueType)
|
|
403
|
+
return true;
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
function positionAt(source, offset) {
|
|
407
|
+
const bounded = Math.max(0, Math.min(source.length, offset));
|
|
408
|
+
const prefix = source.slice(0, bounded);
|
|
409
|
+
const line = prefix.split("\n");
|
|
410
|
+
return { line: line.length, column: (line[line.length - 1]?.length ?? 0) + 1 };
|
|
411
|
+
}
|
|
412
|
+
function offsetAt(source, position) {
|
|
413
|
+
const lines = source.split("\n");
|
|
414
|
+
const line = Math.max(1, Math.min(lines.length, position.line));
|
|
415
|
+
const offset = lines.slice(0, line - 1).reduce((sum, value) => sum + value.length + 1, 0);
|
|
416
|
+
return Math.min(source.length, offset + Math.max(0, position.column - 1));
|
|
417
|
+
}
|
|
418
|
+
function mapRange(source, generatedSource, map, generatedRange) {
|
|
419
|
+
const start = mapGeneratedPosition(map, positionAt(generatedSource, generatedRange.start));
|
|
420
|
+
const end = mapGeneratedPosition(map, positionAt(generatedSource, generatedRange.end));
|
|
421
|
+
return { start: offsetAt(source, start), end: Math.max(offsetAt(source, start), offsetAt(source, end)) };
|
|
422
|
+
}
|
|
423
|
+
function matchingDelimiter(source, open, opener, closer) {
|
|
424
|
+
let depth = 0;
|
|
425
|
+
for (let index = open; index < source.length; index += 1) {
|
|
426
|
+
if (source[index] === "\"" || source[index] === "'" || source[index] === "`") {
|
|
427
|
+
const quote = source[index];
|
|
428
|
+
index += 1;
|
|
429
|
+
while (index < source.length) {
|
|
430
|
+
if (source[index] === "\\") {
|
|
431
|
+
index += 2;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (source[index] === quote)
|
|
435
|
+
break;
|
|
436
|
+
index += 1;
|
|
437
|
+
}
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (source[index] === opener)
|
|
441
|
+
depth += 1;
|
|
442
|
+
else if (source[index] === closer && --depth === 0)
|
|
443
|
+
return index;
|
|
444
|
+
}
|
|
445
|
+
return source.length - 1;
|
|
446
|
+
}
|
|
447
|
+
function originalCallRange(source, name) {
|
|
448
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
449
|
+
const expression = new RegExp(`\\b${escaped}\\s*\\(`, "g");
|
|
450
|
+
const match = expression.exec(source);
|
|
451
|
+
if (!match)
|
|
452
|
+
return undefined;
|
|
453
|
+
const open = source.indexOf("(", match.index + name.length);
|
|
454
|
+
if (open < 0)
|
|
455
|
+
return undefined;
|
|
456
|
+
const close = matchingDelimiter(source, open, "(", ")");
|
|
457
|
+
return { start: match.index, end: Math.min(source.length, close + 1) };
|
|
458
|
+
}
|
|
459
|
+
function originalElementRange(source, tag, from) {
|
|
460
|
+
const rawStart = source.indexOf("<" + tag, from);
|
|
461
|
+
const rawBoundary = rawStart < 0 ? undefined : source[rawStart + tag.length + 1];
|
|
462
|
+
const validRawStart = rawStart >= 0 && rawBoundary !== undefined && (rawBoundary === " " || rawBoundary === "\t" || rawBoundary === "/" || rawBoundary === ">") ? rawStart : -1;
|
|
463
|
+
const callStart = source.indexOf("Element(", from);
|
|
464
|
+
if (validRawStart >= 0 && (callStart < 0 || validRawStart < callStart)) {
|
|
465
|
+
const openingEnd = source.indexOf(">", validRawStart);
|
|
466
|
+
if (openingEnd < 0)
|
|
467
|
+
return { range: { start: validRawStart, end: source.length }, next: source.length };
|
|
468
|
+
const closingStart = source.indexOf("</" + tag, openingEnd + 1);
|
|
469
|
+
const closingEnd = closingStart >= 0 ? source.indexOf(">", closingStart) : -1;
|
|
470
|
+
const end = closingEnd >= 0 ? closingEnd + 1 : openingEnd + 1;
|
|
471
|
+
return { range: { start: validRawStart, end: Math.max(openingEnd + 1, end) }, next: Math.max(openingEnd + 1, end) };
|
|
472
|
+
}
|
|
473
|
+
if (callStart >= 0) {
|
|
474
|
+
const close = matchingDelimiter(source, callStart + "Element".length, "(", ")");
|
|
475
|
+
return { range: { start: callStart, end: Math.min(source.length, close + 1) }, next: Math.min(source.length, close + 1) };
|
|
476
|
+
}
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
function typescriptGraphSymbols(source, generatedSource, sourceFile, checker, sourceMap) {
|
|
480
|
+
const htmlElements = [];
|
|
481
|
+
const htmlDiagnostics = [];
|
|
482
|
+
const foreignComponents = [];
|
|
483
|
+
const vueImports = new Map();
|
|
484
|
+
let elementCursor = 0;
|
|
485
|
+
for (const statement of sourceFile.statements) {
|
|
486
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
|
|
487
|
+
continue;
|
|
488
|
+
const module = statement.moduleSpecifier.text;
|
|
489
|
+
if (!/\.vue$/i.test(module) || !statement.importClause?.name)
|
|
490
|
+
continue;
|
|
491
|
+
vueImports.set(statement.importClause.name.text, module);
|
|
492
|
+
}
|
|
493
|
+
const visit = (node) => {
|
|
494
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer)) {
|
|
495
|
+
const expression = node.initializer.expression;
|
|
496
|
+
const argument = node.initializer.arguments[0];
|
|
497
|
+
if (ts.isIdentifier(expression) && /^(?:__vuneForeignComponent|__vuneVueComponent)/.test(expression.text) && ts.isIdentifier(argument)) {
|
|
498
|
+
const module = vueImports.get(argument.text);
|
|
499
|
+
if (module) {
|
|
500
|
+
const generatedRange = { start: node.getStart(sourceFile), end: node.end };
|
|
501
|
+
foreignComponents.push({
|
|
502
|
+
localName: node.name.text,
|
|
503
|
+
module,
|
|
504
|
+
range: originalCallRange(source, node.name.text) ?? mapRange(source, generatedSource, sourceMap, generatedRange),
|
|
505
|
+
generatedRange,
|
|
506
|
+
symbol: {
|
|
507
|
+
kind: "foreign-component",
|
|
508
|
+
localName: node.name.text,
|
|
509
|
+
module,
|
|
510
|
+
rendererAdapter: "@vune-ui/vue",
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "Element") {
|
|
517
|
+
const tag = node.arguments[0];
|
|
518
|
+
if (tag && ts.isStringLiteral(tag)) {
|
|
519
|
+
const props = node.arguments[1];
|
|
520
|
+
const generatedRange = { start: node.getStart(sourceFile), end: node.end };
|
|
521
|
+
const original = originalElementRange(source, tag.text, elementCursor);
|
|
522
|
+
const attributes = props && ts.isObjectLiteralExpression(props)
|
|
523
|
+
? props.properties.flatMap(property => {
|
|
524
|
+
if (ts.isSpreadAssignment(property))
|
|
525
|
+
return ["..."];
|
|
526
|
+
return objectPropertyName(property) ?? [];
|
|
527
|
+
})
|
|
528
|
+
: [];
|
|
529
|
+
const attributeSymbols = [];
|
|
530
|
+
if (props && ts.isObjectLiteralExpression(props)) {
|
|
531
|
+
for (const property of props.properties) {
|
|
532
|
+
if (ts.isSpreadAssignment(property)) {
|
|
533
|
+
attributeSymbols.push({ name: "...", category: "custom", type: "unknown" });
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
const name = objectPropertyName(property);
|
|
537
|
+
if (!name)
|
|
538
|
+
continue;
|
|
539
|
+
const expression = ts.isPropertyAssignment(property)
|
|
540
|
+
? property.initializer
|
|
541
|
+
: ts.isShorthandPropertyAssignment(property)
|
|
542
|
+
? property.objectAssignmentInitializer
|
|
543
|
+
: undefined;
|
|
544
|
+
const spec = semanticHtmlAttributeSpec(tag.text, name);
|
|
545
|
+
const valueType = ts.isMethodDeclaration(property)
|
|
546
|
+
? "event"
|
|
547
|
+
: expression
|
|
548
|
+
? expressionValueType(expression, checker)
|
|
549
|
+
: undefined;
|
|
550
|
+
attributeSymbols.push({ name, category: spec?.category ?? "custom", type: spec?.type ?? "unknown", valueType });
|
|
551
|
+
const generatedAttributeRange = { start: property.getStart(sourceFile), end: property.end };
|
|
552
|
+
const diagnosticRange = mapRange(source, generatedSource, sourceMap, generatedAttributeRange);
|
|
553
|
+
if (!spec) {
|
|
554
|
+
htmlDiagnostics.push({
|
|
555
|
+
code: "VUNE_HTML_ATTRIBUTE",
|
|
556
|
+
message: `Unknown attribute \"${name}\" on <${tag.text}>.`,
|
|
557
|
+
range: diagnosticRange,
|
|
558
|
+
generatedRange: generatedAttributeRange,
|
|
559
|
+
});
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
if (!acceptsHtmlValue(spec, valueType, expression)) {
|
|
563
|
+
const expected = spec.values?.length ? spec.values.map(value => `\"${value}\"`).join(" | ") : spec.type;
|
|
564
|
+
htmlDiagnostics.push({
|
|
565
|
+
code: "VUNE_HTML_VALUE",
|
|
566
|
+
message: `Attribute \"${name}\" on <${tag.text}> expects ${expected}.`,
|
|
567
|
+
range: diagnosticRange,
|
|
568
|
+
generatedRange: generatedAttributeRange,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
const tagSpec = semanticHtmlTagSpec(tag.text);
|
|
574
|
+
const symbol = {
|
|
575
|
+
kind: "html-element",
|
|
576
|
+
name: `Element#${generatedRange.start}`,
|
|
577
|
+
tag: tag.text,
|
|
578
|
+
custom: tagSpec.custom,
|
|
579
|
+
attributes: attributeSymbols,
|
|
580
|
+
};
|
|
581
|
+
htmlElements.push({
|
|
582
|
+
tag: tag.text,
|
|
583
|
+
attributes,
|
|
584
|
+
attributeSymbols,
|
|
585
|
+
symbol,
|
|
586
|
+
range: original?.range ?? mapRange(source, generatedSource, sourceMap, generatedRange),
|
|
587
|
+
generatedRange,
|
|
588
|
+
});
|
|
589
|
+
if (original)
|
|
590
|
+
elementCursor = original.next;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
ts.forEachChild(node, visit);
|
|
594
|
+
};
|
|
595
|
+
visit(sourceFile);
|
|
596
|
+
return { htmlElements, htmlDiagnostics, foreignComponents };
|
|
597
|
+
}
|
|
598
|
+
function typescriptSnapshot(fileName, source) {
|
|
599
|
+
const options = {
|
|
600
|
+
allowJs: false,
|
|
601
|
+
module: ts.ModuleKind.ESNext,
|
|
602
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
603
|
+
noEmit: true,
|
|
604
|
+
noResolve: true,
|
|
605
|
+
skipLibCheck: true,
|
|
606
|
+
target: ts.ScriptTarget.ES2022,
|
|
607
|
+
};
|
|
608
|
+
const host = ts.createCompilerHost(options, true);
|
|
609
|
+
const normalize = (value) => value.replaceAll("\\", "/");
|
|
610
|
+
const requestedRoot = normalize(fileName);
|
|
611
|
+
const originalGetSourceFile = host.getSourceFile.bind(host);
|
|
612
|
+
const originalReadFile = host.readFile.bind(host);
|
|
613
|
+
const originalFileExists = host.fileExists.bind(host);
|
|
614
|
+
host.fileExists = requested => normalize(requested) === requestedRoot || originalFileExists(requested);
|
|
615
|
+
host.readFile = requested => normalize(requested) === requestedRoot ? source : originalReadFile(requested);
|
|
616
|
+
host.getSourceFile = (requested, languageVersion, onError, shouldCreateNewSourceFile) => normalize(requested) === requestedRoot
|
|
617
|
+
? ts.createSourceFile(requested, source, languageVersion, true, /\.tsx?$/i.test(fileName) ? ts.ScriptKind.TS : ts.ScriptKind.TS)
|
|
618
|
+
: originalGetSourceFile(requested, languageVersion, onError, shouldCreateNewSourceFile);
|
|
619
|
+
const program = ts.createProgram([fileName], options, host);
|
|
620
|
+
const sourceFile = program.getSourceFile(fileName) ?? ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
621
|
+
return {
|
|
622
|
+
sourceFile,
|
|
623
|
+
checker: program.getTypeChecker(),
|
|
624
|
+
diagnostics: program.getSyntacticDiagnostics(sourceFile),
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
export function createSemanticModel(source, fileName, generatedSource) {
|
|
628
|
+
const snapshot = typescriptSnapshot(fileName, generatedSource);
|
|
629
|
+
const typescript = snapshot.sourceFile;
|
|
630
|
+
const structs = parseVuneStructs(source);
|
|
631
|
+
const builderPrograms = builderProgramsFor(source, structs);
|
|
632
|
+
const collectedCalls = [];
|
|
633
|
+
for (const program of builderPrograms)
|
|
634
|
+
collectCalls(program, collectedCalls);
|
|
635
|
+
const views = flattenStructs(structs);
|
|
636
|
+
const calls = resolvedCalls(collectedCalls, views, snapshot.checker, typescript);
|
|
637
|
+
const sourceMap = createVuneSourceMap(source, generatedSource, fileName);
|
|
638
|
+
const graphSymbols = typescriptGraphSymbols(source, generatedSource, typescript, snapshot.checker, sourceMap);
|
|
639
|
+
const symbolTable = new SemanticModel();
|
|
640
|
+
for (const view of canonicalViewSymbols().values()) {
|
|
641
|
+
symbolTable.register(view);
|
|
642
|
+
for (const initializer of view.initializers)
|
|
643
|
+
symbolTable.register(initializer);
|
|
644
|
+
}
|
|
645
|
+
for (const view of views) {
|
|
646
|
+
symbolTable.register(view.symbol);
|
|
647
|
+
for (const initializer of view.symbol.initializers)
|
|
648
|
+
symbolTable.register(initializer);
|
|
649
|
+
for (const field of view.symbol.fields) {
|
|
650
|
+
if (field.kind === "state")
|
|
651
|
+
symbolTable.register({ kind: "state", name: `${view.qualifiedName}.${field.name}`, type: field.type });
|
|
652
|
+
if (field.kind === "binding")
|
|
653
|
+
symbolTable.register({ kind: "binding", name: `${view.qualifiedName}.${field.name}`, type: field.type });
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
symbolTable.register({
|
|
657
|
+
kind: "builder",
|
|
658
|
+
name: "ViewBuilder",
|
|
659
|
+
contentType: "View",
|
|
660
|
+
operations: ["buildBlock", "buildOptional", "buildEither", "buildArray"],
|
|
661
|
+
});
|
|
662
|
+
for (const foreign of graphSymbols.foreignComponents)
|
|
663
|
+
symbolTable.register(foreign.symbol);
|
|
664
|
+
for (const element of graphSymbols.htmlElements)
|
|
665
|
+
symbolTable.register(element.symbol);
|
|
666
|
+
return {
|
|
667
|
+
kind: "VuneSemanticModel",
|
|
668
|
+
fileName,
|
|
669
|
+
source,
|
|
670
|
+
generatedSource,
|
|
671
|
+
typescript,
|
|
672
|
+
typeChecker: snapshot.checker,
|
|
673
|
+
typescriptDiagnostics: snapshot.diagnostics,
|
|
674
|
+
htmlDiagnostics: graphSymbols.htmlDiagnostics,
|
|
675
|
+
structs,
|
|
676
|
+
views,
|
|
677
|
+
builderPrograms,
|
|
678
|
+
calls,
|
|
679
|
+
imports: importsOf(source, generatedSource, typescript, sourceMap),
|
|
680
|
+
htmlElements: graphSymbols.htmlElements,
|
|
681
|
+
foreignComponents: graphSymbols.foreignComponents,
|
|
682
|
+
symbolTable,
|
|
683
|
+
symbols: symbolTable.values(),
|
|
684
|
+
view(name) {
|
|
685
|
+
return views.find(view => view.name === name || view.qualifiedName === name);
|
|
686
|
+
},
|
|
687
|
+
symbol(name) {
|
|
688
|
+
return symbolTable.get(name);
|
|
689
|
+
},
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
//# sourceMappingURL=semantic.js.map
|