sveld 0.26.1 → 0.27.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.
@@ -1,2657 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const comment_parser_1 = require("comment-parser");
4
- const compiler_1 = require("svelte/compiler");
5
- const element_tag_map_1 = require("./element-tag-map");
6
- /**
7
- * Regular expression for matching variable declarations.
8
- *
9
- * Matches `const`, `let`, or `function` declarations and captures the variable name.
10
- * Used to find variable declarations when searching for JSDoc comments.
11
- *
12
- * @example
13
- * ```ts
14
- * // Matches:
15
- * // "const count = 0" -> captures "count"
16
- * // "let name = 'test'" -> captures "name"
17
- * // "function foo() {}" -> captures "foo"
18
- * ```
19
- */
20
- const VAR_DECLARATION_REGEX = /(?:const|let|function)\s+(\w+)\s*[=(]/;
21
- /**
22
- * Regular expression for removing leading dash and whitespace from descriptions.
23
- *
24
- * Used to clean up inline descriptions in JSDoc tags that may be prefixed
25
- * with a dash (e.g., `@slot name - description`).
26
- *
27
- * @example
28
- * ```ts
29
- * // Matches and removes:
30
- * // "- description" -> "description"
31
- * // "- padded" -> "padded"
32
- * ```
33
- */
34
- const DESCRIPTION_DASH_PREFIX_REGEX = /^-\s*/;
35
- /** Matches a single word character (letter, digit, or underscore). Used for dotted prop access validation. */
36
- const WORD_CHAR_REGEX = /\w/;
37
- /**
38
- * Extracts description text after the last dash from JSDoc comments.
39
- *
40
- * Used for parsing inline descriptions in JSDoc tags like `@event` where the
41
- * description follows a dash separator.
42
- *
43
- * @param description - The description string that may contain a dash separator
44
- * @returns The description text after the last dash, or the trimmed description if no dash is found
45
- *
46
- * @example
47
- * ```ts
48
- * extractDescriptionAfterDash("@event click - Fires when clicked")
49
- * // Returns: "Fires when clicked"
50
- *
51
- * extractDescriptionAfterDash("Simple description")
52
- * // Returns: "Simple description"
53
- *
54
- * extractDescriptionAfterDash("@event change - Updates value")
55
- * // Returns: "Updates value"
56
- * ```
57
- */
58
- function extractDescriptionAfterDash(description) {
59
- if (!description)
60
- return undefined;
61
- const dashIndex = description.lastIndexOf("-");
62
- return dashIndex >= 0 ? description.substring(dashIndex + 1).trim() : description.trim();
63
- }
64
- /**
65
- * Removes leading dash and whitespace from a description string.
66
- *
67
- * Used for cleaning up inline descriptions in JSDoc tags that may have been
68
- * prefixed with a dash (e.g., `@slot name - description`).
69
- *
70
- * @param description - The description string to clean
71
- * @returns The cleaned description, empty string if result is empty, or undefined if input was undefined
72
- *
73
- * @example
74
- * ```ts
75
- * cleanDescription("- Description text")
76
- * // Returns: "Description text"
77
- *
78
- * cleanDescription(" - Padded description ")
79
- * // Returns: "Padded description"
80
- *
81
- * cleanDescription("-")
82
- * // Returns: ""
83
- *
84
- * cleanDescription(undefined)
85
- * // Returns: undefined
86
- * ```
87
- */
88
- function cleanDescription(description) {
89
- if (description === undefined)
90
- return undefined;
91
- const cleaned = description.replace(DESCRIPTION_DASH_PREFIX_REGEX, "").trim();
92
- return cleaned === "" ? "" : cleaned;
93
- }
94
- /**
95
- * Default slot name constant.
96
- *
97
- * Used to represent the default (unnamed) slot in Svelte components.
98
- * The default slot is accessed without a name attribute.
99
- */
100
- const DEFAULT_SLOT_NAME = null;
101
- /**
102
- * Regular expression for detecting type definition endings.
103
- *
104
- * Matches closing braces that indicate the end of a type definition
105
- * (e.g., `}` or `};`). Used to determine if a typedef should be
106
- * formatted as an interface or type alias.
107
- *
108
- * @example
109
- * ```ts
110
- * // Matches:
111
- * // "{ x: number }" -> interface
112
- * // "{ x: number };" -> interface
113
- * // "string | number" -> type alias
114
- * ```
115
- */
116
- const TYPEDEF_END_REGEX = /(\}|\};)$/;
117
- /**
118
- * Regular expression for splitting context keys into parts.
119
- *
120
- * Splits on dashes, underscores, and whitespace to convert kebab-case,
121
- * snake_case, or space-separated keys into parts for PascalCase conversion.
122
- *
123
- * @example
124
- * ```ts
125
- * // Splits:
126
- * // "simple-modal" -> ["simple", "modal"]
127
- * // "user_settings" -> ["user", "settings"]
128
- * // "my context" -> ["my", "context"]
129
- * ```
130
- */
131
- const CONTEXT_KEY_SPLIT_REGEX = /[-_.\s]+/;
132
- /**
133
- * Regular expression for matching component comment markers.
134
- *
135
- * Matches HTML comments that start with `@component` in the template.
136
- * Used to extract component-level descriptions.
137
- *
138
- * @example
139
- * ```ts
140
- * // Matches:
141
- * // "<!-- @component This is a button component -->"
142
- * ```
143
- */
144
- const COMPONENT_COMMENT_REGEX = /^@component/;
145
- /**
146
- * Regular expression for matching carriage return characters.
147
- *
148
- * Global regex for removing `\r` characters from strings.
149
- * Used to normalize line endings when processing component comments.
150
- */
151
- const CARRIAGE_RETURN_REGEX = /\r/g;
152
- /**
153
- * Regular expression for matching newline and carriage return characters.
154
- *
155
- * Global regex for matching all newline variations (`\n`, `\r\n`, `\r`).
156
- * Used to normalize or replace newlines in source code strings.
157
- *
158
- * @example
159
- * ```ts
160
- * // Matches:
161
- * // "\n" -> newline
162
- * // "\r\n" -> Windows line ending
163
- * // "\r" -> carriage return
164
- * ```
165
- */
166
- const NEWLINE_CR_REGEX = /[\r\n]+/g;
167
- class ComponentParser {
168
- /** Parser configuration options (e.g., verbose logging) */
169
- options;
170
- /** Raw source code of the Svelte component being parsed */
171
- source;
172
- /** Compiled Svelte code containing extracted variables and AST */
173
- compiled;
174
- /** Parsed abstract syntax tree from the Svelte compiler */
175
- parsed;
176
- /** Rest props configuration (e.g., `$$restProps`) if present in component */
177
- rest_props;
178
- /** Component extension information (e.g., `extends` attribute) */
179
- extends;
180
- /** Component-level description extracted from `@component` HTML comment */
181
- componentComment;
182
- /** Set of reactive variable names found in the component */
183
- reactive_vars = new Set();
184
- /** Set of all variable declarations found in the component script */
185
- vars = new Set();
186
- /** Map of component props keyed by prop name */
187
- props = new Map();
188
- /** Map of module exports (functions/variables exported from script) keyed by name */
189
- moduleExports = new Map();
190
- /** Map of component slots keyed by slot name (null for default slot) */
191
- slots = new Map();
192
- /** Map of component events (dispatched events) keyed by event name */
193
- events = new Map();
194
- /** Map of event descriptions extracted from JSDoc comments keyed by event name */
195
- eventDescriptions = new Map();
196
- /** Map of forwarded events (events forwarded from child components) keyed by event name */
197
- forwardedEvents = new Map();
198
- /** Map of type definitions (typedefs) extracted from JSDoc comments keyed by type name */
199
- typedefs = new Map();
200
- /** Component generic type parameters (null if no generics) */
201
- generics = null;
202
- /** Map of prop bindings (e.g., `bind:value`) keyed by prop name */
203
- bindings = new Map();
204
- /** Map of component contexts (created with `setContext`) keyed by context name */
205
- contexts = new Map();
206
- /** Cache for variable type and description information to avoid redundant lookups */
207
- variableInfoCache = new Map();
208
- /** Cached array of source code lines split by newline for efficient line-based operations */
209
- sourceLinesCache;
210
- constructor(options) {
211
- this.options = options;
212
- }
213
- static mapToArray(map) {
214
- return Array.from(map, ([_key, value]) => value);
215
- }
216
- static assignValue(value) {
217
- return value === undefined || value === "" ? undefined : value;
218
- }
219
- static formatComment(comment) {
220
- let formatted_comment = comment;
221
- if (!formatted_comment.startsWith("/*")) {
222
- formatted_comment = `/*${formatted_comment}`;
223
- }
224
- if (!formatted_comment.endsWith("*/")) {
225
- formatted_comment += "*/";
226
- }
227
- return formatted_comment;
228
- }
229
- /**
230
- * Extracts and categorizes JSDoc tags from a parsed comment.
231
- *
232
- * Separates tags into type, param, returns, and additional categories while
233
- * excluding tags that are handled separately (extends, restProps, slot, event, typedef).
234
- *
235
- * @param parsed - The parsed comment result from comment-parser
236
- * @returns An object containing categorized tags and the comment description
237
- *
238
- * @example
239
- * ```ts
240
- * // Input: Parsed comment with tags
241
- * // Output:
242
- * {
243
- * type: { tag: "type", type: "string" },
244
- * param: [
245
- * { tag: "param", name: "value", type: "string" }
246
- * ],
247
- * returns: { tag: "returns", type: "void" },
248
- * additional: [{ tag: "since", name: "1.0.0" }],
249
- * description: "Main description text"
250
- * }
251
- * ```
252
- */
253
- getCommentTags(parsed) {
254
- const tags = parsed[0]?.tags ?? [];
255
- const excludedTags = new Set([
256
- "type",
257
- "param",
258
- "returns",
259
- "return",
260
- "extends",
261
- "extendProps",
262
- "restProps",
263
- "slot",
264
- "event",
265
- "typedef",
266
- ]);
267
- let typeTag;
268
- const paramTags = [];
269
- let returnsTag;
270
- const additionalTags = [];
271
- for (const tag of tags) {
272
- if (tag.tag === "type") {
273
- typeTag = tag;
274
- }
275
- else if (tag.tag === "param") {
276
- paramTags.push(tag);
277
- }
278
- else if (tag.tag === "returns" || tag.tag === "return") {
279
- returnsTag = tag;
280
- }
281
- else if (!excludedTags.has(tag.tag)) {
282
- additionalTags.push(tag);
283
- }
284
- }
285
- return {
286
- type: typeTag,
287
- param: paramTags,
288
- returns: returnsTag,
289
- additional: additionalTags,
290
- description: parsed[0]?.description,
291
- };
292
- }
293
- /**
294
- * Finds the last comment from an array of leading comments.
295
- *
296
- * TypeScript directives are stripped before parsing, so we can safely take
297
- * the last comment as it will be the JSDoc comment if present.
298
- *
299
- * @param leadingComments - Array of comment nodes from the AST
300
- * @returns The last comment's value if found, undefined otherwise
301
- *
302
- * @example
303
- * ```ts
304
- * // Given leadingComments with multiple comments:
305
- * // [/* regular comment *\/, /** JSDoc comment *\/]
306
- * // Returns: { value: " JSDoc comment " }
307
- *
308
- * // If no comments:
309
- * // Returns: undefined
310
- * ```
311
- */
312
- static findJSDocComment(leadingComments) {
313
- if (!leadingComments || leadingComments.length === 0)
314
- return undefined;
315
- const comment = leadingComments[leadingComments.length - 1];
316
- return comment && typeof comment === "object" && "value" in comment ? comment : undefined;
317
- }
318
- /**
319
- * Processes JSDoc comments from leadingComments and extracts structured information.
320
- *
321
- * Parses JSDoc comments to extract type information, parameters, return types,
322
- * and descriptions. Handles both inline and block-level descriptions.
323
- *
324
- * @param leadingComments - Array of comment nodes from the AST
325
- * @returns Structured JSDoc information or undefined if no JSDoc comment is found
326
- *
327
- * @example
328
- * ```ts
329
- * // Input JSDoc:
330
- * /**
331
- * * @type {string}
332
- * * @param {number} x - The x coordinate
333
- * * @param {number} y - The y coordinate
334
- * * @returns {void}
335
- * * Description text
336
- * *\/
337
- *
338
- * // Output:
339
- * {
340
- * type: "string",
341
- * params: [
342
- * { name: "x", type: "number", description: "The x coordinate", optional: false },
343
- * { name: "y", type: "number", description: "The y coordinate", optional: false }
344
- * ],
345
- * returnType: "void",
346
- * description: "Description text"
347
- * }
348
- * ```
349
- */
350
- processJSDocComment(leadingComments) {
351
- if (!leadingComments)
352
- return undefined;
353
- const jsdoc_comment = ComponentParser.findJSDocComment(leadingComments);
354
- if (!jsdoc_comment)
355
- return undefined;
356
- const comment = (0, comment_parser_1.parse)(ComponentParser.formatComment(jsdoc_comment.value), {
357
- spacing: "preserve",
358
- });
359
- const { type: typeTag, param: paramTags, returns: returnsTag, additional: additionalTags, description: commentDescription, } = this.getCommentTags(comment);
360
- let type;
361
- let params;
362
- let returnType;
363
- let description;
364
- // `@type` tag overrides any inferred type from the initializer
365
- if (typeTag)
366
- type = this.aliasType(typeTag.type);
367
- /**
368
- * Extract `@param` tags to document function parameters.
369
- * Nested params like "options.expand" are excluded as they represent
370
- * object property access rather than direct parameters.
371
- */
372
- if (paramTags.length > 0) {
373
- params = paramTags
374
- .filter((tag) => !tag.name.includes("."))
375
- .map((tag) => ({
376
- name: tag.name,
377
- type: this.aliasType(tag.type),
378
- description: cleanDescription(tag.description),
379
- optional: tag.optional || false,
380
- }));
381
- }
382
- if (returnsTag)
383
- returnType = this.aliasType(returnsTag.type);
384
- /**
385
- * Build description from comment description and non-param/non-type tags.
386
- * Additional tags (like `@since`, `@deprecated`) are included in the description
387
- * as formatted strings to preserve all metadata.
388
- */
389
- const formattedDescription = ComponentParser.assignValue(commentDescription?.trim());
390
- if (formattedDescription || additionalTags.length > 0) {
391
- const descriptionParts = [];
392
- if (formattedDescription) {
393
- descriptionParts.push(formattedDescription);
394
- }
395
- for (const tag of additionalTags) {
396
- const tagStr = `@${tag.tag}${tag.name ? ` ${tag.name}` : ""}${tag.description ? ` ${tag.description}` : ""}`;
397
- descriptionParts.push(tagStr);
398
- }
399
- description = descriptionParts.join("\n");
400
- }
401
- return { type, params, returnType, description };
402
- }
403
- /**
404
- * Checks if a MemberExpression represents a well-known numeric constant.
405
- *
406
- * Identifies constants from the Number and Math objects that should be
407
- * typed as `number` rather than their literal values.
408
- *
409
- * @param memberExpr - The AST node to check
410
- * @returns True if the expression is a recognized numeric constant
411
- *
412
- * @example
413
- * ```ts
414
- * // Recognized constants:
415
- * Number.POSITIVE_INFINITY // true
416
- * Number.MAX_VALUE // true
417
- * Math.PI // true
418
- * Math.E // true
419
- *
420
- * // Not recognized:
421
- * Custom.CONSTANT // false
422
- * Number.UNKNOWN // false
423
- * ```
424
- */
425
- isNumericConstant(memberExpr) {
426
- if (!memberExpr || typeof memberExpr !== "object" || !("type" in memberExpr))
427
- return false;
428
- if (memberExpr.type !== "MemberExpression")
429
- return false;
430
- const expr = memberExpr;
431
- const objectName = expr.object && "name" in expr.object ? expr.object.name : undefined;
432
- const propertyName = expr.property && "name" in expr.property ? expr.property.name : undefined;
433
- if (!objectName || !propertyName)
434
- return false;
435
- if (objectName === "Number") {
436
- return [
437
- "POSITIVE_INFINITY",
438
- "NEGATIVE_INFINITY",
439
- "MAX_VALUE",
440
- "MIN_VALUE",
441
- "MAX_SAFE_INTEGER",
442
- "MIN_SAFE_INTEGER",
443
- "EPSILON",
444
- "NaN",
445
- ].includes(propertyName);
446
- }
447
- if (objectName === "Math") {
448
- return ["PI", "E", "LN2", "LN10", "LOG2E", "LOG10E", "SQRT2", "SQRT1_2"].includes(propertyName);
449
- }
450
- return false;
451
- }
452
- /**
453
- * Extracts source code at the given position range.
454
- *
455
- * @param start - Start position in the source
456
- * @param end - End position in the source
457
- * @returns The source code substring, or undefined if source is not available
458
- */
459
- sourceAtPos(start, end) {
460
- return this.source?.slice(start, end);
461
- }
462
- /**
463
- * Processes an initializer expression to extract its value, type, and function status.
464
- *
465
- * Handles various expression types including object literals, arrays, binary expressions,
466
- * arrow functions, unary expressions, identifiers, member expressions, template literals,
467
- * and primitive literals. Extracts the source code representation and infers types
468
- * where possible.
469
- *
470
- * @param init - The initializer AST node
471
- * @returns An object containing the value (source code), inferred type, and whether it's a function
472
- *
473
- * @example
474
- * ```ts
475
- * // ObjectExpression: { x: 1, y: 2 }
476
- * // Returns: { value: "{ x: 1, y: 2 }", type: "{ x: 1, y: 2 }", isFunction: false }
477
- *
478
- * // ArrowFunctionExpression: () => {}
479
- * // Returns: { value: undefined, type: "(...args: any[]) => any", isFunction: true }
480
- *
481
- * // Literal: "hello"
482
- * // Returns: { value: '"hello"', type: "string", isFunction: false }
483
- *
484
- * // BinaryExpression: "a" + "b"
485
- * // Returns: { value: '"a" + "b"', type: "string", isFunction: false }
486
- *
487
- * // MemberExpression: Math.PI
488
- * // Returns: { value: "Math.PI", type: "number" (if numeric constant), isFunction: false }
489
- * ```
490
- */
491
- processInitializer(init) {
492
- let value;
493
- let type;
494
- let isFunction = false;
495
- if (!init || typeof init !== "object" || !("type" in init)) {
496
- return { value, type, isFunction };
497
- }
498
- if (init.type === "ObjectExpression" ||
499
- init.type === "BinaryExpression" ||
500
- init.type === "ArrayExpression" ||
501
- init.type === "ArrowFunctionExpression") {
502
- const expr = init;
503
- if ("start" in expr && "end" in expr && typeof expr.start === "number" && typeof expr.end === "number") {
504
- value = this.sourceAtPos(expr.start, expr.end)?.replace(NEWLINE_CR_REGEX, " ");
505
- }
506
- type = value;
507
- isFunction = init.type === "ArrowFunctionExpression";
508
- if (init.type === "BinaryExpression") {
509
- const binExpr = init;
510
- if (binExpr.left &&
511
- typeof binExpr.left === "object" &&
512
- "type" in binExpr.left &&
513
- binExpr.left.type === "Literal" &&
514
- "value" in binExpr.left &&
515
- typeof binExpr.left.value === "string") {
516
- type = "string";
517
- }
518
- }
519
- if (init.type === "ArrowFunctionExpression") {
520
- type = "(...args: any[]) => any";
521
- value = undefined;
522
- }
523
- }
524
- else if (init.type === "UnaryExpression") {
525
- const unaryExpr = init;
526
- if ("start" in unaryExpr &&
527
- "end" in unaryExpr &&
528
- typeof unaryExpr.start === "number" &&
529
- typeof unaryExpr.end === "number") {
530
- value = this.sourceAtPos(unaryExpr.start, unaryExpr.end);
531
- }
532
- if (unaryExpr.argument) {
533
- // If the argument is another UnaryExpression, recursively resolve the type
534
- if (typeof unaryExpr.argument === "object" &&
535
- "type" in unaryExpr.argument &&
536
- unaryExpr.argument.type === "UnaryExpression") {
537
- const nestedResult = this.processInitializer(unaryExpr.argument);
538
- type = nestedResult.type;
539
- }
540
- else if (typeof unaryExpr.argument === "object" && "value" in unaryExpr.argument) {
541
- // Direct literal argument
542
- type = typeof unaryExpr.argument.value;
543
- }
544
- }
545
- }
546
- else if (init.type === "NewExpression") {
547
- const newExpr = init;
548
- if ("start" in newExpr &&
549
- "end" in newExpr &&
550
- typeof newExpr.start === "number" &&
551
- typeof newExpr.end === "number") {
552
- value = this.sourceAtPos(newExpr.start, newExpr.end);
553
- }
554
- // Infer type from callee if it's an Identifier (e.g., new Date() -> Date)
555
- if (newExpr.callee &&
556
- typeof newExpr.callee === "object" &&
557
- "type" in newExpr.callee &&
558
- newExpr.callee.type === "Identifier") {
559
- const calleeName = newExpr.callee.name;
560
- // Common built-in constructors
561
- if (calleeName === "Date") {
562
- type = "Date";
563
- }
564
- else if (calleeName === "Map") {
565
- type = "Map<any, any>";
566
- }
567
- else if (calleeName === "Set") {
568
- type = "Set<any>";
569
- }
570
- else if (calleeName === "WeakMap") {
571
- type = "WeakMap<object, any>";
572
- }
573
- else if (calleeName === "WeakSet") {
574
- type = "WeakSet<object>";
575
- }
576
- else if (calleeName === "Array") {
577
- type = "any[]";
578
- }
579
- else if (calleeName === "RegExp" || calleeName === "Regexp") {
580
- type = "RegExp";
581
- }
582
- else if (calleeName === "Error") {
583
- type = "Error";
584
- }
585
- else {
586
- // For other constructors, use the constructor name as the type
587
- type = calleeName;
588
- }
589
- }
590
- }
591
- else if (init.type === "CallExpression") {
592
- const callExpr = init;
593
- if ("start" in callExpr &&
594
- "end" in callExpr &&
595
- typeof callExpr.start === "number" &&
596
- typeof callExpr.end === "number") {
597
- value = this.sourceAtPos(callExpr.start, callExpr.end);
598
- }
599
- }
600
- else if (init.type === "Identifier") {
601
- const ident = init;
602
- if ("start" in ident && "end" in ident && typeof ident.start === "number" && typeof ident.end === "number") {
603
- value = this.sourceAtPos(ident.start, ident.end);
604
- }
605
- }
606
- else if (init.type === "MemberExpression") {
607
- const memberExpr = init;
608
- if ("start" in memberExpr &&
609
- "end" in memberExpr &&
610
- typeof memberExpr.start === "number" &&
611
- typeof memberExpr.end === "number") {
612
- value = this.sourceAtPos(memberExpr.start, memberExpr.end);
613
- }
614
- if (this.isNumericConstant(init)) {
615
- type = "number";
616
- }
617
- }
618
- else if (init.type === "TemplateLiteral") {
619
- const template = init;
620
- if ("start" in template &&
621
- "end" in template &&
622
- typeof template.start === "number" &&
623
- typeof template.end === "number") {
624
- value = this.sourceAtPos(template.start, template.end);
625
- }
626
- type = "string";
627
- }
628
- else if ("raw" in init && typeof init.raw === "string") {
629
- value = init.raw;
630
- if ("value" in init) {
631
- type = init.value == null ? undefined : typeof init.value;
632
- }
633
- }
634
- return { value, type, isFunction };
635
- }
636
- /**
637
- * Collects reactive variables from the compiled Svelte component.
638
- *
639
- * Reactive variables are those that are both reassigned and writable,
640
- * indicating they can change and trigger reactivity in Svelte.
641
- */
642
- collectReactiveVars() {
643
- const reactiveVars = this.compiled?.vars.filter(({ reassigned, writable }) => reassigned && writable) ?? [];
644
- for (const { name } of reactiveVars) {
645
- this.reactive_vars.add(name);
646
- }
647
- }
648
- /**
649
- * Adds or merges a component prop to the props map.
650
- *
651
- * If a prop with the same name already exists, the new data is merged
652
- * with the existing prop, with new values taking precedence.
653
- *
654
- * @param prop_name - The name of the prop
655
- * @param data - The prop data to add or merge
656
- *
657
- * @example
658
- * ```ts
659
- * // First call:
660
- * addProp("count", { name: "count", type: "number", kind: "let" })
661
- * // Props map: { "count" => { name: "count", type: "number", kind: "let" } }
662
- *
663
- * // Second call (merge):
664
- * addProp("count", { description: "The count value" })
665
- * // Props map: { "count" => { name: "count", type: "number", kind: "let", description: "The count value" } }
666
- * ```
667
- */
668
- addProp(prop_name, data) {
669
- if (ComponentParser.assignValue(prop_name) === undefined)
670
- return;
671
- if (this.props.has(prop_name)) {
672
- const existing_slot = this.props.get(prop_name);
673
- this.props.set(prop_name, {
674
- ...existing_slot,
675
- ...data,
676
- });
677
- }
678
- else {
679
- this.props.set(prop_name, data);
680
- }
681
- }
682
- /**
683
- * Adds or merges a module export to the moduleExports map.
684
- *
685
- * Similar to {@link addProp}, but for exported values from the module script.
686
- * If an export with the same name already exists, the new data is merged
687
- * with the existing export.
688
- *
689
- * @param prop_name - The name of the exported value
690
- * @param data - The export data to add or merge
691
- *
692
- * @example
693
- * ```ts
694
- * // For: export const API_URL = "https://api.example.com"
695
- * addModuleExport("API_URL", {
696
- * name: "API_URL",
697
- * kind: "const",
698
- * type: "string",
699
- * value: '"https://api.example.com"'
700
- * })
701
- * ```
702
- */
703
- addModuleExport(prop_name, data) {
704
- if (ComponentParser.assignValue(prop_name) === undefined)
705
- return;
706
- if (this.moduleExports.has(prop_name)) {
707
- const existing_slot = this.moduleExports.get(prop_name);
708
- this.moduleExports.set(prop_name, {
709
- ...existing_slot,
710
- ...data,
711
- });
712
- }
713
- else {
714
- this.moduleExports.set(prop_name, data);
715
- }
716
- }
717
- /**
718
- * Normalizes type strings by aliasing common patterns.
719
- *
720
- * Converts `*` to `any` (common JSDoc wildcard) and trims whitespace
721
- * from type annotations.
722
- *
723
- * @param type - The type string to normalize
724
- * @returns The normalized type string
725
- *
726
- * @example
727
- * ```ts
728
- * aliasType("*") // Returns: "any"
729
- * aliasType(" string ") // Returns: "string"
730
- * aliasType("number") // Returns: "number"
731
- * ```
732
- */
733
- aliasType(type) {
734
- if (type === "*")
735
- return "any";
736
- return type.trim();
737
- }
738
- /**
739
- * Extracts a property's type from an object type string.
740
- *
741
- * Parses type strings like `{ value: string; other: number }` and returns
742
- * the type for the requested property name. Handles nested braces, generics,
743
- * and optional properties.
744
- *
745
- * @returns The property type string, or undefined if not found
746
- */
747
- extractPropertyType(typeStr, propName) {
748
- const trimmed = typeStr.trim();
749
- if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
750
- return undefined;
751
- const inner = trimmed.slice(1, -1);
752
- const segments = [];
753
- let depth = 0;
754
- let current = "";
755
- for (const char of inner) {
756
- if (char === "{" || char === "<" || char === "(" || char === "[") {
757
- depth++;
758
- current += char;
759
- }
760
- else if (char === "}" || char === ">" || char === ")" || char === "]") {
761
- depth--;
762
- current += char;
763
- }
764
- else if ((char === ";" || char === ",") && depth === 0) {
765
- segments.push(current.trim());
766
- current = "";
767
- }
768
- else {
769
- current += char;
770
- }
771
- }
772
- if (current.trim())
773
- segments.push(current.trim());
774
- for (const segment of segments) {
775
- if (!segment.startsWith(propName))
776
- continue;
777
- const afterName = segment.slice(propName.length);
778
- if (afterName.length > 0 && WORD_CHAR_REGEX.test(afterName[0]))
779
- continue;
780
- let rest = afterName.trimStart();
781
- if (rest.startsWith("?"))
782
- rest = rest.slice(1).trimStart();
783
- if (rest.startsWith(":")) {
784
- return rest.slice(1).trim();
785
- }
786
- }
787
- return undefined;
788
- }
789
- /**
790
- * Resolves the type of a MemberExpression (e.g., `obj.value`) by looking up
791
- * the object's type annotation and extracting the property type.
792
- *
793
- * @returns The resolved type string, or undefined if it cannot be resolved
794
- */
795
- resolveMemberExpressionType(expr) {
796
- const memberExpr = expr;
797
- if (memberExpr.computed || memberExpr.object?.type !== "Identifier" || memberExpr.property?.type !== "Identifier") {
798
- return undefined;
799
- }
800
- const objName = memberExpr.object.name;
801
- const propName = memberExpr.property.name;
802
- if (!objName || !propName)
803
- return undefined;
804
- const objType = this.props.get(objName)?.type ?? this.findVariableTypeAndDescription(objName)?.type;
805
- if (!objType)
806
- return undefined;
807
- return this.extractPropertyType(objType, propName);
808
- }
809
- /**
810
- * Adds or merges a slot definition to the slots map.
811
- *
812
- * Handles both named slots and the default slot. If a slot with the same
813
- * name already exists, merges the data with existing values taking precedence
814
- * where appropriate.
815
- *
816
- * @param slot_name - Optional slot name (undefined/empty = default slot)
817
- * @param slot_props - Optional slot props type definition
818
- * @param slot_fallback - Optional fallback content for the slot
819
- * @param slot_description - Optional description for the slot
820
- *
821
- * @example
822
- * ```ts
823
- * // Default slot:
824
- * addSlot({ slot_name: undefined, slot_props: "{ children: string }" })
825
- *
826
- * // Named slot:
827
- * addSlot({
828
- * slot_name: "header",
829
- * slot_props: "{ title: string }",
830
- * slot_description: "Header slot with title prop"
831
- * })
832
- *
833
- * // Slot with fallback:
834
- * addSlot({
835
- * slot_name: "footer",
836
- * slot_fallback: "<p>Default footer</p>"
837
- * })
838
- * ```
839
- */
840
- addSlot({ slot_name, slot_props, slot_fallback, slot_description, }) {
841
- const default_slot = slot_name === undefined || slot_name === "";
842
- const name = default_slot ? DEFAULT_SLOT_NAME : (slot_name ?? "");
843
- const fallback = ComponentParser.assignValue(slot_fallback);
844
- const props = ComponentParser.assignValue(slot_props);
845
- const description = extractDescriptionAfterDash(slot_description);
846
- if (this.slots.has(name)) {
847
- const existing_slot = this.slots.get(name);
848
- if (existing_slot) {
849
- this.slots.set(name, {
850
- ...existing_slot,
851
- default: existing_slot.default ?? default_slot,
852
- fallback,
853
- slot_props: existing_slot.slot_props === undefined ? props : existing_slot.slot_props,
854
- description: existing_slot.description || description,
855
- });
856
- }
857
- }
858
- else {
859
- this.slots.set(name, {
860
- name,
861
- default: default_slot,
862
- fallback,
863
- slot_props,
864
- description,
865
- });
866
- }
867
- }
868
- /**
869
- * Adds or merges a dispatched event to the events map.
870
- *
871
- * Handles event detail type inference: if no argument is provided to the
872
- * dispatcher and no `@event` tag specifies a detail type, the detail defaults
873
- * to `null`. Otherwise, uses the provided detail type.
874
- *
875
- * @param name - The event name
876
- * @param detail - The event detail type string
877
- * @param has_argument - Whether the dispatcher call includes a detail argument
878
- * @param description - Optional event description
879
- *
880
- * @example
881
- * ```ts
882
- * // Event without detail:
883
- * // createEventDispatcher()("click")
884
- * addDispatchedEvent({
885
- * name: "click",
886
- * detail: "",
887
- * has_argument: false,
888
- * description: "Fires on click"
889
- * })
890
- * // Result: { type: "dispatched", name: "click", detail: "null" }
891
- *
892
- * // Event with detail:
893
- * // dispatch("change", { value: 42 })
894
- * addDispatchedEvent({
895
- * name: "change",
896
- * detail: "{ value: number }",
897
- * has_argument: true,
898
- * description: "Fires when value changes"
899
- * })
900
- * // Result: { type: "dispatched", name: "change", detail: "{ value: number }" }
901
- * ```
902
- */
903
- addDispatchedEvent({ name, detail, has_argument, description, }) {
904
- if (name === undefined)
905
- return;
906
- /**
907
- * `e.detail` should be `null` if the dispatcher is not provided a second
908
- * argument and if `@event` is not specified. This matches Svelte's behavior
909
- * where events without detail have `detail: null`.
910
- */
911
- const default_detail = !has_argument && !detail ? "null" : ComponentParser.assignValue(detail);
912
- const event_description = extractDescriptionAfterDash(description);
913
- if (this.events.has(name)) {
914
- const existing_event = this.events.get(name);
915
- this.events.set(name, {
916
- ...existing_event,
917
- detail: existing_event.detail === undefined ? default_detail : existing_event.detail,
918
- description: existing_event.description || event_description,
919
- });
920
- }
921
- else {
922
- this.events.set(name, {
923
- type: "dispatched",
924
- name,
925
- detail: default_detail,
926
- description: event_description,
927
- });
928
- }
929
- }
930
- /**
931
- * Parses custom types, events, slots, and other JSDoc annotations from component comments.
932
- *
933
- * Scans the entire source for JSDoc comment blocks and extracts structured information
934
- * about events, typedefs, slots, extends, restProps, and generics. Handles complex
935
- * description extraction logic that supports both inline descriptions and preceding
936
- * line descriptions.
937
- *
938
- * @example
939
- * ```ts
940
- * // Parses comments like:
941
- * /**
942
- * * @event {CustomEvent} change - Fires when value changes
943
- * * @property {string} value - The new value
944
- * * @property {number} timestamp - When it changed
945
- * *\/
946
- *
947
- * // Or:
948
- * /**
949
- * * Description for the event
950
- * * @event change
951
- * *\/
952
- * ```
953
- */
954
- parseCustomTypes() {
955
- if (!this.source)
956
- return;
957
- for (const { tags, description: commentDescription, source: blockSource } of (0, comment_parser_1.parse)(this.source, {
958
- spacing: "preserve",
959
- })) {
960
- let currentEventName;
961
- let currentEventType;
962
- let currentEventDescription;
963
- const eventProperties = [];
964
- let currentTypedefName;
965
- let currentTypedefType;
966
- let currentTypedefDescription;
967
- const typedefProperties = [];
968
- /**
969
- * Track if we've used the comment block description for any tag in this block.
970
- * Only the first tag (that needs a description) should use the comment block
971
- * description to avoid duplicating it across multiple tags.
972
- */
973
- let commentDescriptionUsed = false;
974
- let isFirstTag = true;
975
- /**
976
- * Build a map of line numbers to their description content (for lines without tags).
977
- * This allows us to find descriptions that appear on lines preceding tags.
978
- */
979
- const lineDescriptions = new Map();
980
- /**
981
- * Track line numbers that contain tags so we can distinguish between
982
- * description lines and tag lines when looking backwards.
983
- */
984
- const tagLineNumbers = new Set();
985
- for (const tagInfo of tags) {
986
- if (tagInfo.source && tagInfo.source.length > 0) {
987
- tagLineNumbers.add(tagInfo.source[0].number);
988
- }
989
- }
990
- for (const line of blockSource) {
991
- /**
992
- * Only track lines that have a description but no tag.
993
- * Also filter out lines that are just "}" (artifact from some comment formats
994
- * that may include closing braces in the description).
995
- */
996
- if (!line.tokens.tag && line.tokens.description && line.tokens.description.trim() !== "}") {
997
- lineDescriptions.set(line.number, line.tokens.description);
998
- }
999
- }
1000
- /**
1001
- * Helper to get the description from lines preceding a tag.
1002
- *
1003
- * Looks backwards from the tag until hitting another tag, collecting description
1004
- * lines. Stops after finding the first contiguous block of description lines,
1005
- * allowing blank lines to separate descriptions from tags.
1006
- *
1007
- * @param tagSource - The source information for the tag
1008
- * @returns The concatenated description from preceding lines, or undefined
1009
- *
1010
- * @example
1011
- * ```ts
1012
- * // Comment structure:
1013
- * /**
1014
- * * This is a description
1015
- * * that spans multiple lines
1016
- * *
1017
- * * @event change
1018
- * *\/
1019
- * // getPrecedingDescription would return: "This is a description\nthat spans multiple lines"
1020
- * ```
1021
- */
1022
- const getPrecedingDescription = (tagSource) => {
1023
- if (!tagSource || tagSource.length === 0)
1024
- return undefined;
1025
- const tagLineNumber = tagSource[0].number;
1026
- /**
1027
- * Look backwards from the tag line to find the immediately preceding description.
1028
- * Collects description lines in order, stopping when we hit another tag or a
1029
- * non-blank non-description line.
1030
- */
1031
- const descLines = [];
1032
- let foundDescriptionBlock = false;
1033
- for (let lineNum = tagLineNumber - 1; lineNum >= 1; lineNum--) {
1034
- /**
1035
- * Stop if we hit a tag line - descriptions belong to the nearest preceding tag.
1036
- */
1037
- if (tagLineNumbers.has(lineNum)) {
1038
- break;
1039
- }
1040
- /**
1041
- * Check if this line has a description and add it to our collection.
1042
- * We unshift to maintain forward order when we reverse through the lines.
1043
- */
1044
- const desc = lineDescriptions.get(lineNum);
1045
- if (desc) {
1046
- descLines.unshift(desc);
1047
- foundDescriptionBlock = true;
1048
- }
1049
- else if (foundDescriptionBlock) {
1050
- /**
1051
- * We've already found description lines and now hit a non-description line.
1052
- * Check if it's blank - if so, continue (blank lines can separate descriptions
1053
- * from tags); if not, stop here as we've reached the end of the description block.
1054
- */
1055
- const sourceLine = blockSource.find((l) => l.number === lineNum);
1056
- const isBlank = !sourceLine ||
1057
- (!sourceLine.tokens.tag &&
1058
- (!sourceLine.tokens.description || sourceLine.tokens.description.trim() === ""));
1059
- if (!isBlank) {
1060
- /**
1061
- * Non-blank non-description line - stop here as we've reached content
1062
- * that's not part of the description.
1063
- */
1064
- break;
1065
- }
1066
- /**
1067
- * Blank line - continue (blank lines can separate descriptions from tags
1068
- * and are allowed in the description block).
1069
- */
1070
- }
1071
- /**
1072
- * If we haven't found any description yet, continue looking backwards
1073
- * to find the description block.
1074
- */
1075
- }
1076
- return descLines.length > 0 ? descLines.join("\n").trim() : undefined;
1077
- };
1078
- /**
1079
- * Finalizes the current event being built and adds it to the events map.
1080
- *
1081
- * If the event has properties defined via `@property` tags, builds a detail type
1082
- * from those properties. Otherwise, uses the type from `@type` tag or empty string.
1083
- * Stores the event description for later use in forwarded event detection.
1084
- *
1085
- * @example
1086
- * ```ts
1087
- * // After processing:
1088
- * // @event change
1089
- * // @type {CustomEvent}
1090
- * // @property {string} value
1091
- * // finalizeEvent() creates:
1092
- * // { type: "dispatched", name: "change", detail: "{ value: string }" }
1093
- * ```
1094
- */
1095
- const finalizeEvent = () => {
1096
- if (currentEventName !== undefined) {
1097
- let detailType;
1098
- if (eventProperties.length > 0) {
1099
- detailType = this.buildEventDetailFromProperties(eventProperties, currentEventName, true);
1100
- }
1101
- else {
1102
- detailType = currentEventType || "";
1103
- }
1104
- this.addDispatchedEvent({
1105
- name: currentEventName,
1106
- detail: detailType,
1107
- has_argument: false,
1108
- description: currentEventDescription,
1109
- });
1110
- this.eventDescriptions.set(currentEventName, currentEventDescription);
1111
- eventProperties.length = 0;
1112
- currentEventName = undefined;
1113
- currentEventType = undefined;
1114
- currentEventDescription = undefined;
1115
- }
1116
- };
1117
- /**
1118
- * Finalizes the current typedef being built and adds it to the typedefs map.
1119
- *
1120
- * Handles three cases:
1121
- * 1. Properties defined via `@property` tags - builds an object type
1122
- * 2. Inline type definition via `@typedef {type}` - uses the type directly
1123
- * 3. No type specified - defaults to empty object type
1124
- *
1125
- * @example
1126
- * ```ts
1127
- * // Case 1: With properties
1128
- * // @typedef User
1129
- * // @property {string} name
1130
- * // @property {number} age
1131
- * // Result: type User = { name: string; age: number; }
1132
- *
1133
- * // Case 2: Inline type
1134
- * // @typedef {string | number} ID
1135
- * // Result: type ID = string | number
1136
- *
1137
- * // Case 3: No type
1138
- * // @typedef Empty
1139
- * // Result: type Empty = {}
1140
- * ```
1141
- */
1142
- const finalizeTypedef = () => {
1143
- if (currentTypedefName !== undefined) {
1144
- let typedefType;
1145
- let typedefTs;
1146
- if (typedefProperties.length > 0) {
1147
- /**
1148
- * Build type alias with property descriptions from `@property` tags.
1149
- * Use multiline formatting for better readability.
1150
- */
1151
- typedefType = this.buildEventDetailFromProperties(typedefProperties, undefined, true);
1152
- typedefTs = `type ${currentTypedefName} = ${typedefType}`;
1153
- }
1154
- else if (currentTypedefType) {
1155
- /**
1156
- * Use inline type definition (existing behavior).
1157
- * If the type ends with `}` or `};`, treat it as an interface body,
1158
- * otherwise treat it as a type alias.
1159
- */
1160
- typedefType = currentTypedefType;
1161
- typedefTs = TYPEDEF_END_REGEX.test(typedefType)
1162
- ? `interface ${currentTypedefName} ${typedefType}`
1163
- : `type ${currentTypedefName} = ${typedefType}`;
1164
- }
1165
- else {
1166
- /**
1167
- * No type or properties specified, default to empty object type.
1168
- */
1169
- typedefType = "{}";
1170
- typedefTs = `type ${currentTypedefName} = ${typedefType}`;
1171
- }
1172
- this.typedefs.set(currentTypedefName, {
1173
- type: typedefType,
1174
- name: currentTypedefName,
1175
- description: ComponentParser.assignValue(currentTypedefDescription),
1176
- ts: typedefTs,
1177
- });
1178
- typedefProperties.length = 0;
1179
- currentTypedefName = undefined;
1180
- currentTypedefType = undefined;
1181
- currentTypedefDescription = undefined;
1182
- }
1183
- };
1184
- for (const { tag, type: tagType, name, description, optional, default: defaultValue, source: tagSource, } of tags) {
1185
- const type = this.aliasType(tagType);
1186
- /**
1187
- * Get the description from the line immediately before this tag.
1188
- * This supports the pattern where descriptions appear on lines preceding tags.
1189
- */
1190
- const precedingDescription = getPrecedingDescription(tagSource);
1191
- switch (tag) {
1192
- case "extends":
1193
- case "extendProps":
1194
- this.extends = {
1195
- interface: name,
1196
- import: type,
1197
- };
1198
- if (isFirstTag)
1199
- isFirstTag = false;
1200
- break;
1201
- case "restProps": {
1202
- /**
1203
- * Prefer inline description (e.g., "@restProps {type} description" or "@restProps {type} - description"),
1204
- * fall back to preceding line description, then fall back to the
1205
- * comment block description (only for first tag if not already used).
1206
- *
1207
- * Note: comment-parser treats the first word after the type as "name" and the rest as "description",
1208
- * so we combine them to form the full inline description for @restProps.
1209
- */
1210
- const rawInlineDesc = name ? (description ? `${name} ${description}` : name) : description;
1211
- const inlineRestPropsDesc = cleanDescription(rawInlineDesc);
1212
- let restPropsDesc = inlineRestPropsDesc || precedingDescription;
1213
- if (!restPropsDesc && isFirstTag && !commentDescriptionUsed && commentDescription) {
1214
- restPropsDesc = commentDescription;
1215
- commentDescriptionUsed = true;
1216
- }
1217
- this.rest_props = {
1218
- type: "Element",
1219
- name: type,
1220
- description: restPropsDesc || undefined,
1221
- };
1222
- if (isFirstTag)
1223
- isFirstTag = false;
1224
- break;
1225
- }
1226
- case "slot": {
1227
- /**
1228
- * Prefer inline description (e.g., "@slot name - description"),
1229
- * fall back to preceding line description, then fall back to the
1230
- * comment block description (only for first tag if not already used).
1231
- */
1232
- const inlineSlotDesc = cleanDescription(description);
1233
- let slotDesc = inlineSlotDesc || precedingDescription;
1234
- if (!slotDesc && isFirstTag && !commentDescriptionUsed && commentDescription) {
1235
- slotDesc = commentDescription;
1236
- commentDescriptionUsed = true;
1237
- }
1238
- if (isFirstTag)
1239
- isFirstTag = false;
1240
- this.addSlot({
1241
- slot_name: name,
1242
- slot_props: type,
1243
- slot_description: slotDesc || undefined,
1244
- });
1245
- break;
1246
- }
1247
- case "event": {
1248
- /**
1249
- * Finalize any previous event being built before starting a new one.
1250
- */
1251
- finalizeEvent();
1252
- /**
1253
- * Start tracking new event with its name and type.
1254
- * Prefer inline description (e.g., "@event {type} name - description"),
1255
- * fall back to preceding line, then fall back to comment block description
1256
- * (only for first tag if not already used).
1257
- */
1258
- currentEventName = name;
1259
- currentEventType = type;
1260
- const inlineEventDesc = cleanDescription(description);
1261
- currentEventDescription = inlineEventDesc || precedingDescription;
1262
- if (!currentEventDescription && isFirstTag && !commentDescriptionUsed && commentDescription) {
1263
- currentEventDescription = commentDescription;
1264
- commentDescriptionUsed = true;
1265
- }
1266
- if (isFirstTag)
1267
- isFirstTag = false;
1268
- break;
1269
- }
1270
- case "type":
1271
- /**
1272
- * Track the `@type` tag for the current event.
1273
- * This allows specifying the event detail type separately from the `@event` tag.
1274
- */
1275
- if (currentEventName !== undefined) {
1276
- currentEventType = type;
1277
- }
1278
- break;
1279
- case "property": {
1280
- /**
1281
- * Collect properties for the current event or typedef.
1282
- * Properties are accumulated until the event/typedef is finalized.
1283
- */
1284
- const propertyData = {
1285
- name,
1286
- type,
1287
- description: cleanDescription(description),
1288
- optional: optional || false,
1289
- default: defaultValue,
1290
- };
1291
- if (currentEventName !== undefined) {
1292
- eventProperties.push(propertyData);
1293
- }
1294
- else if (currentTypedefName !== undefined) {
1295
- typedefProperties.push(propertyData);
1296
- }
1297
- break;
1298
- }
1299
- case "typedef": {
1300
- /**
1301
- * Finalize any previous typedef being built before starting a new one.
1302
- */
1303
- finalizeTypedef();
1304
- /**
1305
- * Start tracking new typedef with its name and type.
1306
- * Prefer inline description, fall back to preceding line description,
1307
- * then fall back to comment block description (only for first tag if not already used).
1308
- */
1309
- currentTypedefName = name;
1310
- currentTypedefType = type;
1311
- const inlineTypedefDesc = cleanDescription(description);
1312
- currentTypedefDescription = inlineTypedefDesc || precedingDescription;
1313
- if (!currentTypedefDescription && isFirstTag && !commentDescriptionUsed && commentDescription) {
1314
- currentTypedefDescription = commentDescription;
1315
- commentDescriptionUsed = true;
1316
- }
1317
- if (isFirstTag)
1318
- isFirstTag = false;
1319
- break;
1320
- }
1321
- case "generics":
1322
- this.generics = [name, type];
1323
- if (isFirstTag)
1324
- isFirstTag = false;
1325
- break;
1326
- }
1327
- }
1328
- /**
1329
- * Finalize any remaining event or typedef that wasn't closed by a new tag.
1330
- * This handles cases where the comment block ends without starting a new event/typedef.
1331
- */
1332
- finalizeEvent();
1333
- finalizeTypedef();
1334
- }
1335
- }
1336
- /**
1337
- * Builds an event detail type string from an array of property definitions.
1338
- *
1339
- * Creates an inline object type with JSDoc comments for each property,
1340
- * including descriptions and default values. Used for both event details
1341
- * and typedef property definitions.
1342
- *
1343
- * @param properties - Array of property definitions with name, type, description, etc.
1344
- * @param _eventName - Optional event name (unused, kept for API consistency)
1345
- * @returns A string representation of the object type with JSDoc comments
1346
- *
1347
- * @example
1348
- * ```ts
1349
- * // Input:
1350
- * [
1351
- * { name: "value", type: "string", description: "The new value" },
1352
- * { name: "count", type: "number", optional: true, default: "0" }
1353
- * ]
1354
- *
1355
- * // Output:
1356
- * "{ /** The new value *\/ value: string; /** @default 0 *\/ count?: number; }"
1357
- * ```
1358
- */
1359
- buildEventDetailFromProperties(properties, _eventName, multiline = false) {
1360
- if (properties.length === 0)
1361
- return "null";
1362
- /**
1363
- * Build inline object type with property descriptions as JSDoc comments.
1364
- * Each property gets a JSDoc comment if it has a description or default value.
1365
- */
1366
- const props = properties
1367
- .map(({ name, type, description, optional, default: defaultValue }) => {
1368
- const optionalMarker = optional ? "?" : "";
1369
- let comment = description || "";
1370
- /**
1371
- * Add default value to description if present.
1372
- * If there's already a description, append the default; otherwise use only the default.
1373
- */
1374
- if (defaultValue && comment) {
1375
- comment = `${comment} @default ${defaultValue}`;
1376
- }
1377
- else if (defaultValue) {
1378
- comment = `@default ${defaultValue}`;
1379
- }
1380
- if (comment) {
1381
- if (multiline) {
1382
- return `/** ${comment} */\n ${name}${optionalMarker}: ${type};`;
1383
- }
1384
- return `/** ${comment} */ ${name}${optionalMarker}: ${type};`;
1385
- }
1386
- return `${name}${optionalMarker}: ${type};`;
1387
- })
1388
- .join(multiline ? "\n " : " ");
1389
- return multiline ? `{\n ${props}\n}` : `{ ${props} }`;
1390
- }
1391
- /**
1392
- * Generates a TypeScript type name for a context key.
1393
- *
1394
- * Converts kebab-case, snake_case, or space-separated keys into PascalCase
1395
- * with "Context" suffix. Splits on dashes, underscores, and spaces, then
1396
- * capitalizes each part.
1397
- *
1398
- * @param key - The context key (e.g., "simple-modal", "tabs_context", "My Context")
1399
- * @returns The generated type name (e.g., "SimpleModalContext", "TabsContext", "MyContextContext")
1400
- *
1401
- * @example
1402
- * ```ts
1403
- * generateContextTypeName("simple-modal") // Returns: "SimpleModalContext"
1404
- * generateContextTypeName("Tabs") // Returns: "TabsContext"
1405
- * generateContextTypeName("user_settings") // Returns: "UserSettingsContext"
1406
- * generateContextTypeName("my context") // Returns: "MyContextContext"
1407
- * ```
1408
- */
1409
- generateContextTypeName(key) {
1410
- const parts = key.split(CONTEXT_KEY_SPLIT_REGEX);
1411
- const capitalized = parts.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
1412
- return `${capitalized}Context`;
1413
- }
1414
- /**
1415
- * Builds a cache of variable type information from JSDoc comments.
1416
- *
1417
- * Scans the source code for variable declarations and extracts type information
1418
- * from preceding JSDoc comments. This cache is used to infer types for context
1419
- * properties and other variable references.
1420
- *
1421
- * @example
1422
- * ```ts
1423
- * // Source code:
1424
- * /**
1425
- * * @type {string}
1426
- * * The user's name
1427
- * *\/
1428
- * const userName = "John";
1429
- *
1430
- * // Cache entry:
1431
- * // { "userName": { type: "string", description: "The user's name" } }
1432
- * ```
1433
- */
1434
- buildVariableInfoCache() {
1435
- if (!this.source)
1436
- return;
1437
- if (!this.sourceLinesCache) {
1438
- this.sourceLinesCache = this.source.split("\n");
1439
- }
1440
- const lines = this.sourceLinesCache;
1441
- for (let i = 0; i < lines.length; i++) {
1442
- const line = lines[i].trim();
1443
- /**
1444
- * Match variable declarations (const, let, function) to find variables
1445
- * that might have JSDoc type annotations.
1446
- */
1447
- const varMatch = line.match(VAR_DECLARATION_REGEX);
1448
- if (varMatch) {
1449
- const varName = varMatch[1];
1450
- /**
1451
- * Look backwards for JSDoc comment preceding this variable declaration.
1452
- * Comments must be immediately before the declaration with no non-comment
1453
- * lines in between.
1454
- */
1455
- for (let j = i - 1; j >= 0; j--) {
1456
- const prevLine = lines[j].trim();
1457
- /**
1458
- * Stop if we hit a non-comment, non-empty line - the comment is too far away.
1459
- */
1460
- if (prevLine && !prevLine.startsWith("*") && !prevLine.startsWith("/*") && !prevLine.startsWith("//")) {
1461
- break;
1462
- }
1463
- /**
1464
- * Found start of JSDoc comment - extract the entire comment block
1465
- * and parse it to get type and description information.
1466
- */
1467
- if (prevLine.startsWith("/**")) {
1468
- /**
1469
- * Extract the JSDoc comment block from start to the line before the variable.
1470
- */
1471
- const commentLines = [];
1472
- for (let k = j; k < i; k++) {
1473
- commentLines.push(lines[k]);
1474
- }
1475
- const commentBlock = commentLines.join("\n");
1476
- /**
1477
- * Parse the JSDoc to extract `@type` tag and description.
1478
- * Store in cache for later lookup.
1479
- */
1480
- const parsed = (0, comment_parser_1.parse)(commentBlock, { spacing: "preserve" });
1481
- const { type: typeTag, description } = this.getCommentTags(parsed);
1482
- if (typeTag) {
1483
- this.variableInfoCache.set(varName, {
1484
- type: this.aliasType(typeTag.type),
1485
- description: description || typeTag.description,
1486
- });
1487
- }
1488
- break;
1489
- }
1490
- }
1491
- }
1492
- }
1493
- }
1494
- /**
1495
- * Cache for compiled regex patterns for variable name matching.
1496
- *
1497
- * Stores three regex patterns (const, let, function) per variable name
1498
- * to avoid recreating them on each lookup. Improves performance when
1499
- * searching for the same variable multiple times.
1500
- */
1501
- static VAR_NAME_REGEX_CACHE = new Map();
1502
- /**
1503
- * Gets or creates cached regex patterns for matching variable declarations.
1504
- *
1505
- * Creates three regex patterns for matching const, let, and function declarations
1506
- * of a specific variable name. The patterns are cached to avoid recreating them
1507
- * for the same variable name.
1508
- *
1509
- * @param varName - The variable name to create regex patterns for
1510
- * @returns A tuple of three RegExp objects for const, let, and function patterns
1511
- *
1512
- * @example
1513
- * ```ts
1514
- * getVarNameRegexes("count")
1515
- * // Returns:
1516
- * // [
1517
- * // /\bconst\s+count\s*=/,
1518
- * // /\blet\s+count\s*=/,
1519
- * // /\bfunction\s+count\s*\(/
1520
- * // ]
1521
- * ```
1522
- */
1523
- static getVarNameRegexes(varName) {
1524
- let cached = ComponentParser.VAR_NAME_REGEX_CACHE.get(varName);
1525
- if (!cached) {
1526
- const escaped = varName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1527
- cached = [
1528
- new RegExp(`const\\s+${escaped}\\s*=`),
1529
- new RegExp(`let\\s+${escaped}\\s*=`),
1530
- new RegExp(`function\\s+${escaped}\\s*\\(`),
1531
- ];
1532
- ComponentParser.VAR_NAME_REGEX_CACHE.set(varName, cached);
1533
- }
1534
- return cached;
1535
- }
1536
- /**
1537
- * Finds the type and description for a variable by searching for its JSDoc comment.
1538
- *
1539
- * First checks the cache built by {@link buildVariableInfoCache}. If not found,
1540
- * searches the source code directly for the variable declaration and its
1541
- * preceding JSDoc comment.
1542
- *
1543
- * @param varName - The variable name to look up
1544
- * @returns The type and description if found, null otherwise
1545
- *
1546
- * @example
1547
- * ```ts
1548
- * // Source:
1549
- * /**
1550
- * * @type {number}
1551
- * * The count value
1552
- * *\/
1553
- * const count = 0;
1554
- *
1555
- * findVariableTypeAndDescription("count")
1556
- * // Returns: { type: "number", description: "The count value" }
1557
- * ```
1558
- */
1559
- findVariableTypeAndDescription(varName) {
1560
- const cached = this.variableInfoCache.get(varName);
1561
- if (cached) {
1562
- return cached;
1563
- }
1564
- /**
1565
- * Search through the source code directly for JSDoc comments.
1566
- * This is a fallback when the variable wasn't found in the cache.
1567
- */
1568
- if (!this.source)
1569
- return null;
1570
- if (!this.sourceLinesCache) {
1571
- this.sourceLinesCache = this.source.split("\n");
1572
- }
1573
- const lines = this.sourceLinesCache;
1574
- const [constRegex, letRegex, funcRegex] = ComponentParser.getVarNameRegexes(varName);
1575
- for (let i = 0; i < lines.length; i++) {
1576
- const line = lines[i].trim();
1577
- /**
1578
- * Check if this line declares our variable.
1579
- * Match patterns like: const varName = ..., let varName = ..., function varName
1580
- */
1581
- const constMatch = line.match(constRegex);
1582
- const letMatch = line.match(letRegex);
1583
- const funcMatch = line.match(funcRegex);
1584
- if (constMatch || letMatch || funcMatch) {
1585
- /**
1586
- * Look backwards for JSDoc comment preceding this variable declaration.
1587
- */
1588
- for (let j = i - 1; j >= 0; j--) {
1589
- const prevLine = lines[j].trim();
1590
- /**
1591
- * Stop if we hit a non-comment, non-empty line - the comment is too far away.
1592
- */
1593
- if (prevLine && !prevLine.startsWith("*") && !prevLine.startsWith("/*") && !prevLine.startsWith("//")) {
1594
- break;
1595
- }
1596
- /**
1597
- * Found start of JSDoc comment - extract and parse it.
1598
- */
1599
- if (prevLine.startsWith("/**")) {
1600
- /**
1601
- * Extract the JSDoc comment block from start to the line before the variable.
1602
- */
1603
- const commentLines = [];
1604
- for (let k = j; k < i; k++) {
1605
- commentLines.push(lines[k]);
1606
- }
1607
- const commentBlock = commentLines.join("\n");
1608
- /**
1609
- * Parse the JSDoc to extract `@type` tag and description.
1610
- */
1611
- const parsed = (0, comment_parser_1.parse)(commentBlock, { spacing: "preserve" });
1612
- const { type: typeTag, description } = this.getCommentTags(parsed);
1613
- if (typeTag) {
1614
- return {
1615
- type: this.aliasType(typeTag.type),
1616
- description: description || typeTag.description,
1617
- };
1618
- }
1619
- break;
1620
- }
1621
- }
1622
- }
1623
- }
1624
- return null;
1625
- }
1626
- /**
1627
- * Parses a context value from an AST node to extract type information.
1628
- *
1629
- * Handles two cases:
1630
- * 1. ObjectExpression: Parses object literal properties and infers types from variable references
1631
- * 2. Identifier: Looks up the variable's type from JSDoc comments
1632
- *
1633
- * @param node - The AST node representing the context value
1634
- * @param key - The context key name
1635
- * @returns A ComponentContext object with parsed properties, or null if parsing fails
1636
- *
1637
- * @example
1638
- * ```ts
1639
- * // Case 1: Object literal
1640
- * // setContext('modal', { open, close })
1641
- * // Returns: { key: "modal", typeName: "ModalContext", properties: [...] }
1642
- *
1643
- * // Case 2: Variable reference
1644
- * // setContext('tabs', tabContext)
1645
- * // Returns: { key: "tabs", typeName: "TabsContext", properties: [...] }
1646
- * ```
1647
- */
1648
- parseContextValue(node, key) {
1649
- if (!node || typeof node !== "object" || !("type" in node))
1650
- return null;
1651
- if (node.type === "ObjectExpression") {
1652
- /**
1653
- * Parse object literal: { open, close }
1654
- * Extract each property and try to infer its type from the variable it references.
1655
- */
1656
- const properties = [];
1657
- const objExpr = node;
1658
- for (const prop of objExpr.properties) {
1659
- if (prop.type === "Property") {
1660
- const propObj = prop;
1661
- const propName = propObj.key && "name" in propObj.key
1662
- ? propObj.key.name
1663
- : propObj.key && "value" in propObj.key
1664
- ? String(propObj.key.value)
1665
- : undefined;
1666
- if (!propName)
1667
- continue;
1668
- /**
1669
- * Try to find the variable definition to get its JSDoc type.
1670
- * If not found, default to "any" and optionally warn in verbose mode.
1671
- */
1672
- let propType = "any";
1673
- let propDescription;
1674
- if (propObj.value && typeof propObj.value === "object" && "type" in propObj.value) {
1675
- if (propObj.value.type === "Identifier") {
1676
- const varName = propObj.value.name;
1677
- const varInfo = this.findVariableTypeAndDescription(varName);
1678
- if (varInfo) {
1679
- propType = varInfo.type;
1680
- propDescription = varInfo.description;
1681
- }
1682
- else if (this.options?.verbose) {
1683
- console.warn(`Warning: Context "${key}" property "${propName}" has no type annotation. Using "any".`);
1684
- }
1685
- }
1686
- else if (propObj.value.type === "ArrowFunctionExpression" ||
1687
- propObj.value.type === "FunctionExpression") {
1688
- /**
1689
- * Inline function - infer function type from parameters.
1690
- * Parameters are typed as `any` since we don't have type information.
1691
- */
1692
- const funcExpr = propObj.value;
1693
- const params = funcExpr.params
1694
- ?.map((p) => {
1695
- if (p && typeof p === "object" && "name" in p) {
1696
- return `${p.name || "arg"}: any`;
1697
- }
1698
- return "arg: any";
1699
- })
1700
- .join(", ") || "";
1701
- propType = `(${params}) => any`;
1702
- }
1703
- else if (propObj.value.type === "Literal") {
1704
- /**
1705
- * Literal value - infer type from the literal's value type.
1706
- */
1707
- const literal = propObj.value;
1708
- propType = literal.value == null ? "null" : typeof literal.value;
1709
- }
1710
- }
1711
- properties.push({
1712
- name: propName,
1713
- type: propType,
1714
- description: propDescription,
1715
- optional: false,
1716
- });
1717
- }
1718
- }
1719
- return {
1720
- key,
1721
- typeName: this.generateContextTypeName(key),
1722
- properties,
1723
- description: undefined,
1724
- };
1725
- }
1726
- else if (node.type === "Identifier") {
1727
- /**
1728
- * setContext('key', someVariable)
1729
- * The context value is a direct variable reference.
1730
- * Look up the variable's type from its JSDoc comment.
1731
- */
1732
- const ident = node;
1733
- const varName = ident.name;
1734
- const varInfo = this.findVariableTypeAndDescription(varName);
1735
- if (varInfo) {
1736
- return {
1737
- key,
1738
- typeName: this.generateContextTypeName(key),
1739
- properties: [
1740
- {
1741
- name: varName,
1742
- type: varInfo.type,
1743
- description: varInfo.description,
1744
- optional: false,
1745
- },
1746
- ],
1747
- };
1748
- }
1749
- else if (this.options?.verbose) {
1750
- console.warn(`Warning: Context "${key}" variable "${varName}" has no type annotation. Using "any".`);
1751
- }
1752
- /**
1753
- * Still create context with 'any' type even if we couldn't find type information.
1754
- * This ensures the context is documented even without type annotations.
1755
- */
1756
- return {
1757
- key,
1758
- typeName: this.generateContextTypeName(key),
1759
- properties: [
1760
- {
1761
- name: varName,
1762
- type: "any",
1763
- description: undefined,
1764
- optional: false,
1765
- },
1766
- ],
1767
- };
1768
- }
1769
- return null;
1770
- }
1771
- /**
1772
- * Parses a `setContext` call expression to extract context information.
1773
- *
1774
- * Extracts the context key from the first argument (must be a string literal
1775
- * or simple template literal) and the context value from the second argument.
1776
- * Only processes static keys - dynamic keys are skipped with a warning.
1777
- *
1778
- * @param node - The AST node (should be a CallExpression)
1779
- * @param _parent - The parent node (unused)
1780
- *
1781
- * @example
1782
- * ```ts
1783
- * // Parses: setContext('modal', { open, close })
1784
- * // Extracts: key = "modal", value = { open, close }
1785
- *
1786
- * // Skips: setContext(dynamicKey, value) // key is not a literal
1787
- * ```
1788
- */
1789
- parseSetContextCall(node, _parent) {
1790
- /**
1791
- * Extract context key (first argument).
1792
- * Must be a CallExpression with at least one argument.
1793
- */
1794
- if (!node || typeof node !== "object" || !("type" in node) || node.type !== "CallExpression") {
1795
- return;
1796
- }
1797
- const callExpr = node;
1798
- const keyArg = callExpr.arguments[0];
1799
- if (!keyArg)
1800
- return;
1801
- let contextKey = null;
1802
- if (keyArg.type === "Literal") {
1803
- const literal = keyArg;
1804
- contextKey = typeof literal.value === "string" ? literal.value : String(literal.value);
1805
- }
1806
- else if (keyArg.type === "TemplateLiteral") {
1807
- /**
1808
- * Handle simple template literals (static strings only).
1809
- * Dynamic template literals with expressions are skipped.
1810
- */
1811
- if (keyArg.quasis?.length === 1) {
1812
- const cooked = keyArg.quasis[0].value.cooked;
1813
- contextKey = cooked != null ? cooked : null;
1814
- }
1815
- else if (this.options?.verbose) {
1816
- console.warn("Warning: Skipping setContext with dynamic template literal key");
1817
- }
1818
- }
1819
- else if (this.options?.verbose) {
1820
- console.warn(`Warning: Skipping setContext with non-literal key (type: ${keyArg.type})`);
1821
- }
1822
- if (!contextKey)
1823
- return;
1824
- /**
1825
- * Extract context value (second argument).
1826
- * This is parsed to extract type information from the value.
1827
- */
1828
- const valueArg = callExpr.arguments[1];
1829
- if (!valueArg)
1830
- return;
1831
- /**
1832
- * Parse the context object to extract properties and types.
1833
- */
1834
- const contextInfo = this.parseContextValue(valueArg, contextKey);
1835
- if (contextInfo) {
1836
- /**
1837
- * Check if context with same key already exists.
1838
- * If it does, warn and use the first occurrence (don't overwrite).
1839
- */
1840
- if (this.contexts.has(contextKey)) {
1841
- if (this.options?.verbose) {
1842
- console.warn(`Warning: Multiple setContext calls with key "${contextKey}". Using first occurrence.`);
1843
- }
1844
- }
1845
- else {
1846
- this.contexts.set(contextKey, contextInfo);
1847
- }
1848
- }
1849
- }
1850
- /**
1851
- * Cleans up all parser state, resetting the instance for reuse.
1852
- *
1853
- * Clears all maps, caches, and resets all state variables to their initial
1854
- * values. Should be called before parsing a new component or when the
1855
- * parser instance needs to be reset.
1856
- *
1857
- * @example
1858
- * ```ts
1859
- * parser.parseSvelteComponent(source1, diagnostics1);
1860
- * parser.cleanup(); // Reset state
1861
- * parser.parseSvelteComponent(source2, diagnostics2); // Fresh parse
1862
- * ```
1863
- */
1864
- cleanup() {
1865
- this.source = undefined;
1866
- this.compiled = undefined;
1867
- this.parsed = undefined;
1868
- this.rest_props = undefined;
1869
- this.extends = undefined;
1870
- this.componentComment = undefined;
1871
- this.reactive_vars.clear();
1872
- this.props.clear();
1873
- this.moduleExports.clear();
1874
- this.slots.clear();
1875
- this.events.clear();
1876
- this.eventDescriptions.clear();
1877
- this.forwardedEvents.clear();
1878
- this.typedefs.clear();
1879
- this.generics = null;
1880
- this.bindings.clear();
1881
- this.contexts.clear();
1882
- this.variableInfoCache.clear();
1883
- this.sourceLinesCache = undefined;
1884
- }
1885
- /**
1886
- * Pre-compiled regex for matching script blocks in Svelte components.
1887
- *
1888
- * Matches `<script>` tags and their content, capturing the opening tag,
1889
- * script content, and closing tag. Global and case-insensitive flags
1890
- * allow matching multiple script blocks.
1891
- *
1892
- * @example
1893
- * ```ts
1894
- * // Matches:
1895
- * // "<script>const x = 1;</script>"
1896
- * // "<script lang='ts'>...</script>"
1897
- * ```
1898
- */
1899
- static SCRIPT_BLOCK_REGEX = /(<script[^>]*>)([\s\S]*?)(<\/script>)/gi;
1900
- /**
1901
- * Pre-compiled regex for matching TypeScript directive comments.
1902
- *
1903
- * Matches TypeScript directive comments like ts-ignore, ts-expect-error,
1904
- * etc. Used to remove these directives from script blocks before JSDoc parsing.
1905
- *
1906
- * @example
1907
- * ```ts
1908
- * // Matches:
1909
- * // "// ts-ignore"
1910
- * // "// ts-expect-error: reason"
1911
- * // "// ts-nocheck"
1912
- * ```
1913
- */
1914
- static TS_DIRECTIVE_REGEX = /\/\/\s*@ts-[^\n\r]*/g;
1915
- /**
1916
- * Strips TypeScript directive comments from script blocks only.
1917
- *
1918
- * Removes TypeScript directive comments (e.g., ts-ignore, ts-expect-error directives)
1919
- * from within `<script>` blocks to prevent them from interfering with JSDoc parsing.
1920
- * Directives outside script blocks are left untouched.
1921
- *
1922
- * @param source - The Svelte component source code
1923
- * @returns The source code with TypeScript directives removed from script blocks
1924
- *
1925
- * @example
1926
- * ```ts
1927
- * // Input (with TypeScript directive):
1928
- * <script>
1929
- * const x: string = 123; // directive removed
1930
- * </script>
1931
- *
1932
- * // Output (directive stripped):
1933
- * <script>
1934
- * const x: string = 123;
1935
- * </script>
1936
- * ```
1937
- */
1938
- static stripTypeScriptDirectivesFromScripts(source) {
1939
- /**
1940
- * Find all script blocks and strip directives only from within them.
1941
- * Note: Need to reset lastIndex for global regex to ensure consistent matching.
1942
- */
1943
- ComponentParser.SCRIPT_BLOCK_REGEX.lastIndex = 0;
1944
- return source.replace(ComponentParser.SCRIPT_BLOCK_REGEX, (_match, openTag, scriptContent, closeTag) => {
1945
- /**
1946
- * Remove TypeScript directives from script content only.
1947
- * Preserves the script tags and other content outside script blocks.
1948
- */
1949
- ComponentParser.TS_DIRECTIVE_REGEX.lastIndex = 0;
1950
- const cleanedContent = scriptContent.replace(ComponentParser.TS_DIRECTIVE_REGEX, "");
1951
- return openTag + cleanedContent + closeTag;
1952
- });
1953
- }
1954
- /**
1955
- * Parses a Svelte component and extracts all component metadata.
1956
- *
1957
- * This is the main entry point that orchestrates the entire parsing process:
1958
- * 1. Cleans up previous state
1959
- * 2. Strips TypeScript directives that might interfere with JSDoc
1960
- * 3. Compiles the component to get the AST
1961
- * 4. Collects reactive variables
1962
- * 5. Builds variable type cache
1963
- * 6. Parses custom types from JSDoc comments
1964
- * 7. Walks the AST to extract props, slots, events, bindings, and contexts
1965
- * 8. Post-processes events to distinguish dispatched vs forwarded
1966
- * 9. Processes props with bindings and slots with prop references
1967
- * 10. Returns the complete parsed component structure
1968
- *
1969
- * @param source - The Svelte component source code
1970
- * @param diagnostics - Diagnostic information (module name and file path)
1971
- * @returns A ParsedComponent object containing all extracted metadata
1972
- *
1973
- * @example
1974
- * ```ts
1975
- * const parser = new ComponentParser();
1976
- * const result = parser.parseSvelteComponent(source, {
1977
- * moduleName: "Button",
1978
- * filePath: "./Button.svelte"
1979
- * });
1980
- * // Returns: { props: [...], slots: [...], events: [...], ... }
1981
- * ```
1982
- */
1983
- parseSvelteComponent(source, diagnostics) {
1984
- if (this.options?.verbose) {
1985
- console.log(`[parsing] "${diagnostics.moduleName}" ${diagnostics.filePath}`);
1986
- }
1987
- this.cleanup();
1988
- /**
1989
- * Strip TypeScript directives from script blocks only to prevent interference with JSDoc.
1990
- * TypeScript directive comments can break JSDoc parsing if not removed.
1991
- */
1992
- const cleanedSource = ComponentParser.stripTypeScriptDirectivesFromScripts(source);
1993
- this.source = cleanedSource;
1994
- /**
1995
- * Parse once - compile() internally calls parse(), so we can extract the AST from it.
1996
- * This avoids parsing the source twice for better performance.
1997
- */
1998
- const compiled = (0, compiler_1.compile)(cleanedSource);
1999
- this.compiled = compiled;
2000
- /**
2001
- * Reuse the AST from compilation instead of parsing again.
2002
- * The compile result includes the parsed AST, so we use that if available,
2003
- * otherwise fall back to parsing directly.
2004
- */
2005
- this.parsed = compiled.ast || (0, compiler_1.parse)(cleanedSource);
2006
- this.collectReactiveVars();
2007
- this.sourceLinesCache = this.source.split("\n");
2008
- this.buildVariableInfoCache();
2009
- this.parseCustomTypes();
2010
- if (this.parsed?.module) {
2011
- (0, compiler_1.walk)(this.parsed?.module, {
2012
- enter: (node) => {
2013
- if (node.type === "ExportNamedDeclaration") {
2014
- /**
2015
- * Skip re-exports (e.g., export { A, B } from 'library').
2016
- * These don't have declarations in the current file, so we can't extract metadata.
2017
- */
2018
- if (node.declaration == null) {
2019
- return;
2020
- }
2021
- /**
2022
- * Handle both VariableDeclaration and FunctionDeclaration exports.
2023
- * Both can be exported from the module script and need type extraction.
2024
- */
2025
- if (!node.declaration || typeof node.declaration !== "object" || !("type" in node.declaration)) {
2026
- return;
2027
- }
2028
- let prop_name;
2029
- let kind;
2030
- let description;
2031
- let isFunctionDeclaration = false;
2032
- let value;
2033
- let type;
2034
- let isFunction = false;
2035
- let params;
2036
- let returnType;
2037
- if (node.declaration.type === "FunctionDeclaration") {
2038
- const funcDecl = node.declaration;
2039
- if (!funcDecl.id || !funcDecl.id.name)
2040
- return;
2041
- prop_name = funcDecl.id.name;
2042
- kind = "function";
2043
- value = undefined;
2044
- type = "() => any";
2045
- isFunction = true;
2046
- isFunctionDeclaration = true;
2047
- }
2048
- else if (node.declaration.type === "VariableDeclaration") {
2049
- const varDecl = node.declaration;
2050
- const firstDeclarator = varDecl.declarations[0];
2051
- if (!firstDeclarator || typeof firstDeclarator !== "object" || !("id" in firstDeclarator)) {
2052
- return;
2053
- }
2054
- const { id, init } = firstDeclarator;
2055
- if (!id || typeof id !== "object" || !("name" in id)) {
2056
- return;
2057
- }
2058
- prop_name = id.name;
2059
- /**
2060
- * VariableDeclaration.kind can be "var" | "let" | "const", but ComponentProp.kind
2061
- * is "let" | "const" | "function". Convert "var" to "let" for compatibility
2062
- * since "var" is not a valid prop kind in Svelte components.
2063
- */
2064
- kind = varDecl.kind === "var" ? "let" : varDecl.kind;
2065
- const initResult = init != null ? this.processInitializer(init) : { isFunction: false };
2066
- ({ value, type, isFunction } = initResult);
2067
- }
2068
- else {
2069
- return;
2070
- }
2071
- if (node.leadingComments) {
2072
- const jsdocInfo = this.processJSDocComment(node.leadingComments);
2073
- if (jsdocInfo) {
2074
- if (jsdocInfo.type)
2075
- type = jsdocInfo.type;
2076
- params = jsdocInfo.params;
2077
- returnType = jsdocInfo.returnType;
2078
- if (jsdocInfo.description)
2079
- description = jsdocInfo.description;
2080
- }
2081
- }
2082
- // Merge returnType into type for function declarations if not overridden by @type
2083
- if (isFunctionDeclaration && type === "() => any" && returnType) {
2084
- if (params && params.length > 0) {
2085
- const paramStrings = params.map((param) => {
2086
- const optional = param.optional ? "?" : "";
2087
- return `${param.name}${optional}: ${param.type}`;
2088
- });
2089
- const paramsString = paramStrings.join(", ");
2090
- type = `(${paramsString}) => ${returnType}`;
2091
- }
2092
- else {
2093
- type = `() => ${returnType}`;
2094
- }
2095
- }
2096
- if (!description && type && this.typedefs.has(type)) {
2097
- description = this.typedefs.get(type)?.description;
2098
- }
2099
- this.addModuleExport(prop_name, {
2100
- name: prop_name,
2101
- kind,
2102
- description,
2103
- type,
2104
- value,
2105
- params,
2106
- returnType,
2107
- isFunction,
2108
- isFunctionDeclaration,
2109
- isRequired: false,
2110
- constant: kind === "const",
2111
- reactive: false,
2112
- });
2113
- }
2114
- },
2115
- });
2116
- }
2117
- let dispatcher_name;
2118
- const callees = [];
2119
- (0, compiler_1.walk)({ html: this.parsed.html, instance: this.parsed.instance }, {
2120
- enter: (node, parent, _prop) => {
2121
- if (node.type === "CallExpression") {
2122
- const callExpr = node;
2123
- const calleeName = callExpr.callee && typeof callExpr.callee === "object" && "name" in callExpr.callee
2124
- ? callExpr.callee.name
2125
- : undefined;
2126
- if (calleeName === "createEventDispatcher") {
2127
- if (parent &&
2128
- typeof parent === "object" &&
2129
- "id" in parent &&
2130
- parent.id &&
2131
- typeof parent.id === "object" &&
2132
- "name" in parent.id) {
2133
- dispatcher_name = parent.id.name;
2134
- }
2135
- }
2136
- if (calleeName === "setContext") {
2137
- this.parseSetContextCall(node, parent ?? undefined);
2138
- }
2139
- if (calleeName) {
2140
- callees.push({
2141
- name: calleeName,
2142
- arguments: callExpr.arguments,
2143
- });
2144
- }
2145
- }
2146
- /**
2147
- * Check for Spread node (Svelte-specific AST node type).
2148
- * Note: Spread is a Svelte-specific type not in estree, so we check the string value.
2149
- * This handles `{...$$restProps}` spread syntax in component templates.
2150
- */
2151
- if (node && typeof node === "object" && "type" in node && String(node.type) === "Spread") {
2152
- const spreadNode = node;
2153
- if (spreadNode.expression?.name === "$$restProps") {
2154
- /**
2155
- * Check if parent is InlineComponent or Element (Svelte-specific types).
2156
- * Rest props can only be spread on components or elements.
2157
- */
2158
- if (parent &&
2159
- typeof parent === "object" &&
2160
- "type" in parent &&
2161
- (parent.type === "InlineComponent" || parent.type === "Element")) {
2162
- const parentType = parent.type;
2163
- const parentName = "name" in parent && typeof parent.name === "string" ? parent.name : undefined;
2164
- if (parentName) {
2165
- const restProps = parentType === "InlineComponent"
2166
- ? {
2167
- type: "InlineComponent",
2168
- name: parentName,
2169
- }
2170
- : {
2171
- type: "Element",
2172
- name: parentName,
2173
- };
2174
- /**
2175
- * Handle svelte:element - check if this attribute is hardcoded.
2176
- * The 'this' value is stored in the 'tag' property of the Element node.
2177
- * If tag is a string, it's hardcoded; if undefined/null, it's dynamic.
2178
- */
2179
- if (parentType === "Element" && parentName === "svelte:element") {
2180
- if ("tag" in parent && typeof parent.tag === "string") {
2181
- restProps.thisValue = parent.tag;
2182
- }
2183
- /**
2184
- * If tag is undefined or not a string, thisValue remains undefined (dynamic).
2185
- */
2186
- }
2187
- /**
2188
- * Only set rest_props from AST if not already set by `@restProps` annotation.
2189
- * The annotation takes precedence as it can specify union types like "ul | ol"
2190
- * which can't be inferred from the AST alone.
2191
- */
2192
- if (this.rest_props === undefined) {
2193
- this.rest_props = restProps;
2194
- }
2195
- }
2196
- }
2197
- }
2198
- }
2199
- if (node.type === "VariableDeclaration") {
2200
- this.vars.add(node);
2201
- }
2202
- if (node.type === "ExportNamedDeclaration") {
2203
- /**
2204
- * Handle export {} - empty export statement, nothing to extract.
2205
- */
2206
- if (node.declaration == null && node.specifiers.length === 0) {
2207
- return;
2208
- }
2209
- /**
2210
- * Handle renamed exports (e.g., export { localName as exportedName }).
2211
- * We need to find the original declaration and use the exported name as the prop name.
2212
- */
2213
- let prop_name;
2214
- if (node.declaration == null && node.specifiers[0]?.type === "ExportSpecifier") {
2215
- const specifier = node.specifiers[0];
2216
- const localName = specifier.local && typeof specifier.local === "object" && "name" in specifier.local
2217
- ? specifier.local.name
2218
- : undefined;
2219
- const exportedName = specifier.exported && typeof specifier.exported === "object" && "name" in specifier.exported
2220
- ? specifier.exported.name
2221
- : undefined;
2222
- if (!localName || !exportedName)
2223
- return;
2224
- let declaration;
2225
- /**
2226
- * Search through all variable declarations for this variable.
2227
- * Limitation: the variable must have been declared before the export
2228
- * since we're walking the AST in order.
2229
- */
2230
- for (const varDecl of Array.from(this.vars)) {
2231
- if (varDecl.declarations.some((decl) => decl.id &&
2232
- typeof decl.id === "object" &&
2233
- "type" in decl.id &&
2234
- decl.id.type === "Identifier" &&
2235
- decl.id.name === localName)) {
2236
- declaration = varDecl;
2237
- break;
2238
- }
2239
- }
2240
- node.declaration = declaration;
2241
- prop_name = exportedName;
2242
- }
2243
- /**
2244
- * Skip re-exports (e.g., export { A, B } from 'library').
2245
- * These don't have declarations in the current file.
2246
- */
2247
- if (node.declaration == null) {
2248
- return;
2249
- }
2250
- /**
2251
- * Handle both VariableDeclaration and FunctionDeclaration.
2252
- * Both can be exported as props from the component.
2253
- */
2254
- if (!node.declaration || typeof node.declaration !== "object" || !("type" in node.declaration)) {
2255
- return;
2256
- }
2257
- let kind;
2258
- let description;
2259
- let isFunctionDeclaration = false;
2260
- let value;
2261
- let type;
2262
- let isFunction = false;
2263
- let params;
2264
- let returnType;
2265
- let isRequired = false;
2266
- if (node.declaration.type === "FunctionDeclaration") {
2267
- const funcDecl = node.declaration;
2268
- if (!funcDecl.id || !funcDecl.id.name)
2269
- return;
2270
- prop_name ??= funcDecl.id.name;
2271
- kind = "function";
2272
- value = undefined;
2273
- type = "() => any";
2274
- isFunction = true;
2275
- isFunctionDeclaration = true;
2276
- isRequired = false;
2277
- }
2278
- else if (node.declaration.type === "VariableDeclaration") {
2279
- const varDecl = node.declaration;
2280
- const firstDeclarator = varDecl.declarations[0];
2281
- if (!firstDeclarator || typeof firstDeclarator !== "object" || !("id" in firstDeclarator)) {
2282
- return;
2283
- }
2284
- const { id, init } = firstDeclarator;
2285
- if (id && typeof id === "object" && "name" in id) {
2286
- prop_name ??= id.name;
2287
- }
2288
- else {
2289
- return;
2290
- }
2291
- /**
2292
- * VariableDeclaration.kind can be "var" | "let" | "const", but ComponentProp.kind
2293
- * is "let" | "const" | "function". Convert "var" to "let" for compatibility
2294
- * since "var" is not a valid prop kind in Svelte components.
2295
- */
2296
- kind = varDecl.kind === "var" ? "let" : varDecl.kind;
2297
- isRequired = kind === "let" && init == null;
2298
- const initResult = init != null ? this.processInitializer(init) : { isFunction: false };
2299
- ({ value, type, isFunction } = initResult);
2300
- }
2301
- else {
2302
- return;
2303
- }
2304
- if (node.leadingComments) {
2305
- const jsdocInfo = this.processJSDocComment(node.leadingComments);
2306
- if (jsdocInfo) {
2307
- if (jsdocInfo.type)
2308
- type = jsdocInfo.type;
2309
- params = jsdocInfo.params;
2310
- returnType = jsdocInfo.returnType;
2311
- if (jsdocInfo.description)
2312
- description = jsdocInfo.description;
2313
- }
2314
- }
2315
- // Merge returnType into type for function declarations if not overridden by @type
2316
- if (isFunctionDeclaration && type === "() => any" && returnType) {
2317
- if (params && params.length > 0) {
2318
- const paramStrings = params.map((param) => {
2319
- const optional = param.optional ? "?" : "";
2320
- return `${param.name}${optional}: ${param.type}`;
2321
- });
2322
- const paramsString = paramStrings.join(", ");
2323
- type = `(${paramsString}) => ${returnType}`;
2324
- }
2325
- else {
2326
- type = `() => ${returnType}`;
2327
- }
2328
- }
2329
- if (!description && type && this.typedefs.has(type)) {
2330
- description = this.typedefs.get(type)?.description;
2331
- }
2332
- this.addProp(prop_name, {
2333
- name: prop_name,
2334
- kind,
2335
- description,
2336
- type,
2337
- value,
2338
- params,
2339
- returnType,
2340
- isFunction,
2341
- isFunctionDeclaration,
2342
- isRequired,
2343
- constant: kind === "const",
2344
- reactive: this.reactive_vars.has(prop_name),
2345
- });
2346
- }
2347
- /**
2348
- * Check for Comment node (Svelte-specific AST node type).
2349
- * Looks for HTML comments in the template that start with `@component`.
2350
- */
2351
- if (node && typeof node === "object" && "type" in node && String(node.type) === "Comment") {
2352
- const commentNode = node;
2353
- const data = commentNode?.data?.trim() ?? "";
2354
- if (COMPONENT_COMMENT_REGEX.test(data)) {
2355
- this.componentComment = data.replace(COMPONENT_COMMENT_REGEX, "").replace(CARRIAGE_RETURN_REGEX, "");
2356
- }
2357
- }
2358
- /**
2359
- * Check for Slot node (Svelte-specific AST node type).
2360
- * Extracts slot definitions from the template, including named slots,
2361
- * slot props, and fallback content.
2362
- */
2363
- if (node && typeof node === "object" && "type" in node && String(node.type) === "Slot") {
2364
- const slotNode = node;
2365
- const slot_name = slotNode.attributes?.find((attr) => attr.name === "name")?.value?.[0]?.data;
2366
- const slot_props = (slotNode.attributes || [])
2367
- .filter((attr) => attr.name !== "name")
2368
- .reduce((slot_props, attr) => {
2369
- const slot_prop_value = {
2370
- value: undefined,
2371
- replace: false,
2372
- };
2373
- const value = attr.value;
2374
- if (value === undefined)
2375
- return slot_props;
2376
- if (value[0]) {
2377
- const firstValue = value[0];
2378
- const { type, expression, raw, start, end } = firstValue;
2379
- if (type === "Text" && raw !== undefined) {
2380
- slot_prop_value.value = raw;
2381
- }
2382
- else if (type === "AttributeShorthand" &&
2383
- expression &&
2384
- typeof expression === "object" &&
2385
- "name" in expression) {
2386
- slot_prop_value.value = expression.name;
2387
- slot_prop_value.replace = true;
2388
- }
2389
- if (expression && typeof expression === "object" && "type" in expression) {
2390
- if (expression.type === "Literal" && "value" in expression) {
2391
- slot_prop_value.value = String(expression.value);
2392
- }
2393
- else if (expression.type === "MemberExpression") {
2394
- slot_prop_value.value = this.resolveMemberExpressionType(expression);
2395
- }
2396
- else if (expression.type !== "Identifier") {
2397
- if (start !== undefined && end !== undefined) {
2398
- if (expression.type === "ObjectExpression" || expression.type === "TemplateLiteral") {
2399
- slot_prop_value.value = this.sourceAtPos(start + 1, end - 1);
2400
- }
2401
- }
2402
- }
2403
- }
2404
- }
2405
- if (attr.name) {
2406
- slot_props[attr.name] = slot_prop_value;
2407
- }
2408
- return slot_props;
2409
- }, {});
2410
- const fallback = slotNode.children
2411
- ?.map(({ start, end }) => this.sourceAtPos(start, end))
2412
- .join("")
2413
- .trim();
2414
- this.addSlot({
2415
- slot_name,
2416
- slot_props: JSON.stringify(slot_props, null, 2),
2417
- slot_fallback: fallback,
2418
- });
2419
- }
2420
- /**
2421
- * Check for EventHandler node (Svelte-specific AST node type).
2422
- * Handles event forwarding syntax like `<Component on:click />` or `<button on:click />`.
2423
- * When an event handler has no expression, it means the event is being forwarded.
2424
- */
2425
- if (node && typeof node === "object" && "type" in node && String(node.type) === "EventHandler") {
2426
- const eventHandlerNode = node;
2427
- if (eventHandlerNode.expression == null && eventHandlerNode.name) {
2428
- if (parent != null && typeof parent === "object" && "name" in parent) {
2429
- const parentName = typeof parent.name === "string" ? parent.name : undefined;
2430
- const parentType = "type" in parent ? String(parent.type) : undefined;
2431
- if (parentName && parentType) {
2432
- /**
2433
- * Determine if parent is InlineComponent or Element.
2434
- * This tells us whether the event is forwarded from a component or native element.
2435
- */
2436
- const element = parentType === "InlineComponent"
2437
- ? { type: "InlineComponent", name: parentName }
2438
- : { type: "Element", name: parentName };
2439
- /**
2440
- * Track that this event is forwarded (we'll use this info later).
2441
- * This helps distinguish between dispatched and forwarded events during post-processing.
2442
- */
2443
- this.forwardedEvents.set(eventHandlerNode.name, element);
2444
- const existing_event = this.events.get(eventHandlerNode.name);
2445
- /**
2446
- * Check if this event has a JSDoc description from `@event` tags.
2447
- */
2448
- const description = this.eventDescriptions.get(eventHandlerNode.name);
2449
- const event_description = extractDescriptionAfterDash(description);
2450
- if (!existing_event) {
2451
- /**
2452
- * Add new forwarded event to the events map.
2453
- */
2454
- this.events.set(eventHandlerNode.name, {
2455
- type: "forwarded",
2456
- name: eventHandlerNode.name,
2457
- element: element,
2458
- description: event_description,
2459
- });
2460
- }
2461
- else if (existing_event.type === "forwarded" && event_description && !existing_event.description) {
2462
- /**
2463
- * Event is already forwarded, just add the description if it wasn't set before.
2464
- */
2465
- this.events.set(eventHandlerNode.name, {
2466
- ...existing_event,
2467
- description: event_description,
2468
- });
2469
- }
2470
- /**
2471
- * Note: if event is dispatched, we don't overwrite it here.
2472
- * We'll handle @event JSDoc on forwarded events after the walk completes
2473
- * to correctly convert dispatched events that are actually forwarded.
2474
- */
2475
- }
2476
- }
2477
- }
2478
- }
2479
- /**
2480
- * Check for Binding node (Svelte-specific AST node type).
2481
- * Handles element bindings like `bind:this={elementRef}` which change the
2482
- * prop type to include the element type (e.g., `HTMLButtonElement | null`).
2483
- */
2484
- if (parent &&
2485
- typeof parent === "object" &&
2486
- "type" in parent &&
2487
- String(parent.type) === "Element" &&
2488
- node &&
2489
- typeof node === "object" &&
2490
- "type" in node &&
2491
- String(node.type) === "Binding") {
2492
- const bindingNode = node;
2493
- const parentElement = parent;
2494
- if (bindingNode.name === "this" && bindingNode.expression?.name && parentElement.name) {
2495
- const prop_name = bindingNode.expression.name;
2496
- const element_name = parentElement.name;
2497
- if (this.bindings.has(prop_name)) {
2498
- const existing_bindings = this.bindings.get(prop_name);
2499
- if (existing_bindings && !existing_bindings.elements.includes(element_name)) {
2500
- this.bindings.set(prop_name, {
2501
- ...existing_bindings,
2502
- elements: [...existing_bindings.elements, element_name],
2503
- });
2504
- }
2505
- }
2506
- else {
2507
- this.bindings.set(prop_name, {
2508
- elements: [element_name],
2509
- });
2510
- }
2511
- }
2512
- }
2513
- },
2514
- });
2515
- if (dispatcher_name !== undefined) {
2516
- for (const callee of callees) {
2517
- if (callee.name === dispatcher_name) {
2518
- const firstArg = callee.arguments[0];
2519
- const event_name = firstArg && typeof firstArg === "object" && "value" in firstArg ? firstArg.value : undefined;
2520
- const event_argument = callee.arguments[1];
2521
- const event_detail = event_argument && typeof event_argument === "object" && "value" in event_argument
2522
- ? event_argument.value
2523
- : undefined;
2524
- if (event_name != null) {
2525
- this.addDispatchedEvent({
2526
- name: String(event_name),
2527
- detail: event_detail != null ? String(event_detail) : "",
2528
- has_argument: Boolean(event_argument),
2529
- });
2530
- }
2531
- }
2532
- }
2533
- }
2534
- /**
2535
- * Post-process events: convert dispatched events from @event JSDoc to forwarded events
2536
- * if they are actually forwarded and not dispatched via createEventDispatcher.
2537
- *
2538
- * This handles the case where an event is documented with @event but is actually
2539
- * forwarded via `on:eventname` syntax rather than dispatched. We need to check
2540
- * which events are actually dispatched and convert the rest to forwarded events.
2541
- */
2542
- const actuallyDispatchedEvents = new Set();
2543
- if (dispatcher_name !== undefined) {
2544
- for (const callee of callees) {
2545
- if (callee.name === dispatcher_name) {
2546
- const firstArg = callee.arguments[0];
2547
- const eventName = firstArg && typeof firstArg === "object" && "value" in firstArg ? firstArg.value : undefined;
2548
- if (eventName != null) {
2549
- actuallyDispatchedEvents.add(String(eventName));
2550
- }
2551
- }
2552
- }
2553
- }
2554
- this.forwardedEvents.forEach((element, eventName) => {
2555
- const event = this.events.get(eventName);
2556
- /**
2557
- * If event is marked as dispatched but is NOT actually dispatched, convert it to forwarded.
2558
- * This happens when @event JSDoc is used but the event is actually forwarded via on: syntax.
2559
- */
2560
- if (event && event.type === "dispatched" && !actuallyDispatchedEvents.has(eventName)) {
2561
- const description = this.eventDescriptions.get(eventName);
2562
- const event_description = extractDescriptionAfterDash(description);
2563
- const forwardedEvent = {
2564
- type: "forwarded",
2565
- name: eventName,
2566
- element: element,
2567
- description: event_description,
2568
- };
2569
- /**
2570
- * Preserve detail type if it was explicitly set in `@event` tag.
2571
- * Note: "null" is a valid explicit type (e.g., `@event {null} eventname`).
2572
- * Only skip if detail is truly undefined or the string "undefined".
2573
- */
2574
- if (event.detail !== undefined && event.detail !== "undefined") {
2575
- forwardedEvent.detail = event.detail;
2576
- }
2577
- this.events.set(eventName, forwardedEvent);
2578
- }
2579
- });
2580
- const processedProps = ComponentParser.mapToArray(this.props).map((prop) => {
2581
- if (this.bindings.has(prop.name)) {
2582
- const elementTypes = this.bindings
2583
- .get(prop.name)
2584
- ?.elements.sort()
2585
- .map((element) => (0, element_tag_map_1.getElementByTag)(element))
2586
- .join(" | ");
2587
- return {
2588
- ...prop,
2589
- type: `null | ${elementTypes}`,
2590
- };
2591
- }
2592
- return prop;
2593
- });
2594
- const processedSlots = ComponentParser.mapToArray(this.slots)
2595
- .map((slot) => {
2596
- try {
2597
- if (!slot.slot_props) {
2598
- return slot;
2599
- }
2600
- const slot_props = JSON.parse(slot.slot_props);
2601
- const new_props = [];
2602
- for (const key of Object.keys(slot_props)) {
2603
- if (slot_props[key].replace && slot_props[key].value !== undefined) {
2604
- slot_props[key].value = this.props.get(slot_props[key].value)?.type;
2605
- }
2606
- if (slot_props[key].value === undefined)
2607
- slot_props[key].value = "any";
2608
- new_props.push(`${key}: ${slot_props[key].value}`);
2609
- }
2610
- const formatted_slot_props = new_props.length === 0 ? "Record<string, never>" : `{ ${new_props.join(", ")} }`;
2611
- return { ...slot, slot_props: formatted_slot_props };
2612
- }
2613
- catch (_e) {
2614
- return slot;
2615
- }
2616
- })
2617
- .sort((a, b) => {
2618
- const aName = a.name ?? "";
2619
- const bName = b.name ?? "";
2620
- if (aName < bName)
2621
- return -1;
2622
- if (aName > bName)
2623
- return 1;
2624
- return 0;
2625
- });
2626
- const moduleExportsArray = ComponentParser.mapToArray(this.moduleExports);
2627
- /**
2628
- * Transform events for JSON serialization: convert element object to string for backward compatibility.
2629
- * The internal representation uses element objects, but JSON output uses strings for compatibility
2630
- * with older versions of sveld and external tools.
2631
- */
2632
- const eventsArray = ComponentParser.mapToArray(this.events).map((event) => {
2633
- if (event.type === "forwarded") {
2634
- return {
2635
- ...event,
2636
- element: event.element.name,
2637
- };
2638
- }
2639
- return event;
2640
- });
2641
- const typedefsArray = ComponentParser.mapToArray(this.typedefs);
2642
- const contextsArray = ComponentParser.mapToArray(this.contexts);
2643
- return {
2644
- props: processedProps,
2645
- moduleExports: moduleExportsArray,
2646
- slots: processedSlots,
2647
- events: eventsArray, // Type assertion: serialized format for JSON, but typed as ComponentEvent for API
2648
- typedefs: typedefsArray,
2649
- generics: this.generics,
2650
- rest_props: this.rest_props,
2651
- extends: this.extends,
2652
- componentComment: this.componentComment,
2653
- contexts: contextsArray,
2654
- };
2655
- }
2656
- }
2657
- exports.default = ComponentParser;