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
|
-
*
|
|
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<
|
|
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
|
package/lib/ComponentParser.d.ts
CHANGED
|
@@ -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.
|
package/lib/ComponentParser.js
CHANGED
|
@@ -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 =
|
|
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
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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 "
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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 ? "
|
|
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 : `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
408
|
+
${genSlotDef({ slots })}
|
|
377
409
|
> {
|
|
378
410
|
${genAccessors({ props })}
|
|
379
411
|
}`;
|