sveld 0.22.5 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -62,7 +62,7 @@ export type ButtonProps = Omit<$RestProps, keyof $Props> & $Props;
62
62
  export default class Button extends SvelteComponentTyped<
63
63
  ButtonProps,
64
64
  { click: WindowEventMap["click"] },
65
- { default: {} }
65
+ { default: Record<string, never> }
66
66
  > {}
67
67
  ```
68
68
 
@@ -105,7 +105,7 @@ export type ButtonProps = Omit<$RestProps, keyof $Props> & $Props;
105
105
  export default class Button extends SvelteComponentTyped<
106
106
  ButtonProps,
107
107
  { click: WindowEventMap["click"] },
108
- { default: {} }
108
+ { default: Record<string, never> }
109
109
  > {}
110
110
  ```
111
111
 
@@ -126,6 +126,7 @@ export default class Button extends SvelteComponentTyped<
126
126
  - [@typedef](#typedef)
127
127
  - [@slot](#slot)
128
128
  - [@event](#event)
129
+ - [Context API](#context-api)
129
130
  - [@restProps](#restprops)
130
131
  - [@extends](#extends)
131
132
  - [@generics](#generics)
@@ -143,6 +144,7 @@ Extracted metadata include:
143
144
  - slots
144
145
  - forwarded events
145
146
  - dispatched events
147
+ - context (setContext/getContext)
146
148
  - `$$restProps`
147
149
 
148
150
  This library adopts a progressively enhanced approach. Any property type that cannot be inferred (e.g., "hello" is a string) falls back to "any" to minimize incorrectly typed properties or signatures. To mitigate this, the library author can add JSDoc annotations to specify types that cannot be reliably inferred. This represents a progressively enhanced approach because JSDocs are comments that can be ignored by the compiler.
@@ -364,6 +366,55 @@ export let author = {};
364
366
  export let authors = [];
365
367
  ```
366
368
 
369
+ #### Using `@property` for complex typedefs
370
+
371
+ For complex object types, use the `@property` tag to document individual properties. This provides better documentation and IDE support with per-property tooltips.
372
+
373
+ Signature:
374
+
375
+ ```js
376
+ /**
377
+ * Type description
378
+ * @typedef {object} TypeName
379
+ * @property {Type} propertyName - Property description
380
+ */
381
+ ```
382
+
383
+ Example:
384
+
385
+ ```js
386
+ /**
387
+ * Represents a user in the system
388
+ * @typedef {object} User
389
+ * @property {string} name - The user's full name
390
+ * @property {string} email - The user's email address
391
+ * @property {number} age - The user's age in years
392
+ */
393
+
394
+ /** @type {User} */
395
+ export let user = { name: "John", email: "john@example.com", age: 30 };
396
+ ```
397
+
398
+ Output:
399
+
400
+ ```ts
401
+ export type User = {
402
+ /** The user's full name */ name: string;
403
+ /** The user's email address */ email: string;
404
+ /** The user's age in years */ age: number;
405
+ };
406
+
407
+ export type ComponentProps = {
408
+ /**
409
+ * Represents a user in the system
410
+ * @default { name: "John", email: "john@example.com", age: 30 }
411
+ */
412
+ user?: User;
413
+ };
414
+ ```
415
+
416
+ > **Note:** The inline syntax `@typedef {{ name: string }} User` continues to work for backwards compatibility.
417
+
367
418
  ### `@slot`
368
419
 
369
420
  Use the `@slot` tag for typing component slots. Note that `@slot` is a non-standard JSDoc tag.
@@ -417,7 +468,8 @@ Signature:
417
468
 
418
469
  ```js
419
470
  /**
420
- * @event {EventDetail} eventname [event description]
471
+ * Optional event description
472
+ * @event {EventDetail} eventname [inline description]
421
473
  */
422
474
  ```
423
475
 
@@ -448,10 +500,230 @@ export default class Component extends SvelteComponentTyped<
448
500
  "button:key": CustomEvent<{ key: string }>;
449
501
  /** Fired when `key` changes. */ key: CustomEvent<null>;
450
502
  },
451
- {}
503
+ Record<string, never>
504
+ > {}
505
+ ```
506
+
507
+ #### Using `@property` for complex event details
508
+
509
+ For events with complex object payloads, use the `@property` tag to document individual properties. The main comment description will be used as the event description.
510
+
511
+ Signature:
512
+
513
+ ```js
514
+ /**
515
+ * Event description
516
+ * @event eventname
517
+ * @type {object}
518
+ * @property {Type} propertyName - Property description
519
+ */
520
+ ```
521
+
522
+ Example:
523
+
524
+ ```js
525
+ /**
526
+ * Fired when the user submits the form
527
+ *
528
+ * @event submit
529
+ * @type {object}
530
+ * @property {string} name - The user's name
531
+ * @property {string} email - The user's email address
532
+ * @property {boolean} newsletter - Whether the user opted into the newsletter
533
+ */
534
+
535
+ import { createEventDispatcher } from "svelte";
536
+
537
+ const dispatch = createEventDispatcher();
538
+
539
+ function handleSubmit() {
540
+ dispatch("submit", {
541
+ name: "Jane Doe",
542
+ email: "jane@example.com",
543
+ newsletter: true
544
+ });
545
+ }
546
+ ```
547
+
548
+ Output:
549
+
550
+ ```ts
551
+ export default class Component extends SvelteComponentTyped<
552
+ ComponentProps,
553
+ {
554
+ /** Fired when the user submits the form */
555
+ submit: CustomEvent<{
556
+ /** The user's name */ name: string;
557
+ /** The user's email address */ email: string;
558
+ /** Whether the user opted into the newsletter */ newsletter: boolean;
559
+ }>;
560
+ },
561
+ Record<string, never>
562
+ > {}
563
+ ```
564
+
565
+ ### Context API
566
+
567
+ `sveld` automatically generates TypeScript definitions for Svelte's `setContext`/`getContext` API by extracting types from JSDoc annotations on the context values.
568
+
569
+ #### How it works
570
+
571
+ When you use `setContext` in a component, `sveld` will:
572
+
573
+ 1. Detect the `setContext` call
574
+ 2. Extract the context key (must be a string literal)
575
+ 3. Find JSDoc `@type` annotations on the variables being passed
576
+ 4. Generate a TypeScript type export for the context
577
+
578
+ #### Example
579
+
580
+ **Modal.svelte**
581
+
582
+ ```svelte
583
+ <script>
584
+ import { setContext } from 'svelte';
585
+
586
+ /**
587
+ * Close the modal
588
+ * @type {() => void}
589
+ */
590
+ const close = () => {
591
+ // Close logic
592
+ };
593
+
594
+ /**
595
+ * Open the modal with content
596
+ * @type {(component: any, props?: any) => void}
597
+ */
598
+ const open = (component, props) => {
599
+ // Open logic
600
+ };
601
+
602
+ setContext('simple-modal', { open, close });
603
+ </script>
604
+
605
+ <div class="modal">
606
+ <slot />
607
+ </div>
608
+ ```
609
+
610
+ **Generated TypeScript definition:**
611
+
612
+ ```ts
613
+ export type SimpleModalContext = {
614
+ /** Open the modal with content */
615
+ open: (component: any, props?: any) => void;
616
+ /** Close the modal */
617
+ close: () => void;
618
+ };
619
+
620
+ export type ModalProps = {};
621
+
622
+ export default class Modal extends SvelteComponentTyped<
623
+ ModalProps,
624
+ Record<string, any>,
625
+ { default: Record<string, never> }
452
626
  > {}
453
627
  ```
454
628
 
629
+ **Consumer usage:**
630
+
631
+ ```svelte
632
+ <script>
633
+ import { getContext } from 'svelte';
634
+ import type { SimpleModalContext } from 'modal-library/Modal.svelte';
635
+
636
+ // Fully typed with autocomplete!
637
+ const { close, open } = getContext<SimpleModalContext>('simple-modal');
638
+ </script>
639
+
640
+ <button on:click={close}>Close</button>
641
+ ```
642
+
643
+ #### Explicitly typing contexts
644
+
645
+ There are several ways to provide type information for contexts:
646
+
647
+ **Option 1: Inline JSDoc on variables (recommended)**
648
+
649
+ ```svelte
650
+ <script>
651
+ import { setContext } from 'svelte';
652
+
653
+ /**
654
+ * @type {() => void}
655
+ */
656
+ const close = () => {};
657
+
658
+ setContext('modal', { close });
659
+ </script>
660
+ ```
661
+
662
+ **Option 2: Using @typedef for complex types**
663
+
664
+ ```svelte
665
+ <script>
666
+ import { setContext } from 'svelte';
667
+
668
+ /**
669
+ * @typedef {object} TabData
670
+ * @property {string} id
671
+ * @property {string} label
672
+ * @property {boolean} [disabled]
673
+ */
674
+
675
+ /**
676
+ * @type {(tab: TabData) => void}
677
+ */
678
+ const addTab = (tab) => {};
679
+
680
+ setContext('tabs', { addTab });
681
+ </script>
682
+ ```
683
+
684
+ **Option 3: Referencing imported types**
685
+
686
+ ```svelte
687
+ <script>
688
+ import { setContext } from 'svelte';
689
+
690
+ /**
691
+ * @type {typeof import("./types").ModalAPI}
692
+ */
693
+ const modalAPI = {
694
+ open: () => {},
695
+ close: () => {}
696
+ };
697
+
698
+ setContext('modal', modalAPI);
699
+ </script>
700
+ ```
701
+
702
+ **Option 4: Direct object literal with inline functions**
703
+
704
+ ```svelte
705
+ <script>
706
+ import { setContext } from 'svelte';
707
+
708
+ // sveld infers basic function signatures
709
+ setContext('modal', {
710
+ open: (component, props) => {}, // Inferred as (arg, arg) => any
711
+ close: () => {} // Inferred as () => any
712
+ });
713
+ </script>
714
+ ```
715
+
716
+ > **Note:** For best results, use explicit JSDoc `@type` annotations. Inline functions without annotations will be inferred with generic signatures.
717
+
718
+ #### Notes
719
+
720
+ - Context keys must be string literals (dynamic keys are not supported)
721
+ - Variables passed to `setContext` should have JSDoc `@type` annotations for accurate types
722
+ - The generated type name follows the pattern: `{PascalCase}Context`
723
+ - `"simple-modal"` → `SimpleModalContext`
724
+ - `"Tabs"` → `TabsContext`
725
+ - If no type annotation is found, the type defaults to `any` with a warning
726
+
455
727
  ### `@restProps`
456
728
 
457
729
  `sveld` can pick up inline HTML elements that `$$restProps` is forwarded to. However, it cannot infer the underlying element for instantiated components.
@@ -593,7 +865,11 @@ Output:
593
865
  * Text
594
866
  * </Button>
595
867
  */
596
- export default class Button extends SvelteComponentTyped<ButtonProps, {}, { default: {} }> {}
868
+ export default class Button extends SvelteComponentTyped<
869
+ ButtonProps,
870
+ Record<string, any>,
871
+ { default: Record<string, never> }
872
+ > {}
597
873
  ```
598
874
 
599
875
  ## Contributing
@@ -18,7 +18,7 @@ interface ComponentProp {
18
18
  reactive: boolean;
19
19
  }
20
20
  interface ComponentSlot {
21
- name?: string;
21
+ name?: string | null;
22
22
  default: boolean;
23
23
  fallback?: string;
24
24
  slot_props?: string;
@@ -58,6 +58,18 @@ interface Extends {
58
58
  interface: string;
59
59
  import: string;
60
60
  }
61
+ interface ComponentContextProp {
62
+ name: string;
63
+ type: string;
64
+ description?: string;
65
+ optional: boolean;
66
+ }
67
+ interface ComponentContext {
68
+ key: string;
69
+ typeName: string;
70
+ description?: string;
71
+ properties: ComponentContextProp[];
72
+ }
61
73
  export interface ParsedComponent {
62
74
  props: ComponentProp[];
63
75
  moduleExports: ComponentProp[];
@@ -68,6 +80,7 @@ export interface ParsedComponent {
68
80
  rest_props: RestProps;
69
81
  extends?: Extends;
70
82
  componentComment?: string;
83
+ contexts?: ComponentContext[];
71
84
  }
72
85
  export default class ComponentParser {
73
86
  private options?;
@@ -88,6 +101,7 @@ export default class ComponentParser {
88
101
  private readonly typedefs;
89
102
  private readonly generics;
90
103
  private readonly bindings;
104
+ private readonly contexts;
91
105
  constructor(options?: ComponentParserOptions);
92
106
  private static mapToArray;
93
107
  private static assignValue;
@@ -105,6 +119,11 @@ export default class ComponentParser {
105
119
  private addSlot;
106
120
  private addDispatchedEvent;
107
121
  private parseCustomTypes;
122
+ private buildEventDetailFromProperties;
123
+ private generateContextTypeName;
124
+ private findVariableTypeAndDescription;
125
+ private parseContextValue;
126
+ private parseSetContextCall;
108
127
  cleanup(): void;
109
128
  /**
110
129
  * Strips TypeScript directive comments from script blocks only.
@@ -38,7 +38,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
38
38
  const commentParser = __importStar(require("comment-parser"));
39
39
  const compiler_1 = require("svelte/compiler");
40
40
  const element_tag_map_1 = require("./element-tag-map");
41
- const DEFAULT_SLOT_NAME = "__default__";
41
+ const DEFAULT_SLOT_NAME = null;
42
42
  class ComponentParser {
43
43
  options;
44
44
  source;
@@ -58,6 +58,7 @@ class ComponentParser {
58
58
  typedefs = new Map();
59
59
  generics = null;
60
60
  bindings = new Map();
61
+ contexts = new Map();
61
62
  constructor(options) {
62
63
  this.options = options;
63
64
  }
@@ -182,7 +183,70 @@ class ComponentParser {
182
183
  }
183
184
  }
184
185
  parseCustomTypes() {
185
- commentParser.parse(this.source, { spacing: "preserve" }).forEach(({ tags }) => {
186
+ commentParser.parse(this.source, { spacing: "preserve" }).forEach(({ tags, description: commentDescription }) => {
187
+ let currentEventName;
188
+ let currentEventType;
189
+ let currentEventDescription;
190
+ const eventProperties = [];
191
+ let currentTypedefName;
192
+ let currentTypedefType;
193
+ let currentTypedefDescription;
194
+ const typedefProperties = [];
195
+ const finalizeEvent = () => {
196
+ if (currentEventName !== undefined) {
197
+ let detailType;
198
+ if (eventProperties.length > 0) {
199
+ detailType = this.buildEventDetailFromProperties(eventProperties, currentEventName);
200
+ }
201
+ else {
202
+ detailType = currentEventType || "";
203
+ }
204
+ this.addDispatchedEvent({
205
+ name: currentEventName,
206
+ detail: detailType,
207
+ has_argument: false,
208
+ description: currentEventDescription,
209
+ });
210
+ this.eventDescriptions.set(currentEventName, currentEventDescription);
211
+ eventProperties.length = 0;
212
+ currentEventName = undefined;
213
+ currentEventType = undefined;
214
+ currentEventDescription = undefined;
215
+ }
216
+ };
217
+ const finalizeTypedef = () => {
218
+ if (currentTypedefName !== undefined) {
219
+ let typedefType;
220
+ let typedefTs;
221
+ if (typedefProperties.length > 0) {
222
+ // Build type alias with property descriptions
223
+ typedefType = this.buildEventDetailFromProperties(typedefProperties);
224
+ typedefTs = `type ${currentTypedefName} = ${typedefType}`;
225
+ }
226
+ else if (currentTypedefType) {
227
+ // Use inline type definition (existing behavior)
228
+ typedefType = currentTypedefType;
229
+ typedefTs = /(\}|\};)$/.test(typedefType)
230
+ ? `interface ${currentTypedefName} ${typedefType}`
231
+ : `type ${currentTypedefName} = ${typedefType}`;
232
+ }
233
+ else {
234
+ // No type or properties specified, default to empty object
235
+ typedefType = "{}";
236
+ typedefTs = `type ${currentTypedefName} = ${typedefType}`;
237
+ }
238
+ this.typedefs.set(currentTypedefName, {
239
+ type: typedefType,
240
+ name: currentTypedefName,
241
+ description: ComponentParser.assignValue(currentTypedefDescription),
242
+ ts: typedefTs,
243
+ });
244
+ typedefProperties.length = 0;
245
+ currentTypedefName = undefined;
246
+ currentTypedefType = undefined;
247
+ currentTypedefDescription = undefined;
248
+ }
249
+ };
186
250
  tags.forEach(({ tag, type: tagType, name, description }) => {
187
251
  const type = this.aliasType(tagType);
188
252
  switch (tag) {
@@ -206,32 +270,250 @@ class ComponentParser {
206
270
  });
207
271
  break;
208
272
  case "event":
209
- // Store event metadata (description and detail) from JSDoc
210
- // This will be used later when we determine if the event is forwarded or dispatched
211
- this.eventDescriptions.set(name, description ? description : undefined);
212
- // For dispatched events, also store the detail type
213
- this.addDispatchedEvent({
214
- name,
215
- detail: type,
216
- has_argument: false,
217
- description: description ? description : undefined,
218
- });
273
+ // Finalize any previous event being built
274
+ finalizeEvent();
275
+ // Start tracking new event
276
+ currentEventName = name;
277
+ currentEventType = type;
278
+ // Use the main comment description if available, otherwise use inline description
279
+ currentEventDescription = commentDescription?.trim() || description || undefined;
219
280
  break;
220
- case "typedef":
221
- this.typedefs.set(name, {
222
- type,
223
- name,
224
- description: ComponentParser.assignValue(description),
225
- ts: /(\}|\};)$/.test(type) ? `interface ${name} ${type}` : `type ${name} = ${type}`,
226
- });
281
+ case "type":
282
+ // Track the @type tag for the current event
283
+ if (currentEventName !== undefined) {
284
+ currentEventType = type;
285
+ }
227
286
  break;
287
+ case "property":
288
+ // Collect properties for the current event or typedef
289
+ if (currentEventName !== undefined) {
290
+ eventProperties.push({
291
+ name,
292
+ type,
293
+ description: description?.replace(/^-\s*/, "").trim(),
294
+ });
295
+ }
296
+ else if (currentTypedefName !== undefined) {
297
+ typedefProperties.push({
298
+ name,
299
+ type,
300
+ description: description?.replace(/^-\s*/, "").trim(),
301
+ });
302
+ }
303
+ break;
304
+ case "typedef": {
305
+ // Finalize any previous typedef being built
306
+ finalizeTypedef();
307
+ // Start tracking new typedef
308
+ currentTypedefName = name;
309
+ currentTypedefType = type;
310
+ // Use inline description if present, otherwise use comment description
311
+ const trimmedCommentDesc = commentDescription?.trim();
312
+ currentTypedefDescription =
313
+ description || (trimmedCommentDesc && trimmedCommentDesc !== "}" ? trimmedCommentDesc : undefined);
314
+ break;
315
+ }
228
316
  case "generics":
229
317
  this.generics = [name, type];
230
318
  break;
231
319
  }
232
320
  });
321
+ // Finalize any remaining event or typedef
322
+ finalizeEvent();
323
+ finalizeTypedef();
233
324
  });
234
325
  }
326
+ buildEventDetailFromProperties(properties) {
327
+ if (properties.length === 0)
328
+ return "null";
329
+ // Build inline object type with property descriptions as JSDoc comments
330
+ const props = properties
331
+ .map(({ name, type, description }) => {
332
+ if (description) {
333
+ return `/** ${description} */ ${name}: ${type};`;
334
+ }
335
+ return `${name}: ${type};`;
336
+ })
337
+ .join(" ");
338
+ return `{ ${props} }`;
339
+ }
340
+ generateContextTypeName(key) {
341
+ // Convert "simple-modal" -> "SimpleModalContext"
342
+ // Convert "Tabs" -> "TabsContext"
343
+ const parts = key.split(/[-_\s]+/);
344
+ const capitalized = parts.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
345
+ return `${capitalized}Context`;
346
+ }
347
+ findVariableTypeAndDescription(varName) {
348
+ // Search through the source code directly for JSDoc comments
349
+ if (!this.source)
350
+ return null;
351
+ // Build a map of variable names to their types by looking at the source
352
+ const lines = this.source.split("\n");
353
+ for (let i = 0; i < lines.length; i++) {
354
+ const line = lines[i].trim();
355
+ // Check if this line declares our variable
356
+ // Match patterns like: const varName = ..., let varName = ..., function varName
357
+ const constMatch = line.match(new RegExp(`const\\s+${varName}\\s*=`));
358
+ const letMatch = line.match(new RegExp(`let\\s+${varName}\\s*=`));
359
+ const funcMatch = line.match(new RegExp(`function\\s+${varName}\\s*\\(`));
360
+ if (constMatch || letMatch || funcMatch) {
361
+ // Look backwards for JSDoc comment
362
+ for (let j = i - 1; j >= 0; j--) {
363
+ const prevLine = lines[j].trim();
364
+ // Stop if we hit a non-comment, non-empty line
365
+ if (prevLine && !prevLine.startsWith("*") && !prevLine.startsWith("/*") && !prevLine.startsWith("//")) {
366
+ break;
367
+ }
368
+ // Found start of JSDoc comment
369
+ if (prevLine.startsWith("/**")) {
370
+ // Extract the JSDoc comment block
371
+ const commentLines = [];
372
+ for (let k = j; k < i; k++) {
373
+ commentLines.push(lines[k]);
374
+ }
375
+ const commentBlock = commentLines.join("\n");
376
+ // Parse the JSDoc
377
+ const parsed = commentParser.parse(commentBlock, { spacing: "preserve" });
378
+ if (parsed[0]?.tags) {
379
+ const typeTag = parsed[0].tags.find((t) => t.tag === "type");
380
+ if (typeTag) {
381
+ return {
382
+ type: this.aliasType(typeTag.type),
383
+ description: parsed[0].description || typeTag.description,
384
+ };
385
+ }
386
+ }
387
+ break;
388
+ }
389
+ }
390
+ }
391
+ }
392
+ return null;
393
+ }
394
+ parseContextValue(node, key) {
395
+ if (node.type === "ObjectExpression") {
396
+ // Parse object literal: { open, close }
397
+ const properties = [];
398
+ for (const prop of node.properties) {
399
+ if (prop.type === "Property" || prop.type === "ObjectProperty") {
400
+ const propName = prop.key.name || prop.key.value;
401
+ // Try to find the variable definition to get its JSDoc type
402
+ let propType = "any";
403
+ let propDescription;
404
+ if (prop.value.type === "Identifier") {
405
+ const varName = prop.value.name;
406
+ const varInfo = this.findVariableTypeAndDescription(varName);
407
+ if (varInfo) {
408
+ propType = varInfo.type;
409
+ propDescription = varInfo.description;
410
+ }
411
+ else if (this.options?.verbose) {
412
+ console.warn(`Warning: Context "${key}" property "${propName}" has no type annotation. Using "any".`);
413
+ }
414
+ }
415
+ else if (prop.value.type === "ArrowFunctionExpression" || prop.value.type === "FunctionExpression") {
416
+ // Inline function
417
+ const params = prop.value.params.map((p) => `${p.name || "arg"}: any`).join(", ");
418
+ propType = `(${params}) => any`;
419
+ }
420
+ else if (prop.value.type === "Literal") {
421
+ // Literal value
422
+ propType = typeof prop.value.value;
423
+ }
424
+ properties.push({
425
+ name: propName,
426
+ type: propType,
427
+ description: propDescription,
428
+ optional: false,
429
+ });
430
+ }
431
+ }
432
+ return {
433
+ key,
434
+ typeName: this.generateContextTypeName(key),
435
+ properties,
436
+ description: undefined,
437
+ };
438
+ }
439
+ else if (node.type === "Identifier") {
440
+ // setContext('key', someVariable)
441
+ const varName = node.name;
442
+ const varInfo = this.findVariableTypeAndDescription(varName);
443
+ if (varInfo) {
444
+ return {
445
+ key,
446
+ typeName: this.generateContextTypeName(key),
447
+ properties: [
448
+ {
449
+ name: varName,
450
+ type: varInfo.type,
451
+ description: varInfo.description,
452
+ optional: false,
453
+ },
454
+ ],
455
+ };
456
+ }
457
+ else if (this.options?.verbose) {
458
+ console.warn(`Warning: Context "${key}" variable "${varName}" has no type annotation. Using "any".`);
459
+ }
460
+ // Still create context with 'any' type
461
+ return {
462
+ key,
463
+ typeName: this.generateContextTypeName(key),
464
+ properties: [
465
+ {
466
+ name: varName,
467
+ type: "any",
468
+ description: undefined,
469
+ optional: false,
470
+ },
471
+ ],
472
+ };
473
+ }
474
+ return null;
475
+ }
476
+ parseSetContextCall(node) {
477
+ // Extract context key (first argument)
478
+ const keyArg = node.arguments[0];
479
+ if (!keyArg)
480
+ return;
481
+ let contextKey = null;
482
+ if (keyArg.type === "Literal") {
483
+ contextKey = keyArg.value;
484
+ }
485
+ else if (keyArg.type === "TemplateLiteral") {
486
+ // Handle simple template literals
487
+ if (keyArg.quasis?.length === 1) {
488
+ contextKey = keyArg.quasis[0].value.cooked;
489
+ }
490
+ else if (this.options?.verbose) {
491
+ console.warn("Warning: Skipping setContext with dynamic template literal key");
492
+ }
493
+ }
494
+ else if (this.options?.verbose) {
495
+ console.warn(`Warning: Skipping setContext with non-literal key (type: ${keyArg.type})`);
496
+ }
497
+ if (!contextKey)
498
+ return;
499
+ // Extract context value (second argument)
500
+ const valueArg = node.arguments[1];
501
+ if (!valueArg)
502
+ return;
503
+ // Parse the context object
504
+ const contextInfo = this.parseContextValue(valueArg, contextKey);
505
+ if (contextInfo) {
506
+ // Check if context with same key already exists
507
+ if (this.contexts.has(contextKey)) {
508
+ if (this.options?.verbose) {
509
+ console.warn(`Warning: Multiple setContext calls with key "${contextKey}". Using first occurrence.`);
510
+ }
511
+ }
512
+ else {
513
+ this.contexts.set(contextKey, contextInfo);
514
+ }
515
+ }
516
+ }
235
517
  cleanup() {
236
518
  this.source = undefined;
237
519
  this.compiled = undefined;
@@ -249,6 +531,7 @@ class ComponentParser {
249
531
  this.typedefs.clear();
250
532
  this.generics = null;
251
533
  this.bindings.clear();
534
+ this.contexts.clear();
252
535
  }
253
536
  /**
254
537
  * Strips TypeScript directive comments from script blocks only.
@@ -356,6 +639,9 @@ class ComponentParser {
356
639
  if (node.callee.name === "createEventDispatcher") {
357
640
  dispatcher_name = parent?.id.name;
358
641
  }
642
+ if (node.callee.name === "setContext") {
643
+ this.parseSetContextCall(node, parent);
644
+ }
359
645
  callees.push({
360
646
  name: node.callee.name,
361
647
  arguments: node.arguments,
@@ -644,7 +930,7 @@ class ComponentParser {
644
930
  slot_props[key].value = "any";
645
931
  new_props.push(`${key}: ${slot_props[key].value}`);
646
932
  });
647
- const formatted_slot_props = new_props.length === 0 ? "{}" : `{ ${new_props.join(", ")} }`;
933
+ const formatted_slot_props = new_props.length === 0 ? "Record<string, never>" : `{ ${new_props.join(", ")} }`;
648
934
  return { ...slot, slot_props: formatted_slot_props };
649
935
  }
650
936
  catch (_e) {
@@ -664,6 +950,7 @@ class ComponentParser {
664
950
  rest_props: this.rest_props,
665
951
  extends: this.extends,
666
952
  componentComment: this.componentComment,
953
+ contexts: ComponentParser.mapToArray(this.contexts),
667
954
  };
668
955
  }
669
956
  }
@@ -42,7 +42,7 @@ const WriterMarkdown_1 = __importDefault(require("./WriterMarkdown"));
42
42
  const writer_ts_definitions_1 = require("./writer-ts-definitions");
43
43
  const PROP_TABLE_HEADER = `| Prop name | Required | Kind | Reactive | Type | Default value | Description |\n| :- | :- | :- | :- |\n`;
44
44
  const SLOT_TABLE_HEADER = `| Slot name | Default | Props | Fallback |\n| :- | :- | :- | :- |\n`;
45
- const EVENT_TABLE_HEADER = `| Event name | Type | Detail |\n| :- | :- | :- |\n`;
45
+ const EVENT_TABLE_HEADER = `| Event name | Type | Detail | Description |\n| :- | :- | :- | :- |\n`;
46
46
  const MD_TYPE_UNDEFINED = "--";
47
47
  function formatPropType(type) {
48
48
  if (type === undefined)
@@ -130,7 +130,7 @@ async function writeMarkdown(components, options) {
130
130
  if (component.events.length > 0) {
131
131
  document.append("raw", EVENT_TABLE_HEADER);
132
132
  component.events.forEach((event) => {
133
- document.append("raw", `| ${event.name} | ${event.type} | ${event.type === "dispatched" ? formatEventDetail(event.detail) : MD_TYPE_UNDEFINED} |\n`);
133
+ document.append("raw", `| ${event.name} | ${event.type} | ${event.type === "dispatched" ? formatEventDetail(event.detail) : MD_TYPE_UNDEFINED} | ${formatPropDescription(event.description)} |\n`);
134
134
  });
135
135
  }
136
136
  else {
@@ -2,6 +2,7 @@ import type { ParsedExports } from "../parse-exports";
2
2
  import type { ComponentDocApi, ComponentDocs } from "../rollup-plugin";
3
3
  export declare function formatTsProps(props?: string): string;
4
4
  export declare function getTypeDefs(def: Pick<ComponentDocApi, "typedefs">): string;
5
+ export declare function getContextDefs(def: Pick<ComponentDocApi, "contexts">): string;
5
6
  export declare function writeTsDefinition(component: ComponentDocApi): string;
6
7
  export interface WriteTsDefinitionsOptions {
7
8
  outDir: string;
@@ -38,6 +38,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.formatTsProps = formatTsProps;
40
40
  exports.getTypeDefs = getTypeDefs;
41
+ exports.getContextDefs = getContextDefs;
41
42
  exports.writeTsDefinition = writeTsDefinition;
42
43
  exports.default = writeTsDefinitions;
43
44
  const path = __importStar(require("node:path"));
@@ -47,6 +48,8 @@ const ANY_TYPE = "any";
47
48
  const EMPTY_STR = "";
48
49
  // Svelte 4 is not compatible with `{}`
49
50
  const EMPTY_EVENTS = "Record<string, any>";
51
+ // Avoid `{}` type per Biome linter rule noBannedTypes
52
+ const EMPTY_OBJECT = "Record<string, never>";
50
53
  function formatTsProps(props) {
51
54
  if (props === undefined)
52
55
  return ANY_TYPE;
@@ -57,9 +60,26 @@ function getTypeDefs(def) {
57
60
  return EMPTY_STR;
58
61
  return def.typedefs.map((typedef) => `export ${typedef.ts}`).join("\n\n");
59
62
  }
63
+ function getContextDefs(def) {
64
+ if (!def.contexts || def.contexts.length === 0)
65
+ return EMPTY_STR;
66
+ return def.contexts
67
+ .map((context) => {
68
+ const props = context.properties
69
+ .map((prop) => {
70
+ const comment = prop.description ? `/** ${prop.description} */\n ` : "";
71
+ const optional = prop.optional ? "?" : "";
72
+ return `${comment}${prop.name}${optional}: ${prop.type};`;
73
+ })
74
+ .join("\n ");
75
+ const contextComment = context.description ? `/**\n * ${context.description}\n */\n` : "";
76
+ return `${contextComment}export type ${context.typeName} = {\n ${props}\n};`;
77
+ })
78
+ .join("\n\n");
79
+ }
60
80
  function clampKey(key) {
61
81
  if (/(-|\s+|:)/.test(key)) {
62
- return /("|')/.test(key) ? key : `["${key}"]`;
82
+ return /("|')/.test(key) ? key : `"${key}"`;
63
83
  }
64
84
  return key;
65
85
  }
@@ -138,11 +158,19 @@ function genPropDef(def) {
138
158
  }
139
159
  }
140
160
  else {
141
- prop_def = `
161
+ // Use EMPTY_OBJECT when there are no props and no extends
162
+ if (props.trim() === "" && def.extends === undefined) {
163
+ prop_def = `
164
+ export type ${props_name}${genericsName} = ${EMPTY_OBJECT};
165
+ `;
166
+ }
167
+ else {
168
+ prop_def = `
142
169
  export type ${props_name}${genericsName} = ${def.extends !== undefined ? `${def.extends.interface} & ` : ""} {
143
170
  ${props}
144
171
  }
145
172
  `;
173
+ }
146
174
  }
147
175
  return {
148
176
  props_name,
@@ -150,13 +178,16 @@ function genPropDef(def) {
150
178
  };
151
179
  }
152
180
  function genSlotDef(def) {
153
- return def.slots
181
+ if (def.slots.length === 0)
182
+ return EMPTY_OBJECT;
183
+ const slotDefs = def.slots
154
184
  .map(({ name, slot_props, ...rest }) => {
155
- const key = rest.default ? "default" : clampKey(name ?? "");
185
+ const key = rest.default || name === null ? "default" : clampKey(name ?? "");
156
186
  const description = rest.description ? `/** ${rest.description} */\n` : "";
157
187
  return `${description}${clampKey(key)}: ${formatTsProps(slot_props)};`;
158
188
  })
159
189
  .join("\n");
190
+ return `{${slotDefs}}`;
160
191
  }
161
192
  const mapEvent = () => {
162
193
  // lib.dom.d.ts should map event types by name.
@@ -353,7 +384,7 @@ function genModuleExports(def) {
353
384
  .join("\n");
354
385
  }
355
386
  function writeTsDefinition(component) {
356
- const { moduleName, typedefs, generics, props, moduleExports, slots, events, rest_props, extends: _extends, componentComment, } = component;
387
+ const { moduleName, typedefs, generics, props, moduleExports, slots, events, rest_props, extends: _extends, componentComment, contexts, } = component;
357
388
  const { props_name, prop_def } = genPropDef({
358
389
  moduleName,
359
390
  props,
@@ -368,12 +399,13 @@ function writeTsDefinition(component) {
368
399
  ${genImports({ extends: _extends })}
369
400
  ${genModuleExports({ moduleExports })}
370
401
  ${getTypeDefs({ typedefs })}
402
+ ${getContextDefs({ contexts })}
371
403
  ${prop_def}
372
404
  ${genComponentComment({ componentComment })}
373
405
  export default class ${moduleName === "default" ? "" : moduleName}${generic} extends SvelteComponentTyped<
374
406
  ${genericProps},
375
407
  ${genEventDef({ events })},
376
- {${genSlotDef({ slots })}}
408
+ ${genSlotDef({ slots })}
377
409
  > {
378
410
  ${genAccessors({ props })}
379
411
  }`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sveld",
3
- "version": "0.22.5",
3
+ "version": "0.23.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Generate TypeScript definitions for your Svelte components.",
6
6
  "main": "./lib/index.js",