sveld 0.25.1 → 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 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:
@@ -112,10 +112,12 @@ export default class ComponentParser {
112
112
  private readonly bindings;
113
113
  private readonly contexts;
114
114
  private variableInfoCache;
115
+ private sourceLinesCache?;
115
116
  constructor(options?: ComponentParserOptions);
116
117
  private static mapToArray;
117
118
  private static assignValue;
118
119
  private static formatComment;
120
+ private getCommentTags;
119
121
  /**
120
122
  * Finds the last comment from an array of leading comments.
121
123
  * TypeScript directives are stripped before parsing, so we can safely take the last comment.
@@ -126,6 +128,16 @@ export default class ComponentParser {
126
128
  * (e.g., Number.POSITIVE_INFINITY, Math.PI, etc.)
127
129
  */
128
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;
129
141
  private sourceAtPos;
130
142
  private collectReactiveVars;
131
143
  private addProp;
@@ -137,6 +149,8 @@ export default class ComponentParser {
137
149
  private buildEventDetailFromProperties;
138
150
  private generateContextTypeName;
139
151
  private buildVariableInfoCache;
152
+ private static readonly VAR_NAME_REGEX_CACHE;
153
+ private static getVarNameRegexes;
140
154
  private findVariableTypeAndDescription;
141
155
  private parseContextValue;
142
156
  private parseSetContextCall;
@@ -35,6 +35,7 @@ class ComponentParser {
35
35
  bindings = new Map();
36
36
  contexts = new Map();
37
37
  variableInfoCache = new Map();
38
+ sourceLinesCache;
38
39
  constructor(options) {
39
40
  this.options = options;
40
41
  }
@@ -54,6 +55,45 @@ class ComponentParser {
54
55
  }
55
56
  return formatted_comment;
56
57
  }
58
+ getCommentTags(parsed) {
59
+ const tags = parsed[0]?.tags ?? [];
60
+ const excludedTags = new Set([
61
+ "type",
62
+ "param",
63
+ "returns",
64
+ "return",
65
+ "extends",
66
+ "restProps",
67
+ "slot",
68
+ "event",
69
+ "typedef",
70
+ ]);
71
+ let typeTag;
72
+ const paramTags = [];
73
+ let returnsTag;
74
+ const additionalTags = [];
75
+ for (const tag of tags) {
76
+ if (tag.tag === "type") {
77
+ typeTag = tag;
78
+ }
79
+ else if (tag.tag === "param") {
80
+ paramTags.push(tag);
81
+ }
82
+ else if (tag.tag === "returns" || tag.tag === "return") {
83
+ returnsTag = tag;
84
+ }
85
+ else if (!excludedTags.has(tag.tag)) {
86
+ additionalTags.push(tag);
87
+ }
88
+ }
89
+ return {
90
+ type: typeTag,
91
+ param: paramTags,
92
+ returns: returnsTag,
93
+ additional: additionalTags,
94
+ description: parsed[0]?.description,
95
+ };
96
+ }
57
97
  /**
58
98
  * Finds the last comment from an array of leading comments.
59
99
  * TypeScript directives are stripped before parsing, so we can safely take the last comment.
@@ -91,6 +131,119 @@ class ComponentParser {
91
131
  }
92
132
  return false;
93
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
+ }
94
247
  sourceAtPos(start, end) {
95
248
  return this.source?.slice(start, end);
96
249
  }
@@ -138,7 +291,9 @@ class ComponentParser {
138
291
  const name = default_slot ? DEFAULT_SLOT_NAME : (slot_name ?? "");
139
292
  const fallback = ComponentParser.assignValue(slot_fallback);
140
293
  const props = ComponentParser.assignValue(slot_props);
141
- const description = slot_description?.split("-").pop()?.trim();
294
+ const description = slot_description
295
+ ? slot_description.substring(slot_description.lastIndexOf("-") + 1).trim()
296
+ : undefined;
142
297
  if (this.slots.has(name)) {
143
298
  const existing_slot = this.slots.get(name);
144
299
  this.slots.set(name, {
@@ -167,7 +322,7 @@ class ComponentParser {
167
322
  * `@event` is not specified.
168
323
  */
169
324
  const default_detail = !has_argument && !detail ? "null" : ComponentParser.assignValue(detail);
170
- const event_description = description?.split("-").pop()?.trim();
325
+ const event_description = description ? description.substring(description.lastIndexOf("-") + 1).trim() : undefined;
171
326
  if (this.events.has(name)) {
172
327
  const existing_event = this.events.get(name);
173
328
  this.events.set(name, {
@@ -453,7 +608,10 @@ class ComponentParser {
453
608
  buildVariableInfoCache() {
454
609
  if (!this.source)
455
610
  return;
456
- const lines = this.source.split("\n");
611
+ if (!this.sourceLinesCache) {
612
+ this.sourceLinesCache = this.source.split("\n");
613
+ }
614
+ const lines = this.sourceLinesCache;
457
615
  for (let i = 0; i < lines.length; i++) {
458
616
  const line = lines[i].trim();
459
617
  // Match variable declarations
@@ -477,14 +635,12 @@ class ComponentParser {
477
635
  const commentBlock = commentLines.join("\n");
478
636
  // Parse the JSDoc
479
637
  const parsed = (0, comment_parser_1.parse)(commentBlock, { spacing: "preserve" });
480
- if (parsed[0]?.tags) {
481
- const typeTag = parsed[0].tags.find((t) => t.tag === "type");
482
- if (typeTag) {
483
- this.variableInfoCache.set(varName, {
484
- type: this.aliasType(typeTag.type),
485
- description: parsed[0].description || typeTag.description,
486
- });
487
- }
638
+ const { type: typeTag, description } = this.getCommentTags(parsed);
639
+ if (typeTag) {
640
+ this.variableInfoCache.set(varName, {
641
+ type: this.aliasType(typeTag.type),
642
+ description: description || typeTag.description,
643
+ });
488
644
  }
489
645
  break;
490
646
  }
@@ -492,6 +648,20 @@ class ComponentParser {
492
648
  }
493
649
  }
494
650
  }
651
+ static VAR_NAME_REGEX_CACHE = new Map();
652
+ static getVarNameRegexes(varName) {
653
+ let cached = ComponentParser.VAR_NAME_REGEX_CACHE.get(varName);
654
+ if (!cached) {
655
+ const escaped = varName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
656
+ cached = [
657
+ new RegExp(`const\\s+${escaped}\\s*=`),
658
+ new RegExp(`let\\s+${escaped}\\s*=`),
659
+ new RegExp(`function\\s+${escaped}\\s*\\(`),
660
+ ];
661
+ ComponentParser.VAR_NAME_REGEX_CACHE.set(varName, cached);
662
+ }
663
+ return cached;
664
+ }
495
665
  findVariableTypeAndDescription(varName) {
496
666
  const cached = this.variableInfoCache.get(varName);
497
667
  if (cached) {
@@ -500,15 +670,18 @@ class ComponentParser {
500
670
  // Search through the source code directly for JSDoc comments
501
671
  if (!this.source)
502
672
  return null;
503
- // Build a map of variable names to their types by looking at the source
504
- const lines = this.source.split("\n");
673
+ if (!this.sourceLinesCache) {
674
+ this.sourceLinesCache = this.source.split("\n");
675
+ }
676
+ const lines = this.sourceLinesCache;
677
+ const [constRegex, letRegex, funcRegex] = ComponentParser.getVarNameRegexes(varName);
505
678
  for (let i = 0; i < lines.length; i++) {
506
679
  const line = lines[i].trim();
507
680
  // Check if this line declares our variable
508
681
  // Match patterns like: const varName = ..., let varName = ..., function varName
509
- const constMatch = line.match(new RegExp(`const\\s+${varName}\\s*=`));
510
- const letMatch = line.match(new RegExp(`let\\s+${varName}\\s*=`));
511
- const funcMatch = line.match(new RegExp(`function\\s+${varName}\\s*\\(`));
682
+ const constMatch = line.match(constRegex);
683
+ const letMatch = line.match(letRegex);
684
+ const funcMatch = line.match(funcRegex);
512
685
  if (constMatch || letMatch || funcMatch) {
513
686
  // Look backwards for JSDoc comment
514
687
  for (let j = i - 1; j >= 0; j--) {
@@ -527,14 +700,12 @@ class ComponentParser {
527
700
  const commentBlock = commentLines.join("\n");
528
701
  // Parse the JSDoc
529
702
  const parsed = (0, comment_parser_1.parse)(commentBlock, { spacing: "preserve" });
530
- if (parsed[0]?.tags) {
531
- const typeTag = parsed[0].tags.find((t) => t.tag === "type");
532
- if (typeTag) {
533
- return {
534
- type: this.aliasType(typeTag.type),
535
- description: parsed[0].description || typeTag.description,
536
- };
537
- }
703
+ const { type: typeTag, description } = this.getCommentTags(parsed);
704
+ if (typeTag) {
705
+ return {
706
+ type: this.aliasType(typeTag.type),
707
+ description: description || typeTag.description,
708
+ };
538
709
  }
539
710
  break;
540
711
  }
@@ -685,6 +856,7 @@ class ComponentParser {
685
856
  this.bindings.clear();
686
857
  this.contexts.clear();
687
858
  this.variableInfoCache.clear();
859
+ this.sourceLinesCache = undefined;
688
860
  }
689
861
  // Pre-compiled regexes for better performance
690
862
  static SCRIPT_BLOCK_REGEX = /(<script[^>]*>)([\s\S]*?)(<\/script>)/gi;
@@ -718,6 +890,7 @@ class ComponentParser {
718
890
  // The compile result includes the parsed AST
719
891
  this.parsed = compiled.ast || (0, compiler_1.parse)(cleanedSource);
720
892
  this.collectReactiveVars();
893
+ this.sourceLinesCache = this.source.split("\n");
721
894
  this.buildVariableInfoCache();
722
895
  this.parseCustomTypes();
723
896
  if (this.parsed?.module) {
@@ -775,6 +948,11 @@ class ComponentParser {
775
948
  }
776
949
  // Otherwise, don't infer type, just preserve existing type annotation.
777
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
+ }
778
956
  else {
779
957
  value = init.raw;
780
958
  type = init.value == null ? undefined : typeof init.value;
@@ -798,12 +976,11 @@ class ComponentParser {
798
976
  const comment = (0, comment_parser_1.parse)(ComponentParser.formatComment(jsdoc_comment.value), {
799
977
  spacing: "preserve",
800
978
  });
979
+ const { type: typeTag, param: paramTags, returns: returnsTag, additional: additionalTags, description: commentDescription, } = this.getCommentTags(comment);
801
980
  // Extract @type tag
802
- const typeTag = comment[0]?.tags.find((t) => t.tag === "type");
803
981
  if (typeTag)
804
982
  type = this.aliasType(typeTag.type);
805
983
  // Extract @param tags
806
- const paramTags = comment[0]?.tags.filter((t) => t.tag === "param") ?? [];
807
984
  if (paramTags.length > 0) {
808
985
  params = paramTags
809
986
  .filter((tag) => !tag.name.includes(".")) // Exclude nested params like "options.expand"
@@ -815,27 +992,20 @@ class ComponentParser {
815
992
  }));
816
993
  }
817
994
  // Extract @returns/@return tag
818
- const returnsTag = comment[0]?.tags.find((t) => t.tag === "returns" || t.tag === "return");
819
995
  if (returnsTag)
820
996
  returnType = this.aliasType(returnsTag.type);
821
997
  // Build description from comment description and non-param/non-type tags
822
- const commentDescription = ComponentParser.assignValue(comment[0]?.description?.trim());
823
- const additionalTags = comment[0]?.tags.filter((tag) => ![
824
- "type",
825
- "param",
826
- "returns",
827
- "return",
828
- "extends",
829
- "restProps",
830
- "slot",
831
- "event",
832
- "typedef",
833
- ].includes(tag.tag)) ?? [];
834
- if (commentDescription || additionalTags.length > 0) {
835
- description = commentDescription || "";
998
+ const formattedDescription = ComponentParser.assignValue(commentDescription?.trim());
999
+ if (formattedDescription || additionalTags.length > 0) {
1000
+ const descriptionParts = [];
1001
+ if (formattedDescription) {
1002
+ descriptionParts.push(formattedDescription);
1003
+ }
836
1004
  for (const tag of additionalTags) {
837
- description += `${description ? "\n" : ""}@${tag.tag}${tag.name ? ` ${tag.name}` : ""}${tag.description ? ` ${tag.description}` : ""}`;
1005
+ const tagStr = `@${tag.tag}${tag.name ? ` ${tag.name}` : ""}${tag.description ? ` ${tag.description}` : ""}`;
1006
+ descriptionParts.push(tagStr);
838
1007
  }
1008
+ description = descriptionParts.join("\n");
839
1009
  }
840
1010
  }
841
1011
  }
@@ -971,6 +1141,11 @@ class ComponentParser {
971
1141
  }
972
1142
  // Otherwise, don't infer type, just preserve existing type annotation.
973
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
+ }
974
1149
  else {
975
1150
  value = init.raw;
976
1151
  type = init.value == null ? undefined : typeof init.value;
@@ -994,12 +1169,11 @@ class ComponentParser {
994
1169
  const comment = (0, comment_parser_1.parse)(ComponentParser.formatComment(jsdoc_comment.value), {
995
1170
  spacing: "preserve",
996
1171
  });
1172
+ const { type: typeTag, param: paramTags, returns: returnsTag, additional: additional_tags, description: commentDescription, } = this.getCommentTags(comment);
997
1173
  // Extract @type tag
998
- const typeTag = comment[0]?.tags.find((t) => t.tag === "type");
999
1174
  if (typeTag)
1000
1175
  type = this.aliasType(typeTag.type);
1001
1176
  // Extract @param tags
1002
- const paramTags = comment[0]?.tags.filter((t) => t.tag === "param") ?? [];
1003
1177
  if (paramTags.length > 0) {
1004
1178
  params = paramTags
1005
1179
  .filter((tag) => !tag.name.includes(".")) // Exclude nested params like "options.expand"
@@ -1011,27 +1185,20 @@ class ComponentParser {
1011
1185
  }));
1012
1186
  }
1013
1187
  // Extract @returns/@return tag
1014
- const returnsTag = comment[0]?.tags.find((t) => t.tag === "returns" || t.tag === "return");
1015
1188
  if (returnsTag)
1016
1189
  returnType = this.aliasType(returnsTag.type);
1017
1190
  // Build description from comment description and non-param/non-type tags
1018
- const commentDescription = ComponentParser.assignValue(comment[0]?.description?.trim());
1019
- const additional_tags = comment[0]?.tags.filter((tag) => ![
1020
- "type",
1021
- "param",
1022
- "returns",
1023
- "return",
1024
- "extends",
1025
- "restProps",
1026
- "slot",
1027
- "event",
1028
- "typedef",
1029
- ].includes(tag.tag)) ?? [];
1030
- if (commentDescription || additional_tags.length > 0) {
1031
- description = commentDescription || "";
1191
+ const formattedDescription = ComponentParser.assignValue(commentDescription?.trim());
1192
+ if (formattedDescription || additional_tags.length > 0) {
1193
+ const descriptionParts = [];
1194
+ if (formattedDescription) {
1195
+ descriptionParts.push(formattedDescription);
1196
+ }
1032
1197
  for (const tag of additional_tags) {
1033
- description += `${description ? "\n" : ""}@${tag.tag}${tag.name ? ` ${tag.name}` : ""}${tag.description ? ` ${tag.description}` : ""}`;
1198
+ const tagStr = `@${tag.tag}${tag.name ? ` ${tag.name}` : ""}${tag.description ? ` ${tag.description}` : ""}`;
1199
+ descriptionParts.push(tagStr);
1034
1200
  }
1201
+ description = descriptionParts.join("\n");
1035
1202
  }
1036
1203
  }
1037
1204
  }
@@ -1113,7 +1280,9 @@ class ComponentParser {
1113
1280
  const existing_event = this.events.get(node.name);
1114
1281
  // Check if this event has a JSDoc description
1115
1282
  const description = this.eventDescriptions.get(node.name);
1116
- const event_description = description?.split("-").pop()?.trim();
1283
+ const event_description = description
1284
+ ? description.substring(description.lastIndexOf("-") + 1).trim()
1285
+ : undefined;
1117
1286
  if (!existing_event) {
1118
1287
  // Add new forwarded event
1119
1288
  this.events.set(node.name, {
@@ -1183,7 +1352,9 @@ class ComponentParser {
1183
1352
  // If event is marked as dispatched but is NOT actually dispatched, convert it to forwarded
1184
1353
  if (event && event.type === "dispatched" && !actuallyDispatchedEvents.has(eventName)) {
1185
1354
  const description = this.eventDescriptions.get(eventName);
1186
- const event_description = description?.split("-").pop()?.trim();
1355
+ const event_description = description
1356
+ ? description.substring(description.lastIndexOf("-") + 1).trim()
1357
+ : undefined;
1187
1358
  const forwardedEvent = {
1188
1359
  type: "forwarded",
1189
1360
  name: eventName,
@@ -1199,56 +1370,62 @@ class ComponentParser {
1199
1370
  this.events.set(eventName, forwardedEvent);
1200
1371
  }
1201
1372
  });
1202
- return {
1203
- props: ComponentParser.mapToArray(this.props).map((prop) => {
1204
- if (this.bindings.has(prop.name)) {
1205
- return {
1206
- ...prop,
1207
- type: "null | " +
1208
- this.bindings
1209
- .get(prop.name)
1210
- ?.elements.sort()
1211
- .map((element) => (0, element_tag_map_1.getElementByTag)(element))
1212
- .join(" | "),
1213
- };
1214
- }
1215
- return prop;
1216
- }),
1217
- moduleExports: ComponentParser.mapToArray(this.moduleExports),
1218
- slots: ComponentParser.mapToArray(this.slots)
1219
- .map((slot) => {
1220
- try {
1221
- const slot_props = JSON.parse(slot.slot_props);
1222
- const new_props = [];
1223
- for (const key of Object.keys(slot_props)) {
1224
- if (slot_props[key].replace && slot_props[key].value !== undefined) {
1225
- slot_props[key].value = this.props.get(slot_props[key].value)?.type;
1226
- }
1227
- if (slot_props[key].value === undefined)
1228
- slot_props[key].value = "any";
1229
- new_props.push(`${key}: ${slot_props[key].value}`);
1373
+ const processedProps = ComponentParser.mapToArray(this.props).map((prop) => {
1374
+ if (this.bindings.has(prop.name)) {
1375
+ const elementTypes = this.bindings
1376
+ .get(prop.name)
1377
+ ?.elements.sort()
1378
+ .map((element) => (0, element_tag_map_1.getElementByTag)(element))
1379
+ .join(" | ");
1380
+ return {
1381
+ ...prop,
1382
+ type: `null | ${elementTypes}`,
1383
+ };
1384
+ }
1385
+ return prop;
1386
+ });
1387
+ const processedSlots = ComponentParser.mapToArray(this.slots)
1388
+ .map((slot) => {
1389
+ try {
1390
+ const slot_props = JSON.parse(slot.slot_props);
1391
+ const new_props = [];
1392
+ for (const key of Object.keys(slot_props)) {
1393
+ if (slot_props[key].replace && slot_props[key].value !== undefined) {
1394
+ slot_props[key].value = this.props.get(slot_props[key].value)?.type;
1230
1395
  }
1231
- const formatted_slot_props = new_props.length === 0 ? "Record<string, never>" : `{ ${new_props.join(", ")} }`;
1232
- return { ...slot, slot_props: formatted_slot_props };
1396
+ if (slot_props[key].value === undefined)
1397
+ slot_props[key].value = "any";
1398
+ new_props.push(`${key}: ${slot_props[key].value}`);
1233
1399
  }
1234
- catch (_e) {
1235
- return slot;
1236
- }
1237
- })
1238
- .sort((a, b) => {
1239
- if (a.name < b.name)
1240
- return -1;
1241
- if (a.name > b.name)
1242
- return 1;
1243
- return 0;
1244
- }),
1245
- events: ComponentParser.mapToArray(this.events),
1246
- typedefs: ComponentParser.mapToArray(this.typedefs),
1400
+ const formatted_slot_props = new_props.length === 0 ? "Record<string, never>" : `{ ${new_props.join(", ")} }`;
1401
+ return { ...slot, slot_props: formatted_slot_props };
1402
+ }
1403
+ catch (_e) {
1404
+ return slot;
1405
+ }
1406
+ })
1407
+ .sort((a, b) => {
1408
+ if (a.name < b.name)
1409
+ return -1;
1410
+ if (a.name > b.name)
1411
+ return 1;
1412
+ return 0;
1413
+ });
1414
+ const moduleExportsArray = ComponentParser.mapToArray(this.moduleExports);
1415
+ const eventsArray = ComponentParser.mapToArray(this.events);
1416
+ const typedefsArray = ComponentParser.mapToArray(this.typedefs);
1417
+ const contextsArray = ComponentParser.mapToArray(this.contexts);
1418
+ return {
1419
+ props: processedProps,
1420
+ moduleExports: moduleExportsArray,
1421
+ slots: processedSlots,
1422
+ events: eventsArray,
1423
+ typedefs: typedefsArray,
1247
1424
  generics: this.generics,
1248
1425
  rest_props: this.rest_props,
1249
1426
  extends: this.extends,
1250
1427
  componentComment: this.componentComment,
1251
- contexts: ComponentParser.mapToArray(this.contexts),
1428
+ contexts: contextsArray,
1252
1429
  };
1253
1430
  }
1254
1431
  }
@@ -7,6 +7,7 @@ const node_fs_1 = require("node:fs");
7
7
  const node_path_1 = require("node:path");
8
8
  const path_1 = require("./path");
9
9
  const configCache = new Map();
10
+ const pathPatternRegexCache = new Map();
10
11
  const COMMENT_PATTERN = /\/\*[\s\S]*?\*\/|\/\/.*/g;
11
12
  const REGEX_SPECIAL_CHARS = /[.+?^${}()|[\]\\]/g;
12
13
  function clearConfigCache() {
@@ -83,12 +84,16 @@ function resolvePathAliasAbsolute(importPath, fromDir) {
83
84
  // e.g., "$lib/*" -> /^\$lib\/(.*)$/
84
85
  // e.g., "$lib" -> /^\$lib$/
85
86
  // e.g., "@components/*" -> /^@components\/(.*)$/
86
- // Escape special regex chars but keep * for replacement
87
- const escapedPattern = pattern
88
- .split("*")
89
- .map((part) => part.replace(REGEX_SPECIAL_CHARS, "\\$&"))
90
- .join("(.*)");
91
- const regex = new RegExp(`^${escapedPattern}$`);
87
+ let regex = pathPatternRegexCache.get(pattern);
88
+ if (!regex) {
89
+ // Escape special regex chars but keep * for replacement
90
+ const escapedPattern = pattern
91
+ .split("*")
92
+ .map((part) => part.replace(REGEX_SPECIAL_CHARS, "\\$&"))
93
+ .join("(.*)");
94
+ regex = new RegExp(`^${escapedPattern}$`);
95
+ pathPatternRegexCache.set(pattern, regex);
96
+ }
92
97
  const match = importPath.match(regex);
93
98
  if (match) {
94
99
  // Use the first mapping (TypeScript uses the first match)
@@ -69,42 +69,47 @@ async function generateBundle(input, glob) {
69
69
  const allComponentsForTypes = new Map();
70
70
  const exportEntries = Object.entries(exports);
71
71
  const allComponentEntries = Object.entries(allComponents);
72
- // Process exported components (for metadata/JSON/Markdown)
73
- const componentPromises = exportEntries.map(async ([exportName, entry]) => {
72
+ const uniqueFilePaths = new Set();
73
+ for (const [, entry] of exportEntries) {
74
74
  const filePath = entry.source;
75
- const { ext, name } = (0, node_path_1.parse)(filePath);
76
- let moduleName = exportName;
77
- if (exportEntries.length === 1 && exportName === "default") {
78
- moduleName = name;
75
+ const { ext } = (0, node_path_1.parse)(filePath);
76
+ if (ext === ".svelte") {
77
+ uniqueFilePaths.add((0, node_path_1.resolve)(dir, filePath));
79
78
  }
79
+ }
80
+ for (const [, entry] of allComponentEntries) {
81
+ const filePath = entry.source;
82
+ const { ext } = (0, node_path_1.parse)(filePath);
80
83
  if (ext === ".svelte") {
81
- const source = await (0, promises_1.readFile)((0, node_path_1.resolve)(dir, filePath), "utf-8");
82
- const { code: processed } = await (0, compiler_1.preprocess)(source, [(0, svelte_preprocess_1.typescript)(), (0, svelte_preprocess_1.replace)([[STYLE_TAG_REGEX, ""]])], {
83
- filename: (0, node_path_1.basename)(filePath),
84
- });
85
- const parser = new ComponentParser_1.default();
86
- const parsed = parser.parseSvelteComponent(processed, {
87
- moduleName,
88
- filePath,
89
- });
90
- return {
91
- moduleName,
92
- filePath,
93
- ...parsed,
94
- };
84
+ uniqueFilePaths.add((0, node_path_1.resolve)(dir, filePath));
95
85
  }
96
- return null;
97
- });
98
- // Process all components (for .d.ts generation)
99
- const allComponentPromises = allComponentEntries.map(async ([exportName, entry]) => {
86
+ }
87
+ const fileContents = await Promise.all(Array.from(uniqueFilePaths).map(async (filePath) => {
88
+ try {
89
+ const content = await (0, promises_1.readFile)(filePath, "utf-8");
90
+ return { path: filePath, content };
91
+ }
92
+ catch (error) {
93
+ console.warn(`Warning: Failed to read file ${filePath}:`, error);
94
+ return { path: filePath, content: null };
95
+ }
96
+ }));
97
+ const fileMap = new Map(fileContents.map(({ path, content }) => [path, content]));
98
+ // Helper function to process a single component
99
+ const processComponent = async ([exportName, entry], entries, fileMap) => {
100
100
  const filePath = entry.source;
101
101
  const { ext, name } = (0, node_path_1.parse)(filePath);
102
102
  let moduleName = exportName;
103
- if (allComponentEntries.length === 1 && exportName === "default") {
103
+ if (entries.length === 1 && exportName === "default") {
104
104
  moduleName = name;
105
105
  }
106
106
  if (ext === ".svelte") {
107
- const source = await (0, promises_1.readFile)((0, node_path_1.resolve)(dir, filePath), "utf-8");
107
+ const resolvedPath = (0, node_path_1.resolve)(dir, filePath);
108
+ const source = fileMap.get(resolvedPath);
109
+ if (source === null || source === undefined) {
110
+ // File was not found or failed to read, skip this component
111
+ return null;
112
+ }
108
113
  const { code: processed } = await (0, compiler_1.preprocess)(source, [(0, svelte_preprocess_1.typescript)(), (0, svelte_preprocess_1.replace)([[STYLE_TAG_REGEX, ""]])], {
109
114
  filename: (0, node_path_1.basename)(filePath),
110
115
  });
@@ -120,7 +125,11 @@ async function generateBundle(input, glob) {
120
125
  };
121
126
  }
122
127
  return null;
123
- });
128
+ };
129
+ // Process exported components (for metadata/JSON/Markdown)
130
+ const componentPromises = exportEntries.map((entry) => processComponent(entry, exportEntries, fileMap));
131
+ // Process all components (for .d.ts generation)
132
+ const allComponentPromises = allComponentEntries.map((entry) => processComponent(entry, allComponentEntries, fileMap));
124
133
  const [results, allResults] = await Promise.all([Promise.all(componentPromises), Promise.all(allComponentPromises)]);
125
134
  for (const result of results) {
126
135
  if (result) {
@@ -10,10 +10,11 @@ interface TocLine {
10
10
  }
11
11
  export default class WriterMarkdown extends Writer {
12
12
  onAppend?: OnAppend;
13
- source: string;
13
+ private sourceParts;
14
14
  hasToC: boolean;
15
15
  toc: TocLine[];
16
16
  constructor(options: MarkdownOptions);
17
+ get source(): string;
17
18
  appendLineBreaks(): this;
18
19
  append(type: AppendType, raw?: string): this;
19
20
  tableOfContents(): this;
@@ -8,15 +8,18 @@ const BACKTICK_REGEX = /`/g;
8
8
  const WHITESPACE_REGEX = /\s+/g;
9
9
  class WriterMarkdown extends Writer_1.default {
10
10
  onAppend;
11
- source = "";
11
+ sourceParts = [];
12
12
  hasToC = false;
13
13
  toc = [];
14
14
  constructor(options) {
15
15
  super({ parser: "markdown", printWidth: 80 });
16
16
  this.onAppend = options.onAppend;
17
17
  }
18
+ get source() {
19
+ return this.sourceParts.join("");
20
+ }
18
21
  appendLineBreaks() {
19
- this.source += "\n\n";
22
+ this.sourceParts.push("\n\n");
20
23
  return this;
21
24
  }
22
25
  append(type, raw) {
@@ -28,9 +31,7 @@ class WriterMarkdown extends Writer_1.default {
28
31
  case "h5":
29
32
  case "h6": {
30
33
  const length = Number(type.slice(-1));
31
- this.source += `${Array.from({ length })
32
- .map((_) => "#")
33
- .join("")} ${raw}`;
34
+ this.sourceParts.push(`${"#".repeat(length)} ${raw}`);
34
35
  if (this.hasToC && type === "h2") {
35
36
  this.toc.push({
36
37
  array: Array.from({ length: (length - 1) * 2 }),
@@ -40,16 +41,16 @@ class WriterMarkdown extends Writer_1.default {
40
41
  break;
41
42
  }
42
43
  case "quote":
43
- this.source += `> ${raw}`;
44
+ this.sourceParts.push(`> ${raw}`);
44
45
  break;
45
46
  case "p":
46
- this.source += raw;
47
+ this.sourceParts.push(raw ?? "");
47
48
  break;
48
49
  case "divider":
49
- this.source += "---";
50
+ this.sourceParts.push("---");
50
51
  break;
51
52
  case "raw":
52
- this.source += raw;
53
+ this.sourceParts.push(raw ?? "");
53
54
  break;
54
55
  }
55
56
  if (type !== "raw")
@@ -58,18 +59,18 @@ class WriterMarkdown extends Writer_1.default {
58
59
  return this;
59
60
  }
60
61
  tableOfContents() {
61
- this.source += "<!-- __TOC__ -->";
62
+ this.sourceParts.push("<!-- __TOC__ -->");
62
63
  this.hasToC = true;
63
64
  this.appendLineBreaks();
64
65
  return this;
65
66
  }
66
67
  end() {
67
- this.source = this.source.replace("<!-- __TOC__ -->", this.toc
68
+ const source = this.sourceParts.join("");
69
+ return source.replace("<!-- __TOC__ -->", this.toc
68
70
  .map(({ array, raw }) => {
69
71
  return `${array.join(" ")} - [${raw}](#${raw.toLowerCase().replace(BACKTICK_REGEX, "").replace(WHITESPACE_REGEX, "-")})`;
70
72
  })
71
73
  .join("\n"));
72
- return this.source;
73
74
  }
74
75
  }
75
76
  exports.default = WriterMarkdown;
@@ -10,10 +10,11 @@ interface TocLine {
10
10
  }
11
11
  export declare class BrowserWriterMarkdown {
12
12
  onAppend?: OnAppend;
13
- source: string;
13
+ private sourceParts;
14
14
  hasToC: boolean;
15
15
  toc: TocLine[];
16
16
  constructor(options: MarkdownOptions);
17
+ get source(): string;
17
18
  appendLineBreaks(): this;
18
19
  append(type: AppendType, raw?: string): this;
19
20
  tableOfContents(): this;
@@ -8,14 +8,17 @@ const WHITESPACE_REGEX = /\s+/g;
8
8
  // Browser-compatible WriterMarkdown that doesn't extend Writer
9
9
  class BrowserWriterMarkdown {
10
10
  onAppend;
11
- source = "";
11
+ sourceParts = [];
12
12
  hasToC = false;
13
13
  toc = [];
14
14
  constructor(options) {
15
15
  this.onAppend = options.onAppend;
16
16
  }
17
+ get source() {
18
+ return this.sourceParts.join("");
19
+ }
17
20
  appendLineBreaks() {
18
- this.source += "\n\n";
21
+ this.sourceParts.push("\n\n");
19
22
  return this;
20
23
  }
21
24
  append(type, raw) {
@@ -27,9 +30,7 @@ class BrowserWriterMarkdown {
27
30
  case "h5":
28
31
  case "h6": {
29
32
  const length = Number(type.slice(-1));
30
- this.source += `${Array.from({ length })
31
- .map((_) => "#")
32
- .join("")} ${raw}`;
33
+ this.sourceParts.push(`${"#".repeat(length)} ${raw}`);
33
34
  if (this.hasToC && type === "h2") {
34
35
  this.toc.push({
35
36
  array: Array.from({ length: (length - 1) * 2 }),
@@ -39,16 +40,16 @@ class BrowserWriterMarkdown {
39
40
  break;
40
41
  }
41
42
  case "quote":
42
- this.source += `> ${raw}`;
43
+ this.sourceParts.push(`> ${raw}`);
43
44
  break;
44
45
  case "p":
45
- this.source += raw;
46
+ this.sourceParts.push(raw ?? "");
46
47
  break;
47
48
  case "divider":
48
- this.source += "---";
49
+ this.sourceParts.push("---");
49
50
  break;
50
51
  case "raw":
51
- this.source += raw;
52
+ this.sourceParts.push(raw ?? "");
52
53
  break;
53
54
  }
54
55
  if (type !== "raw")
@@ -57,18 +58,18 @@ class BrowserWriterMarkdown {
57
58
  return this;
58
59
  }
59
60
  tableOfContents() {
60
- this.source += "<!-- __TOC__ -->";
61
+ this.sourceParts.push("<!-- __TOC__ -->");
61
62
  this.hasToC = true;
62
63
  this.appendLineBreaks();
63
64
  return this;
64
65
  }
65
66
  end() {
66
- this.source = this.source.replace("<!-- __TOC__ -->", this.toc
67
+ const source = this.sourceParts.join("");
68
+ return source.replace("<!-- __TOC__ -->", this.toc
67
69
  .map(({ array, raw }) => {
68
70
  return `${array.join(" ")} - [${raw}](#${raw.toLowerCase().replace(BACKTICK_REGEX, "").replace(WHITESPACE_REGEX, "-")})`;
69
71
  })
70
72
  .join("\n"));
71
- return this.source;
72
73
  }
73
74
  }
74
75
  exports.BrowserWriterMarkdown = BrowserWriterMarkdown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sveld",
3
- "version": "0.25.1",
3
+ "version": "0.25.3",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Generate TypeScript definitions for your Svelte components.",
6
6
  "main": "./lib/index.js",