sveld 0.25.2 → 0.25.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 +15 -0
- package/lib/ComponentParser.d.ts +10 -0
- package/lib/ComponentParser.js +123 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -332,6 +332,21 @@ export let kind = "primary";
|
|
|
332
332
|
// inferred type: "string"
|
|
333
333
|
```
|
|
334
334
|
|
|
335
|
+
For template literal default values, `sveld` automatically infers [template literal types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html) when possible:
|
|
336
|
+
|
|
337
|
+
```js
|
|
338
|
+
export let id = `ccs-${Math.random().toString(36)}`;
|
|
339
|
+
// inferred type: `ccs-${string}` & {}
|
|
340
|
+
|
|
341
|
+
export let prefix = `prefix-`;
|
|
342
|
+
// inferred type: `prefix-` & {}
|
|
343
|
+
|
|
344
|
+
export let suffix = `-suffix`;
|
|
345
|
+
// inferred type: `-suffix` & {}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
The `& {}` intersection type makes the template literal type more permissive, allowing regular strings to be assigned while preserving the template literal type for better autocomplete and type hints. This provides more precise type checking than a generic `string` type while remaining flexible. For complex expressions in template literals, the type defaults to `string` for safety.
|
|
349
|
+
|
|
335
350
|
Use the `@type` tag to explicitly document the type. In the following example, the `kind` property has an enumerated (enum) type.
|
|
336
351
|
|
|
337
352
|
Signature:
|
package/lib/ComponentParser.d.ts
CHANGED
|
@@ -128,6 +128,16 @@ export default class ComponentParser {
|
|
|
128
128
|
* (e.g., Number.POSITIVE_INFINITY, Math.PI, etc.)
|
|
129
129
|
*/
|
|
130
130
|
private isNumericConstant;
|
|
131
|
+
/**
|
|
132
|
+
* Infers the TypeScript type for an expression in a template literal.
|
|
133
|
+
* Returns a type string that can be used in a template literal type.
|
|
134
|
+
*/
|
|
135
|
+
private inferExpressionType;
|
|
136
|
+
/**
|
|
137
|
+
* Infers a template literal type from a TemplateLiteral AST node.
|
|
138
|
+
* Returns a TypeScript template literal type string like `prefix-${string}`.
|
|
139
|
+
*/
|
|
140
|
+
private inferTemplateLiteralType;
|
|
131
141
|
private sourceAtPos;
|
|
132
142
|
private collectReactiveVars;
|
|
133
143
|
private addProp;
|
package/lib/ComponentParser.js
CHANGED
|
@@ -131,6 +131,119 @@ class ComponentParser {
|
|
|
131
131
|
}
|
|
132
132
|
return false;
|
|
133
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Infers the TypeScript type for an expression in a template literal.
|
|
136
|
+
* Returns a type string that can be used in a template literal type.
|
|
137
|
+
*/
|
|
138
|
+
inferExpressionType(expr) {
|
|
139
|
+
if (!expr || typeof expr !== "object" || !("type" in expr)) {
|
|
140
|
+
return "string";
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* @example `"hello"` → `"hello"`
|
|
144
|
+
* @example `42` → `42`
|
|
145
|
+
* @example `true` → `true`
|
|
146
|
+
*/
|
|
147
|
+
if (expr.type === "Literal") {
|
|
148
|
+
if (typeof expr.value === "string") {
|
|
149
|
+
return `"${expr.value}"`;
|
|
150
|
+
}
|
|
151
|
+
if (typeof expr.value === "number") {
|
|
152
|
+
return `${expr.value}`;
|
|
153
|
+
}
|
|
154
|
+
if (typeof expr.value === "boolean") {
|
|
155
|
+
return `${expr.value}`;
|
|
156
|
+
}
|
|
157
|
+
return "string";
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* @example `someVar` → `string`
|
|
161
|
+
*/
|
|
162
|
+
if (expr.type === "Identifier") {
|
|
163
|
+
// For variables, we can't know the exact type, so default to string
|
|
164
|
+
// Users can add explicit @type annotations if they need more precision
|
|
165
|
+
return "string";
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* @example `Math.PI` → `number`
|
|
169
|
+
* @example `Number.MAX_VALUE` → `number`
|
|
170
|
+
* @example `Math.random().toString` → `string`
|
|
171
|
+
*/
|
|
172
|
+
if (expr.type === "MemberExpression") {
|
|
173
|
+
// Check if it's a well-known numeric constant
|
|
174
|
+
if (this.isNumericConstant(expr)) {
|
|
175
|
+
return "number";
|
|
176
|
+
}
|
|
177
|
+
// For other member expressions, default to string
|
|
178
|
+
// Common cases like Math.random().toString(36) will be string
|
|
179
|
+
return "string";
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* @example `Math.random().toString(36)` → `string`
|
|
183
|
+
* @example `someFunction()` → `string`
|
|
184
|
+
*/
|
|
185
|
+
if (expr.type === "CallExpression") {
|
|
186
|
+
// Most call expressions in template literals return strings or numbers
|
|
187
|
+
// Default to string for safety
|
|
188
|
+
return "string";
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* @example `a + b` → `string`
|
|
192
|
+
* @example `x * y` → `string`
|
|
193
|
+
*/
|
|
194
|
+
if (expr.type === "BinaryExpression") {
|
|
195
|
+
// For string concatenation or arithmetic, default to string
|
|
196
|
+
return "string";
|
|
197
|
+
}
|
|
198
|
+
return "string";
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Infers a template literal type from a TemplateLiteral AST node.
|
|
202
|
+
* Returns a TypeScript template literal type string like `prefix-${string}`.
|
|
203
|
+
*/
|
|
204
|
+
inferTemplateLiteralType(templateLiteral) {
|
|
205
|
+
if (!templateLiteral || typeof templateLiteral !== "object" || templateLiteral.type !== "TemplateLiteral") {
|
|
206
|
+
return "string";
|
|
207
|
+
}
|
|
208
|
+
const quasis = templateLiteral.quasis || [];
|
|
209
|
+
const expressions = templateLiteral.expressions || [];
|
|
210
|
+
// If there are no expressions, it's a static string
|
|
211
|
+
if (expressions.length === 0) {
|
|
212
|
+
if (quasis.length === 1) {
|
|
213
|
+
const staticValue = quasis[0].value?.cooked || quasis[0].value?.raw || "";
|
|
214
|
+
// Escape backticks and backslashes in the static string
|
|
215
|
+
const escaped = staticValue.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
|
216
|
+
const templateLiteralType = `\`${escaped}\``;
|
|
217
|
+
// Use the (template-literal & {}) trick to make the type more permissive
|
|
218
|
+
return `(${templateLiteralType} & {})`;
|
|
219
|
+
}
|
|
220
|
+
return "string";
|
|
221
|
+
}
|
|
222
|
+
// Build the template literal type
|
|
223
|
+
const parts = [];
|
|
224
|
+
for (let i = 0; i < quasis.length; i++) {
|
|
225
|
+
const quasi = quasis[i];
|
|
226
|
+
const staticValue = quasi.value?.cooked || quasi.value?.raw || "";
|
|
227
|
+
// Escape backticks and backslashes in static parts
|
|
228
|
+
const escaped = staticValue.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
|
229
|
+
if (escaped) {
|
|
230
|
+
parts.push(escaped);
|
|
231
|
+
}
|
|
232
|
+
// Add the expression type if there's a corresponding expression
|
|
233
|
+
if (i < expressions.length) {
|
|
234
|
+
const exprType = this.inferExpressionType(expressions[i]);
|
|
235
|
+
parts.push(`\${${exprType}}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// If we couldn't build a meaningful template literal type, fall back to string
|
|
239
|
+
if (parts.length === 0) {
|
|
240
|
+
return "string";
|
|
241
|
+
}
|
|
242
|
+
const templateLiteralType = `\`${parts.join("")}\``;
|
|
243
|
+
// Use the (template-literal & {}) trick to make the type more permissive
|
|
244
|
+
// This allows regular strings to be assigned while preserving template literal type for autocomplete
|
|
245
|
+
return `(${templateLiteralType} & {})`;
|
|
246
|
+
}
|
|
134
247
|
sourceAtPos(start, end) {
|
|
135
248
|
return this.source?.slice(start, end);
|
|
136
249
|
}
|
|
@@ -835,6 +948,11 @@ class ComponentParser {
|
|
|
835
948
|
}
|
|
836
949
|
// Otherwise, don't infer type, just preserve existing type annotation.
|
|
837
950
|
}
|
|
951
|
+
else if (init.type === "TemplateLiteral") {
|
|
952
|
+
// Handle template literals - infer template literal type when possible
|
|
953
|
+
value = this.sourceAtPos(init.start, init.end);
|
|
954
|
+
type = this.inferTemplateLiteralType(init);
|
|
955
|
+
}
|
|
838
956
|
else {
|
|
839
957
|
value = init.raw;
|
|
840
958
|
type = init.value == null ? undefined : typeof init.value;
|
|
@@ -1023,6 +1141,11 @@ class ComponentParser {
|
|
|
1023
1141
|
}
|
|
1024
1142
|
// Otherwise, don't infer type, just preserve existing type annotation.
|
|
1025
1143
|
}
|
|
1144
|
+
else if (init.type === "TemplateLiteral") {
|
|
1145
|
+
// Handle template literals - infer template literal type when possible
|
|
1146
|
+
value = this.sourceAtPos(init.start, init.end);
|
|
1147
|
+
type = this.inferTemplateLiteralType(init);
|
|
1148
|
+
}
|
|
1026
1149
|
else {
|
|
1027
1150
|
value = init.raw;
|
|
1028
1151
|
type = init.value == null ? undefined : typeof init.value;
|