sveld 0.25.0 → 0.25.2

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.
@@ -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.
@@ -137,6 +139,8 @@ export default class ComponentParser {
137
139
  private buildEventDetailFromProperties;
138
140
  private generateContextTypeName;
139
141
  private buildVariableInfoCache;
142
+ private static readonly VAR_NAME_REGEX_CACHE;
143
+ private static getVarNameRegexes;
140
144
  private findVariableTypeAndDescription;
141
145
  private parseContextValue;
142
146
  private parseSetContextCall;
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
5
5
  const comment_parser_1 = require("comment-parser");
6
6
  const compiler_1 = require("svelte/compiler");
7
7
  const element_tag_map_1 = require("./element-tag-map");
8
+ const COMMENT_BLOCK_DESCRIPTION_REGEX = /^-\s*/;
8
9
  const VAR_DECLARATION_REGEX = /(?:const|let|function)\s+(\w+)\s*[=(]/;
9
10
  const DEFAULT_SLOT_NAME = null;
10
11
  const TYPEDEF_END_REGEX = /(\}|\};)$/;
@@ -34,6 +35,7 @@ class ComponentParser {
34
35
  bindings = new Map();
35
36
  contexts = new Map();
36
37
  variableInfoCache = new Map();
38
+ sourceLinesCache;
37
39
  constructor(options) {
38
40
  this.options = options;
39
41
  }
@@ -53,6 +55,45 @@ class ComponentParser {
53
55
  }
54
56
  return formatted_comment;
55
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
+ }
56
97
  /**
57
98
  * Finds the last comment from an array of leading comments.
58
99
  * TypeScript directives are stripped before parsing, so we can safely take the last comment.
@@ -137,7 +178,9 @@ class ComponentParser {
137
178
  const name = default_slot ? DEFAULT_SLOT_NAME : (slot_name ?? "");
138
179
  const fallback = ComponentParser.assignValue(slot_fallback);
139
180
  const props = ComponentParser.assignValue(slot_props);
140
- const description = slot_description?.split("-").pop()?.trim();
181
+ const description = slot_description
182
+ ? slot_description.substring(slot_description.lastIndexOf("-") + 1).trim()
183
+ : undefined;
141
184
  if (this.slots.has(name)) {
142
185
  const existing_slot = this.slots.get(name);
143
186
  this.slots.set(name, {
@@ -166,7 +209,7 @@ class ComponentParser {
166
209
  * `@event` is not specified.
167
210
  */
168
211
  const default_detail = !has_argument && !detail ? "null" : ComponentParser.assignValue(detail);
169
- const event_description = description?.split("-").pop()?.trim();
212
+ const event_description = description ? description.substring(description.lastIndexOf("-") + 1).trim() : undefined;
170
213
  if (this.events.has(name)) {
171
214
  const existing_event = this.events.get(name);
172
215
  this.events.set(name, {
@@ -185,7 +228,9 @@ class ComponentParser {
185
228
  }
186
229
  }
187
230
  parseCustomTypes() {
188
- for (const { tags, description: commentDescription } of (0, comment_parser_1.parse)(this.source, { spacing: "preserve" })) {
231
+ for (const { tags, description: commentDescription, source: blockSource } of (0, comment_parser_1.parse)(this.source, {
232
+ spacing: "preserve",
233
+ })) {
189
234
  let currentEventName;
190
235
  let currentEventType;
191
236
  let currentEventDescription;
@@ -193,8 +238,65 @@ class ComponentParser {
193
238
  let currentTypedefName;
194
239
  let currentTypedefType;
195
240
  let currentTypedefDescription;
196
- let commentDescriptionUsed = false;
197
241
  const typedefProperties = [];
242
+ // Track if we've used the comment block description for any tag in this block
243
+ // Only the first tag (that needs a description) should use the comment block description
244
+ let commentDescriptionUsed = false;
245
+ let isFirstTag = true;
246
+ // Build a map of line numbers to their description content (for lines without tags)
247
+ const lineDescriptions = new Map();
248
+ // Track line numbers that contain tags
249
+ const tagLineNumbers = new Set();
250
+ for (const tagInfo of tags) {
251
+ if (tagInfo.source && tagInfo.source.length > 0) {
252
+ tagLineNumbers.add(tagInfo.source[0].number);
253
+ }
254
+ }
255
+ for (const line of blockSource) {
256
+ // Only track lines that have a description but no tag
257
+ // Also filter out lines that are just "}" (artifact from some comment formats)
258
+ if (!line.tokens.tag && line.tokens.description && line.tokens.description.trim() !== "}") {
259
+ lineDescriptions.set(line.number, line.tokens.description);
260
+ }
261
+ }
262
+ // Helper to get the description from lines preceding a tag
263
+ // Look backwards from the tag until we hit another tag, collecting description lines
264
+ // Stop after finding the first contiguous block of description lines
265
+ const getPrecedingDescription = (tagSource) => {
266
+ if (!tagSource || tagSource.length === 0)
267
+ return undefined;
268
+ const tagLineNumber = tagSource[0].number;
269
+ // Look backwards from the tag line to find the immediately preceding description
270
+ const descLines = [];
271
+ let foundDescriptionBlock = false;
272
+ for (let lineNum = tagLineNumber - 1; lineNum >= 1; lineNum--) {
273
+ // Stop if we hit a tag line
274
+ if (tagLineNumbers.has(lineNum)) {
275
+ break;
276
+ }
277
+ // Check if this line has a description
278
+ const desc = lineDescriptions.get(lineNum);
279
+ if (desc) {
280
+ descLines.unshift(desc); // Add to beginning to maintain order
281
+ foundDescriptionBlock = true;
282
+ }
283
+ else if (foundDescriptionBlock) {
284
+ // We've already found description lines and now hit a non-description line
285
+ // Check if it's blank - if so, continue; if not, stop
286
+ const sourceLine = blockSource.find((l) => l.number === lineNum);
287
+ const isBlank = !sourceLine ||
288
+ (!sourceLine.tokens.tag &&
289
+ (!sourceLine.tokens.description || sourceLine.tokens.description.trim() === ""));
290
+ if (!isBlank) {
291
+ // Non-blank non-description line - stop here
292
+ break;
293
+ }
294
+ // Blank line - continue (blank lines can separate descriptions from tags)
295
+ }
296
+ // If we haven't found any description yet, continue looking backwards
297
+ }
298
+ return descLines.length > 0 ? descLines.join("\n").trim() : undefined;
299
+ };
198
300
  const finalizeEvent = () => {
199
301
  if (currentEventName !== undefined) {
200
302
  let detailType;
@@ -250,37 +352,63 @@ class ComponentParser {
250
352
  currentTypedefDescription = undefined;
251
353
  }
252
354
  };
253
- for (const { tag, type: tagType, name, description, optional, default: defaultValue } of tags) {
355
+ for (const { tag, type: tagType, name, description, optional, default: defaultValue, source: tagSource, } of tags) {
254
356
  const type = this.aliasType(tagType);
357
+ // Get the description from the line immediately before this tag
358
+ const precedingDescription = getPrecedingDescription(tagSource);
255
359
  switch (tag) {
256
360
  case "extends":
257
361
  this.extends = {
258
362
  interface: name,
259
363
  import: type,
260
364
  };
365
+ if (isFirstTag)
366
+ isFirstTag = false;
261
367
  break;
262
368
  case "restProps":
263
369
  this.rest_props = {
264
370
  type: "Element",
265
371
  name: type,
266
372
  };
373
+ if (isFirstTag)
374
+ isFirstTag = false;
267
375
  break;
268
- case "slot":
376
+ case "slot": {
377
+ // Prefer inline description, fall back to preceding line description,
378
+ // then fall back to the comment block description (only for first tag if not already used)
379
+ const inlineSlotDesc = description?.replace(COMMENT_BLOCK_DESCRIPTION_REGEX, "").trim();
380
+ let slotDesc = inlineSlotDesc || precedingDescription;
381
+ if (!slotDesc && isFirstTag && !commentDescriptionUsed && commentDescription) {
382
+ slotDesc = commentDescription;
383
+ commentDescriptionUsed = true;
384
+ }
385
+ if (isFirstTag)
386
+ isFirstTag = false;
269
387
  this.addSlot({
270
388
  slot_name: name,
271
389
  slot_props: type,
272
- slot_description: description ? description : undefined,
390
+ slot_description: slotDesc || undefined,
273
391
  });
274
392
  break;
275
- case "event":
393
+ }
394
+ case "event": {
276
395
  // Finalize any previous event being built
277
396
  finalizeEvent();
278
397
  // Start tracking new event
279
398
  currentEventName = name;
280
399
  currentEventType = type;
281
- // Use the main comment description if available, otherwise use inline description
282
- currentEventDescription = commentDescription?.trim() || description || undefined;
400
+ // Prefer inline description (e.g., "@event {type} name - description"),
401
+ // fall back to preceding line, then fall back to comment block description (only for first tag if not already used)
402
+ const inlineEventDesc = description?.replace(COMMENT_BLOCK_DESCRIPTION_REGEX, "").trim();
403
+ currentEventDescription = inlineEventDesc || precedingDescription;
404
+ if (!currentEventDescription && isFirstTag && !commentDescriptionUsed && commentDescription) {
405
+ currentEventDescription = commentDescription;
406
+ commentDescriptionUsed = true;
407
+ }
408
+ if (isFirstTag)
409
+ isFirstTag = false;
283
410
  break;
411
+ }
284
412
  case "type":
285
413
  // Track the @type tag for the current event
286
414
  if (currentEventName !== undefined) {
@@ -310,22 +438,22 @@ class ComponentParser {
310
438
  // Start tracking new typedef
311
439
  currentTypedefName = name;
312
440
  currentTypedefType = type;
313
- // Use inline description if present, otherwise use comment description only if not already used
314
- const trimmedCommentDesc = commentDescription?.trim();
315
- if (description) {
316
- currentTypedefDescription = description;
317
- }
318
- else if (!commentDescriptionUsed && trimmedCommentDesc && trimmedCommentDesc !== "}") {
319
- currentTypedefDescription = trimmedCommentDesc;
441
+ // Prefer inline description, fall back to preceding line description,
442
+ // then fall back to comment block description (only for first tag if not already used)
443
+ const inlineTypedefDesc = description?.replace(COMMENT_BLOCK_DESCRIPTION_REGEX, "").trim();
444
+ currentTypedefDescription = inlineTypedefDesc || precedingDescription;
445
+ if (!currentTypedefDescription && isFirstTag && !commentDescriptionUsed && commentDescription) {
446
+ currentTypedefDescription = commentDescription;
320
447
  commentDescriptionUsed = true;
321
448
  }
322
- else {
323
- currentTypedefDescription = undefined;
324
- }
449
+ if (isFirstTag)
450
+ isFirstTag = false;
325
451
  break;
326
452
  }
327
453
  case "generics":
328
454
  this.generics = [name, type];
455
+ if (isFirstTag)
456
+ isFirstTag = false;
329
457
  break;
330
458
  }
331
459
  }
@@ -367,7 +495,10 @@ class ComponentParser {
367
495
  buildVariableInfoCache() {
368
496
  if (!this.source)
369
497
  return;
370
- const lines = this.source.split("\n");
498
+ if (!this.sourceLinesCache) {
499
+ this.sourceLinesCache = this.source.split("\n");
500
+ }
501
+ const lines = this.sourceLinesCache;
371
502
  for (let i = 0; i < lines.length; i++) {
372
503
  const line = lines[i].trim();
373
504
  // Match variable declarations
@@ -391,14 +522,12 @@ class ComponentParser {
391
522
  const commentBlock = commentLines.join("\n");
392
523
  // Parse the JSDoc
393
524
  const parsed = (0, comment_parser_1.parse)(commentBlock, { spacing: "preserve" });
394
- if (parsed[0]?.tags) {
395
- const typeTag = parsed[0].tags.find((t) => t.tag === "type");
396
- if (typeTag) {
397
- this.variableInfoCache.set(varName, {
398
- type: this.aliasType(typeTag.type),
399
- description: parsed[0].description || typeTag.description,
400
- });
401
- }
525
+ const { type: typeTag, description } = this.getCommentTags(parsed);
526
+ if (typeTag) {
527
+ this.variableInfoCache.set(varName, {
528
+ type: this.aliasType(typeTag.type),
529
+ description: description || typeTag.description,
530
+ });
402
531
  }
403
532
  break;
404
533
  }
@@ -406,6 +535,20 @@ class ComponentParser {
406
535
  }
407
536
  }
408
537
  }
538
+ static VAR_NAME_REGEX_CACHE = new Map();
539
+ static getVarNameRegexes(varName) {
540
+ let cached = ComponentParser.VAR_NAME_REGEX_CACHE.get(varName);
541
+ if (!cached) {
542
+ const escaped = varName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
543
+ cached = [
544
+ new RegExp(`const\\s+${escaped}\\s*=`),
545
+ new RegExp(`let\\s+${escaped}\\s*=`),
546
+ new RegExp(`function\\s+${escaped}\\s*\\(`),
547
+ ];
548
+ ComponentParser.VAR_NAME_REGEX_CACHE.set(varName, cached);
549
+ }
550
+ return cached;
551
+ }
409
552
  findVariableTypeAndDescription(varName) {
410
553
  const cached = this.variableInfoCache.get(varName);
411
554
  if (cached) {
@@ -414,15 +557,18 @@ class ComponentParser {
414
557
  // Search through the source code directly for JSDoc comments
415
558
  if (!this.source)
416
559
  return null;
417
- // Build a map of variable names to their types by looking at the source
418
- const lines = this.source.split("\n");
560
+ if (!this.sourceLinesCache) {
561
+ this.sourceLinesCache = this.source.split("\n");
562
+ }
563
+ const lines = this.sourceLinesCache;
564
+ const [constRegex, letRegex, funcRegex] = ComponentParser.getVarNameRegexes(varName);
419
565
  for (let i = 0; i < lines.length; i++) {
420
566
  const line = lines[i].trim();
421
567
  // Check if this line declares our variable
422
568
  // Match patterns like: const varName = ..., let varName = ..., function varName
423
- const constMatch = line.match(new RegExp(`const\\s+${varName}\\s*=`));
424
- const letMatch = line.match(new RegExp(`let\\s+${varName}\\s*=`));
425
- const funcMatch = line.match(new RegExp(`function\\s+${varName}\\s*\\(`));
569
+ const constMatch = line.match(constRegex);
570
+ const letMatch = line.match(letRegex);
571
+ const funcMatch = line.match(funcRegex);
426
572
  if (constMatch || letMatch || funcMatch) {
427
573
  // Look backwards for JSDoc comment
428
574
  for (let j = i - 1; j >= 0; j--) {
@@ -441,14 +587,12 @@ class ComponentParser {
441
587
  const commentBlock = commentLines.join("\n");
442
588
  // Parse the JSDoc
443
589
  const parsed = (0, comment_parser_1.parse)(commentBlock, { spacing: "preserve" });
444
- if (parsed[0]?.tags) {
445
- const typeTag = parsed[0].tags.find((t) => t.tag === "type");
446
- if (typeTag) {
447
- return {
448
- type: this.aliasType(typeTag.type),
449
- description: parsed[0].description || typeTag.description,
450
- };
451
- }
590
+ const { type: typeTag, description } = this.getCommentTags(parsed);
591
+ if (typeTag) {
592
+ return {
593
+ type: this.aliasType(typeTag.type),
594
+ description: description || typeTag.description,
595
+ };
452
596
  }
453
597
  break;
454
598
  }
@@ -599,6 +743,7 @@ class ComponentParser {
599
743
  this.bindings.clear();
600
744
  this.contexts.clear();
601
745
  this.variableInfoCache.clear();
746
+ this.sourceLinesCache = undefined;
602
747
  }
603
748
  // Pre-compiled regexes for better performance
604
749
  static SCRIPT_BLOCK_REGEX = /(<script[^>]*>)([\s\S]*?)(<\/script>)/gi;
@@ -632,6 +777,7 @@ class ComponentParser {
632
777
  // The compile result includes the parsed AST
633
778
  this.parsed = compiled.ast || (0, compiler_1.parse)(cleanedSource);
634
779
  this.collectReactiveVars();
780
+ this.sourceLinesCache = this.source.split("\n");
635
781
  this.buildVariableInfoCache();
636
782
  this.parseCustomTypes();
637
783
  if (this.parsed?.module) {
@@ -712,12 +858,11 @@ class ComponentParser {
712
858
  const comment = (0, comment_parser_1.parse)(ComponentParser.formatComment(jsdoc_comment.value), {
713
859
  spacing: "preserve",
714
860
  });
861
+ const { type: typeTag, param: paramTags, returns: returnsTag, additional: additionalTags, description: commentDescription, } = this.getCommentTags(comment);
715
862
  // Extract @type tag
716
- const typeTag = comment[0]?.tags.find((t) => t.tag === "type");
717
863
  if (typeTag)
718
864
  type = this.aliasType(typeTag.type);
719
865
  // Extract @param tags
720
- const paramTags = comment[0]?.tags.filter((t) => t.tag === "param") ?? [];
721
866
  if (paramTags.length > 0) {
722
867
  params = paramTags
723
868
  .filter((tag) => !tag.name.includes(".")) // Exclude nested params like "options.expand"
@@ -729,27 +874,20 @@ class ComponentParser {
729
874
  }));
730
875
  }
731
876
  // Extract @returns/@return tag
732
- const returnsTag = comment[0]?.tags.find((t) => t.tag === "returns" || t.tag === "return");
733
877
  if (returnsTag)
734
878
  returnType = this.aliasType(returnsTag.type);
735
879
  // Build description from comment description and non-param/non-type tags
736
- const commentDescription = ComponentParser.assignValue(comment[0]?.description?.trim());
737
- const additionalTags = comment[0]?.tags.filter((tag) => ![
738
- "type",
739
- "param",
740
- "returns",
741
- "return",
742
- "extends",
743
- "restProps",
744
- "slot",
745
- "event",
746
- "typedef",
747
- ].includes(tag.tag)) ?? [];
748
- if (commentDescription || additionalTags.length > 0) {
749
- description = commentDescription || "";
880
+ const formattedDescription = ComponentParser.assignValue(commentDescription?.trim());
881
+ if (formattedDescription || additionalTags.length > 0) {
882
+ const descriptionParts = [];
883
+ if (formattedDescription) {
884
+ descriptionParts.push(formattedDescription);
885
+ }
750
886
  for (const tag of additionalTags) {
751
- description += `${description ? "\n" : ""}@${tag.tag}${tag.name ? ` ${tag.name}` : ""}${tag.description ? ` ${tag.description}` : ""}`;
887
+ const tagStr = `@${tag.tag}${tag.name ? ` ${tag.name}` : ""}${tag.description ? ` ${tag.description}` : ""}`;
888
+ descriptionParts.push(tagStr);
752
889
  }
890
+ description = descriptionParts.join("\n");
753
891
  }
754
892
  }
755
893
  }
@@ -908,12 +1046,11 @@ class ComponentParser {
908
1046
  const comment = (0, comment_parser_1.parse)(ComponentParser.formatComment(jsdoc_comment.value), {
909
1047
  spacing: "preserve",
910
1048
  });
1049
+ const { type: typeTag, param: paramTags, returns: returnsTag, additional: additional_tags, description: commentDescription, } = this.getCommentTags(comment);
911
1050
  // Extract @type tag
912
- const typeTag = comment[0]?.tags.find((t) => t.tag === "type");
913
1051
  if (typeTag)
914
1052
  type = this.aliasType(typeTag.type);
915
1053
  // Extract @param tags
916
- const paramTags = comment[0]?.tags.filter((t) => t.tag === "param") ?? [];
917
1054
  if (paramTags.length > 0) {
918
1055
  params = paramTags
919
1056
  .filter((tag) => !tag.name.includes(".")) // Exclude nested params like "options.expand"
@@ -925,27 +1062,20 @@ class ComponentParser {
925
1062
  }));
926
1063
  }
927
1064
  // Extract @returns/@return tag
928
- const returnsTag = comment[0]?.tags.find((t) => t.tag === "returns" || t.tag === "return");
929
1065
  if (returnsTag)
930
1066
  returnType = this.aliasType(returnsTag.type);
931
1067
  // Build description from comment description and non-param/non-type tags
932
- const commentDescription = ComponentParser.assignValue(comment[0]?.description?.trim());
933
- const additional_tags = comment[0]?.tags.filter((tag) => ![
934
- "type",
935
- "param",
936
- "returns",
937
- "return",
938
- "extends",
939
- "restProps",
940
- "slot",
941
- "event",
942
- "typedef",
943
- ].includes(tag.tag)) ?? [];
944
- if (commentDescription || additional_tags.length > 0) {
945
- description = commentDescription || "";
1068
+ const formattedDescription = ComponentParser.assignValue(commentDescription?.trim());
1069
+ if (formattedDescription || additional_tags.length > 0) {
1070
+ const descriptionParts = [];
1071
+ if (formattedDescription) {
1072
+ descriptionParts.push(formattedDescription);
1073
+ }
946
1074
  for (const tag of additional_tags) {
947
- description += `${description ? "\n" : ""}@${tag.tag}${tag.name ? ` ${tag.name}` : ""}${tag.description ? ` ${tag.description}` : ""}`;
1075
+ const tagStr = `@${tag.tag}${tag.name ? ` ${tag.name}` : ""}${tag.description ? ` ${tag.description}` : ""}`;
1076
+ descriptionParts.push(tagStr);
948
1077
  }
1078
+ description = descriptionParts.join("\n");
949
1079
  }
950
1080
  }
951
1081
  }
@@ -1027,7 +1157,9 @@ class ComponentParser {
1027
1157
  const existing_event = this.events.get(node.name);
1028
1158
  // Check if this event has a JSDoc description
1029
1159
  const description = this.eventDescriptions.get(node.name);
1030
- const event_description = description?.split("-").pop()?.trim();
1160
+ const event_description = description
1161
+ ? description.substring(description.lastIndexOf("-") + 1).trim()
1162
+ : undefined;
1031
1163
  if (!existing_event) {
1032
1164
  // Add new forwarded event
1033
1165
  this.events.set(node.name, {
@@ -1097,7 +1229,9 @@ class ComponentParser {
1097
1229
  // If event is marked as dispatched but is NOT actually dispatched, convert it to forwarded
1098
1230
  if (event && event.type === "dispatched" && !actuallyDispatchedEvents.has(eventName)) {
1099
1231
  const description = this.eventDescriptions.get(eventName);
1100
- const event_description = description?.split("-").pop()?.trim();
1232
+ const event_description = description
1233
+ ? description.substring(description.lastIndexOf("-") + 1).trim()
1234
+ : undefined;
1101
1235
  const forwardedEvent = {
1102
1236
  type: "forwarded",
1103
1237
  name: eventName,
@@ -1113,56 +1247,62 @@ class ComponentParser {
1113
1247
  this.events.set(eventName, forwardedEvent);
1114
1248
  }
1115
1249
  });
1116
- return {
1117
- props: ComponentParser.mapToArray(this.props).map((prop) => {
1118
- if (this.bindings.has(prop.name)) {
1119
- return {
1120
- ...prop,
1121
- type: "null | " +
1122
- this.bindings
1123
- .get(prop.name)
1124
- ?.elements.sort()
1125
- .map((element) => (0, element_tag_map_1.getElementByTag)(element))
1126
- .join(" | "),
1127
- };
1128
- }
1129
- return prop;
1130
- }),
1131
- moduleExports: ComponentParser.mapToArray(this.moduleExports),
1132
- slots: ComponentParser.mapToArray(this.slots)
1133
- .map((slot) => {
1134
- try {
1135
- const slot_props = JSON.parse(slot.slot_props);
1136
- const new_props = [];
1137
- for (const key of Object.keys(slot_props)) {
1138
- if (slot_props[key].replace && slot_props[key].value !== undefined) {
1139
- slot_props[key].value = this.props.get(slot_props[key].value)?.type;
1140
- }
1141
- if (slot_props[key].value === undefined)
1142
- slot_props[key].value = "any";
1143
- new_props.push(`${key}: ${slot_props[key].value}`);
1250
+ const processedProps = ComponentParser.mapToArray(this.props).map((prop) => {
1251
+ if (this.bindings.has(prop.name)) {
1252
+ const elementTypes = this.bindings
1253
+ .get(prop.name)
1254
+ ?.elements.sort()
1255
+ .map((element) => (0, element_tag_map_1.getElementByTag)(element))
1256
+ .join(" | ");
1257
+ return {
1258
+ ...prop,
1259
+ type: `null | ${elementTypes}`,
1260
+ };
1261
+ }
1262
+ return prop;
1263
+ });
1264
+ const processedSlots = ComponentParser.mapToArray(this.slots)
1265
+ .map((slot) => {
1266
+ try {
1267
+ const slot_props = JSON.parse(slot.slot_props);
1268
+ const new_props = [];
1269
+ for (const key of Object.keys(slot_props)) {
1270
+ if (slot_props[key].replace && slot_props[key].value !== undefined) {
1271
+ slot_props[key].value = this.props.get(slot_props[key].value)?.type;
1144
1272
  }
1145
- const formatted_slot_props = new_props.length === 0 ? "Record<string, never>" : `{ ${new_props.join(", ")} }`;
1146
- return { ...slot, slot_props: formatted_slot_props };
1273
+ if (slot_props[key].value === undefined)
1274
+ slot_props[key].value = "any";
1275
+ new_props.push(`${key}: ${slot_props[key].value}`);
1147
1276
  }
1148
- catch (_e) {
1149
- return slot;
1150
- }
1151
- })
1152
- .sort((a, b) => {
1153
- if (a.name < b.name)
1154
- return -1;
1155
- if (a.name > b.name)
1156
- return 1;
1157
- return 0;
1158
- }),
1159
- events: ComponentParser.mapToArray(this.events),
1160
- typedefs: ComponentParser.mapToArray(this.typedefs),
1277
+ const formatted_slot_props = new_props.length === 0 ? "Record<string, never>" : `{ ${new_props.join(", ")} }`;
1278
+ return { ...slot, slot_props: formatted_slot_props };
1279
+ }
1280
+ catch (_e) {
1281
+ return slot;
1282
+ }
1283
+ })
1284
+ .sort((a, b) => {
1285
+ if (a.name < b.name)
1286
+ return -1;
1287
+ if (a.name > b.name)
1288
+ return 1;
1289
+ return 0;
1290
+ });
1291
+ const moduleExportsArray = ComponentParser.mapToArray(this.moduleExports);
1292
+ const eventsArray = ComponentParser.mapToArray(this.events);
1293
+ const typedefsArray = ComponentParser.mapToArray(this.typedefs);
1294
+ const contextsArray = ComponentParser.mapToArray(this.contexts);
1295
+ return {
1296
+ props: processedProps,
1297
+ moduleExports: moduleExportsArray,
1298
+ slots: processedSlots,
1299
+ events: eventsArray,
1300
+ typedefs: typedefsArray,
1161
1301
  generics: this.generics,
1162
1302
  rest_props: this.rest_props,
1163
1303
  extends: this.extends,
1164
1304
  componentComment: this.componentComment,
1165
- contexts: ComponentParser.mapToArray(this.contexts),
1305
+ contexts: contextsArray,
1166
1306
  };
1167
1307
  }
1168
1308
  }
@@ -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.0",
3
+ "version": "0.25.2",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Generate TypeScript definitions for your Svelte components.",
6
6
  "main": "./lib/index.js",