svelte-origin 0.0.0 → 1.0.0-next.15

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/LLM.md ADDED
@@ -0,0 +1,754 @@
1
+ # svelte-origin - AI/LLM Reference
2
+
3
+ This document provides comprehensive documentation for AI coding assistants working with `svelte-origin`, a compiler-assisted state and prop ergonomics library for Svelte 5.
4
+
5
+ ## Overview
6
+
7
+ `svelte-origin` provides compile-time macros that transform into standard Svelte 5 code:
8
+
9
+ - **`$origin({ ... })`** - Define reusable state + props + methods in `.svelte.ts` files
10
+ - **`$attrs({ ... })`** - Define the props schema inside an origin
11
+ - **`$bindable(value)`** - Mark a prop as bindable (two-way binding)
12
+ - **`$attrs.origin(Factory)`** - Create an origin instance from component props
13
+ - **`$attrs.for(Factory)`** - Forward props to child components
14
+ - **`$attrs.Of<typeof Factory>`** - Type helper to extract component props
15
+
16
+ ## Quick Reference
17
+
18
+ ### Origin Definition (`.svelte.ts` file)
19
+
20
+ ```ts
21
+ // counter.svelte.ts
22
+ export const Counter = $origin({
23
+ // Props schema - these become component props
24
+ props: $attrs({
25
+ label: 'Counter', // Default value: 'Counter'
26
+ count: $bindable(0), // Bindable with default 0
27
+ step: 1 as number, // Typed with default
28
+ optional: undefined as string | undefined // Optional prop
29
+ }),
30
+
31
+ // Private state (prefixed with _)
32
+ _clickCount: $state(0),
33
+
34
+ // Derived values (getters with $derived)
35
+ get double() {
36
+ return $derived(this.props.count * 2)
37
+ },
38
+
39
+ // Methods
40
+ increment() {
41
+ this._clickCount++
42
+ this.props.count += this.props.step
43
+ },
44
+
45
+ decrement() {
46
+ this.props.count -= this.props.step
47
+ },
48
+
49
+ reset() {
50
+ this.props.count = 0
51
+ }
52
+ })
53
+ ```
54
+
55
+ ### Component Usage (`.svelte` file)
56
+
57
+ ```svelte
58
+ <script lang="ts">
59
+ import { Counter } from './counter.svelte'
60
+
61
+ // Optional: Explicit $$Props for svelte-check compatibility
62
+ type $$Props = $attrs.Of<typeof Counter>
63
+
64
+ // Create instance from component props
65
+ let counter = $attrs.origin(Counter)
66
+ </script>
67
+
68
+ <h3>{counter.props.label}</h3>
69
+ <p>Count: {counter.props.count} (Double: {counter.double})</p>
70
+ <button onclick={() => counter.increment()}>+{counter.props.step}</button>
71
+ <button onclick={() => counter.decrement()}>-{counter.props.step}</button>
72
+ <button onclick={() => counter.reset()}>Reset</button>
73
+ ```
74
+
75
+ ### Parent Component Using Child
76
+
77
+ ```svelte
78
+ <script lang="ts">
79
+ import CounterComponent from './CounterComponent.svelte'
80
+ </script>
81
+
82
+ <!-- All props from Counter origin are available -->
83
+ <CounterComponent label="My Counter" count={10} step={5} />
84
+
85
+ <!-- Two-way binding works for $bindable props -->
86
+ <CounterComponent bind:count={myCount} />
87
+ ```
88
+
89
+ ## API Reference
90
+
91
+ ### `$origin(definition)`
92
+
93
+ Creates an origin factory from a definition object.
94
+
95
+ **Signature:**
96
+ ```ts
97
+ function $origin<T>(definition: T): OriginFactory<T>
98
+ function $origin<Parents extends OriginFactory[], T>(parents: Parents, definition: T): OriginFactory<Merged<Parents, T>>
99
+ ```
100
+
101
+ **Definition Object Properties:**
102
+
103
+ | Property | Description |
104
+ |----------|-------------|
105
+ | `props: $attrs({...})` | Props schema (becomes component props) |
106
+ | `attrs: $attrs({...})` | Alternative name for props schema |
107
+ | `_privateField` | Private state/method (not exposed on instance) |
108
+ | `get propertyName()` | Derived value (use `$derived` inside) |
109
+ | `methodName()` | Method accessible on the instance |
110
+
111
+ **Context (`this`) inside definition:**
112
+ - `this.props` - Access props (reactive)
113
+ - `this.super` - Parent instance (when extending)
114
+ - `this._privateField` - Access private state
115
+
116
+ ### `$attrs(schema)`
117
+
118
+ Defines the props schema inside an origin.
119
+
120
+ **Example:**
121
+ ```ts
122
+ props: $attrs({
123
+ // With default value (optional prop)
124
+ label: 'Default Label',
125
+
126
+ // Bindable with default
127
+ count: $bindable(0),
128
+
129
+ // Bindable without default (required)
130
+ value: $bindable(),
131
+
132
+ // Type annotation with default
133
+ step: 1 as number,
134
+
135
+ // Optional prop (no default)
136
+ optional: undefined as string | undefined,
137
+
138
+ // Required prop (typed, no default)
139
+ required: '' as string,
140
+ })
141
+ ```
142
+
143
+ ### `$bindable(defaultValue?)`
144
+
145
+ Marks a prop as bindable for two-way binding.
146
+
147
+ ```ts
148
+ props: $attrs({
149
+ // Bindable with default - optional prop
150
+ count: $bindable(0),
151
+
152
+ // Bindable without default - required prop
153
+ value: $bindable(),
154
+
155
+ // Bindable with typed default
156
+ text: $bindable('') as string,
157
+ })
158
+ ```
159
+
160
+ ### `$attrs.origin(Factory)`
161
+
162
+ Creates an origin instance from component props. Use inside `.svelte` components.
163
+
164
+ **Example:**
165
+ ```svelte
166
+ <script lang="ts">
167
+ import { Counter } from './counter.svelte'
168
+
169
+ let counter = $attrs.origin(Counter)
170
+ </script>
171
+ ```
172
+
173
+ **Transformation:**
174
+ ```svelte
175
+ <!-- Before (user writes) -->
176
+ let counter = $attrs.origin(Counter)
177
+
178
+ <!-- After (plugin transforms to) -->
179
+ let { label = 'Counter', count = $bindable(0), step = 1 } = $props()
180
+ let __attrs = { get label() { return label }, get count() { return count }, set count(v) { count = v }, get step() { return step } }
181
+ let counter = Counter(__attrs)
182
+ ```
183
+
184
+ ### `$attrs.for(Factory)`
185
+
186
+ Forwards props for child origin components.
187
+
188
+ ```svelte
189
+ <script lang="ts">
190
+ import { ChildOrigin } from './child.svelte'
191
+ import ChildComponent from './ChildComponent.svelte'
192
+
193
+ let childProps = $attrs.for(ChildOrigin)
194
+ </script>
195
+
196
+ <ChildComponent {...childProps} />
197
+ ```
198
+
199
+ ### `$attrs.for('element')`
200
+
201
+ Gets typed rest props for HTML element wrappers.
202
+
203
+ ```svelte
204
+ <script lang="ts">
205
+ let inputProps = $attrs.for('input')
206
+ </script>
207
+
208
+ <input {...inputProps} />
209
+ ```
210
+
211
+ ### `$attrs.Of<typeof Factory>`
212
+
213
+ Type helper to extract component props from an origin factory.
214
+
215
+ ```svelte
216
+ <script lang="ts">
217
+ import { Counter } from './counter.svelte'
218
+
219
+ // Explicit $$Props declaration for svelte-check
220
+ type $$Props = $attrs.Of<typeof Counter>
221
+
222
+ let counter = $attrs.origin(Counter)
223
+ </script>
224
+ ```
225
+
226
+ ## Origin Inheritance
227
+
228
+ Origins can extend other origins using array syntax:
229
+
230
+ ```ts
231
+ // base.svelte.ts
232
+ export const Counter = $origin({
233
+ props: $attrs({
234
+ label: 'Counter',
235
+ count: $bindable(0),
236
+ step: 1
237
+ }),
238
+
239
+ increment() {
240
+ this.props.count += this.props.step
241
+ },
242
+
243
+ decrement() {
244
+ this.props.count -= this.props.step
245
+ }
246
+ })
247
+
248
+ // extended.svelte.ts
249
+ import { Counter } from './base.svelte'
250
+
251
+ export const ExtendedCounter = $origin([Counter], {
252
+ props: $attrs({
253
+ bonus: 10 // Additional prop
254
+ }),
255
+
256
+ // Override method - call parent with this.super
257
+ increment() {
258
+ this.super.increment()
259
+ console.log('Incremented!')
260
+ },
261
+
262
+ // New method using parent
263
+ incrementWithBonus() {
264
+ this.super.increment()
265
+ this.super.props.count += this.props.bonus
266
+ },
267
+
268
+ // Delegate derived value
269
+ get double() {
270
+ return $derived(this.super.double)
271
+ }
272
+ })
273
+ ```
274
+
275
+ **Inheritance Rules:**
276
+ - Child inherits all parent props (merged with child's own props)
277
+ - Access parent instance via `this.super`
278
+ - Access parent props via `this.super.props`
279
+ - Child props are on `this.props`
280
+ - Override methods by redefining them
281
+ - Call parent method with `this.super.methodName()`
282
+
283
+ ## Init Callbacks
284
+
285
+ Origins can have an optional init callback that runs when the instance is created:
286
+
287
+ ```ts
288
+ export const Timer = $origin({
289
+ props: $attrs({
290
+ interval: 1000
291
+ }),
292
+
293
+ _count: $state(0),
294
+
295
+ get count() {
296
+ return $derived(this._count)
297
+ }
298
+ }, function() {
299
+ // Runs when the component is created
300
+ const id = setInterval(() => {
301
+ this._count++
302
+ }, this.props.interval)
303
+
304
+ // Return cleanup function (runs on unmount)
305
+ return () => clearInterval(id)
306
+ })
307
+ ```
308
+
309
+ **Init Callback Rules:**
310
+ - Second argument to `$origin()` (or third when extending)
311
+ - Runs after the instance is created
312
+ - Has access to `this` (full instance including private `_` prefixed members)
313
+ - Can return a cleanup function that runs when the component is destroyed
314
+ - Useful for side effects, subscriptions, or DOM interactions
315
+
316
+ ## Attachments
317
+
318
+ Origins can define **attachment methods** that run when an element is attached to the DOM. These are methods prefixed with `$` that receive the element and optionally return a cleanup function.
319
+
320
+ ### Defining Attachment Methods
321
+
322
+ ```ts
323
+ // widget.svelte.ts
324
+ export const Widget = $origin({
325
+ props: $attrs({
326
+ color: 'blue',
327
+ tooltipText: 'Hello'
328
+ }),
329
+
330
+ /** Attachment: adds a tooltip to an element */
331
+ $tooltip(element: Element) {
332
+ const tooltip = document.createElement('div')
333
+ tooltip.className = 'tooltip'
334
+ tooltip.textContent = this.props.tooltipText
335
+ element.appendChild(tooltip)
336
+
337
+ // Return cleanup function (runs on unmount or re-attach)
338
+ return () => tooltip.remove()
339
+ },
340
+
341
+ /** Attachment: highlights the element with the color prop */
342
+ $highlight(element: Element) {
343
+ const el = element as HTMLElement
344
+ const original = el.style.backgroundColor
345
+ el.style.backgroundColor = this.props.color
346
+ return () => { el.style.backgroundColor = original }
347
+ }
348
+ })
349
+ ```
350
+
351
+ **Key Points:**
352
+ - Attachment methods are prefixed with `$` (e.g., `$tooltip`, `$highlight`)
353
+ - They receive the DOM `Element` as their first argument
354
+ - They can access `this.props`, `this.super`, and other instance members
355
+ - Return a cleanup function to run on unmount or when the attachment re-runs
356
+ - They are NOT exposed on the public instance type
357
+
358
+ ### Using `$attachments`
359
+
360
+ Every origin instance has a `$attachments` getter that returns an object suitable for spreading onto elements:
361
+
362
+ ```svelte
363
+ <script lang="ts">
364
+ import { Widget } from './widget.svelte'
365
+ let widget = $attrs.origin(Widget)
366
+ </script>
367
+
368
+ <!-- All attachment methods ($tooltip, $highlight) are applied -->
369
+ <div {...widget.$attachments}>
370
+ Content with tooltip and highlight
371
+ </div>
372
+
373
+ <!-- Can also apply to multiple elements -->
374
+ <button {...widget.$attachments}>Button with attachments</button>
375
+ ```
376
+
377
+ ### Forwarding Incoming Attachments
378
+
379
+ When a parent component uses Svelte's `{@attach ...}` directive on your component, those attachments are automatically merged into `$attachments`:
380
+
381
+ ```svelte
382
+ <!-- Parent.svelte -->
383
+ <script lang="ts">
384
+ function logElement(el: Element) {
385
+ console.log('Attached to:', el)
386
+ return () => console.log('Detached from:', el)
387
+ }
388
+ </script>
389
+
390
+ <WidgetComponent {@attach logElement} color="red" />
391
+ ```
392
+
393
+ ```svelte
394
+ <!-- WidgetComponent.svelte -->
395
+ <script lang="ts">
396
+ import { Widget } from './widget.svelte'
397
+ let widget = $attrs.origin(Widget)
398
+ </script>
399
+
400
+ <!-- Both origin-defined ($tooltip, $highlight) AND incoming (logElement) attachments are applied -->
401
+ <div {...widget.$attachments}>...</div>
402
+ ```
403
+
404
+ **How it works:**
405
+ 1. Incoming attachments from `{@attach ...}` are passed as Symbol-keyed props
406
+ 2. The `$attachments` getter collects both origin-defined and incoming attachments
407
+ 3. Each attachment gets a unique Symbol key (via `createAttachmentKey()` from Svelte)
408
+ 4. When spread onto an element, Svelte's runtime registers all attachments
409
+
410
+ ### Attachment Type
411
+
412
+ ```ts
413
+ type AttachmentFn = (this: any, element: Element) => void | (() => void)
414
+ ```
415
+
416
+ The `$attachments` getter returns:
417
+ ```ts
418
+ type SvelteOriginAttachments = {
419
+ [key: symbol]: (element: Element) => void | (() => void)
420
+ }
421
+ ```
422
+
423
+ ## Project Setup
424
+
425
+ ### Installation
426
+
427
+ ```bash
428
+ npm install svelte-origin
429
+ # or
430
+ bun add svelte-origin
431
+ ```
432
+
433
+ ### Vite Configuration
434
+
435
+ ```ts
436
+ // vite.config.ts
437
+ import { sveltekit } from '@sveltejs/kit/vite'
438
+ import { svelteOrigin } from 'svelte-origin/plugin'
439
+
440
+ export default {
441
+ plugins: [
442
+ svelteOrigin(), // MUST come BEFORE sveltekit/svelte
443
+ sveltekit()
444
+ ]
445
+ }
446
+ ```
447
+
448
+ ### Svelte Configuration
449
+
450
+ ```js
451
+ // svelte.config.js
452
+ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
453
+ import { svelteOriginPreprocess } from 'svelte-origin/preprocess'
454
+
455
+ export default {
456
+ preprocess: [
457
+ svelteOriginPreprocess(), // MUST come BEFORE vitePreprocess
458
+ vitePreprocess()
459
+ ]
460
+ }
461
+ ```
462
+
463
+ ### TypeScript Configuration
464
+
465
+ ```json
466
+ {
467
+ "compilerOptions": {
468
+ "types": ["svelte-origin/globals"]
469
+ }
470
+ }
471
+ ```
472
+
473
+ ## File Naming Conventions
474
+
475
+ | File Pattern | Description |
476
+ |--------------|-------------|
477
+ | `*.svelte.ts` | Origin definition files (runes enabled) |
478
+ | `*.svelte` | Svelte components using origins |
479
+ | `*.origin.svelte.ts` | Alternative pattern for origin files |
480
+
481
+ ## Type System
482
+
483
+ ### Core Types
484
+
485
+ ```ts
486
+ import type {
487
+ SvelteOriginFactory,
488
+ SvelteOriginInstance,
489
+ SvelteOriginBindable,
490
+ SvelteOriginDefinition,
491
+ SvelteOriginComponentProps,
492
+ } from 'svelte-origin'
493
+ ```
494
+
495
+ ### Factory Type
496
+
497
+ ```ts
498
+ interface SvelteOriginFactory<TDef, TAllAttrs> {
499
+ (inputAttrs: TAllAttrs): SvelteOriginInstance<TDef>
500
+ readonly __origin: true
501
+ readonly __attrSchema: AttrSchema
502
+ readonly __parents: SvelteOriginFactory[]
503
+ }
504
+ ```
505
+
506
+ ### Instance Type
507
+
508
+ ```ts
509
+ type SvelteOriginInstance<T> = {
510
+ props: ReactiveAttrs<T['props']> // Or 'attrs' if that's what you named it
511
+ super?: ParentInstance
512
+ $attachments: SvelteOriginAttachments
513
+ } & PublicMembers<T>
514
+ ```
515
+
516
+ **Note:** The `props`/`attrs` property name matches what you use in your origin definition.
517
+
518
+ ## Common Patterns
519
+
520
+ ### Counter with Two-Way Binding
521
+
522
+ ```ts
523
+ // counter.svelte.ts
524
+ export const Counter = $origin({
525
+ props: $attrs({
526
+ count: $bindable(0)
527
+ }),
528
+
529
+ increment() { this.props.count++ },
530
+ decrement() { this.props.count-- }
531
+ })
532
+ ```
533
+
534
+ ```svelte
535
+ <!-- CounterComponent.svelte -->
536
+ <script lang="ts">
537
+ import { Counter } from './counter.svelte'
538
+ type $$Props = $attrs.Of<typeof Counter>
539
+ let counter = $attrs.origin(Counter)
540
+ </script>
541
+
542
+ <button onclick={() => counter.increment()}>{counter.props.count}</button>
543
+ ```
544
+
545
+ ```svelte
546
+ <!-- Parent using CounterComponent -->
547
+ <script lang="ts">
548
+ let count = $state(0)
549
+ </script>
550
+
551
+ <CounterComponent bind:count />
552
+ <p>Count in parent: {count}</p>
553
+ ```
554
+
555
+ ### Form Input Wrapper
556
+
557
+ ```ts
558
+ // input.svelte.ts
559
+ export const TextInput = $origin({
560
+ props: $attrs({
561
+ value: $bindable(''),
562
+ label: '',
563
+ placeholder: ''
564
+ }),
565
+
566
+ handleInput(e: Event) {
567
+ this.props.value = (e.target as HTMLInputElement).value
568
+ }
569
+ })
570
+ ```
571
+
572
+ ```svelte
573
+ <!-- TextInput.svelte -->
574
+ <script lang="ts">
575
+ import { TextInput } from './input.svelte'
576
+ type $$Props = $attrs.Of<typeof TextInput>
577
+ let input = $attrs.origin(TextInput)
578
+ </script>
579
+
580
+ <label>
581
+ {input.props.label}
582
+ <input
583
+ type="text"
584
+ value={input.props.value}
585
+ placeholder={input.props.placeholder}
586
+ oninput={(e) => input.handleInput(e)}
587
+ />
588
+ </label>
589
+ ```
590
+
591
+ ### Toggle with Derived State
592
+
593
+ ```ts
594
+ // toggle.svelte.ts
595
+ export const Toggle = $origin({
596
+ props: $attrs({
597
+ checked: $bindable(false),
598
+ label: 'Toggle'
599
+ }),
600
+
601
+ get labelText() {
602
+ return $derived(this.props.checked ? 'On' : 'Off')
603
+ },
604
+
605
+ toggle() {
606
+ this.props.checked = !this.props.checked
607
+ }
608
+ })
609
+ ```
610
+
611
+ ### List with Generic Types
612
+
613
+ ```ts
614
+ // list.svelte.ts
615
+ export const List = <T>() => $origin({
616
+ props: $attrs({
617
+ items: [] as T[],
618
+ selected: $bindable<T | null>(null)
619
+ }),
620
+
621
+ get isEmpty() {
622
+ return $derived(this.props.items.length === 0)
623
+ },
624
+
625
+ select(item: T) {
626
+ this.props.selected = item
627
+ },
628
+
629
+ clear() {
630
+ this.props.selected = null
631
+ }
632
+ })
633
+ ```
634
+
635
+ ```svelte
636
+ <!-- Usage with generics -->
637
+ <script lang="ts">
638
+ import { List } from './list.svelte'
639
+
640
+ type Item = { id: number; name: string }
641
+ const ItemList = List<Item>
642
+
643
+ type $$Props = $attrs.Of<ReturnType<typeof ItemList>>
644
+ let list = $attrs.origin(ItemList())
645
+ </script>
646
+ ```
647
+
648
+ ## Known Limitations
649
+
650
+ ### svelte-check Type Inference
651
+
652
+ The `svelte-check` CLI runs `svelte2tsx` on the **original** source, not the transformed output. This means:
653
+
654
+ - Components using `$attrs.origin()` may show type errors
655
+ - Runtime behavior is always correct
656
+ - Two-way binding and reactivity work as expected
657
+
658
+ **Workarounds:**
659
+ 1. Declare `type $$Props = $attrs.Of<typeof Factory>` explicitly
660
+ 2. Use `// @ts-expect-error` comments when needed
661
+ 3. Enable `outputTransformed: true` in plugin options for debugging
662
+
663
+ ### File Requirements
664
+
665
+ - Origin definitions must be in `.svelte.ts` files (runes enabled)
666
+ - Components using origins must import from `.svelte` or `.svelte.ts` extensions
667
+ - Plugin must be placed **before** svelte/sveltekit plugins
668
+
669
+ ## Debugging
670
+
671
+ ### Enable Debug Output
672
+
673
+ ```ts
674
+ // vite.config.ts
675
+ svelteOrigin({
676
+ debug: true,
677
+ outputTransformed: true // Writes .transformed.txt files
678
+ })
679
+ ```
680
+
681
+ ### View Transformed Code
682
+
683
+ With `outputTransformed: true`, the plugin writes `*.transformed.txt` files showing the macro expansion results.
684
+
685
+ ## CLI Commands
686
+
687
+ The `svelte-origin` CLI provides utilities for library authors and type generation.
688
+
689
+ ### generate-dts
690
+
691
+ Generate `.svelte.d.ts` declaration files for svelte-check compatibility:
692
+
693
+ ```bash
694
+ # Generate .d.ts files for all components using $attrs.origin()
695
+ svelte-origin generate-dts [src-dir]
696
+
697
+ # Show what would be generated without writing
698
+ svelte-origin generate-dts --dry-run
699
+
700
+ # Remove generated .d.ts files
701
+ svelte-origin generate-dts --clean
702
+ ```
703
+
704
+ ### post-process
705
+
706
+ Transform macros in svelte-package output for library distribution:
707
+
708
+ ```bash
709
+ # Run after svelte-package to transform macros in dist/
710
+ svelte-package && svelte-origin post-process [dist-dir]
711
+
712
+ # Preview what would be transformed
713
+ svelte-origin post-process --dry-run --verbose
714
+ ```
715
+
716
+ ## Package Exports
717
+
718
+ ```ts
719
+ // Main entry
720
+ import { transformScript, svelteOriginDts } from 'svelte-origin'
721
+
722
+ // Runtime functions
723
+ import { __createOrigin, bindable, __attrsFor } from 'svelte-origin/runtime'
724
+
725
+ // Vite/Rollup plugin
726
+ import { svelteOrigin } from 'svelte-origin/plugin'
727
+
728
+ // Svelte preprocessor
729
+ import { svelteOriginPreprocess } from 'svelte-origin/preprocess'
730
+
731
+ // Type globals (use in tsconfig.json)
732
+ // "types": ["svelte-origin/globals"]
733
+ ```
734
+
735
+ ## Architecture
736
+
737
+ ```
738
+ User Code Transformation Output
739
+ ───────── ────────────── ──────
740
+
741
+ $origin({ Vite Plugin __createOrigin({
742
+ props: $attrs({...}), ─────────────► __attrSchema: {...},
743
+ ... __create: (attrs) => {...}
744
+ }) })
745
+
746
+ $attrs.origin(Factory) Plugin + Preprocessor let {...} = $props()
747
+ ─────────────────────► let counter = Factory(...)
748
+ ```
749
+
750
+ The transformation ensures:
751
+ 1. Origins compile to factory functions with schema metadata
752
+ 2. Components use standard `$props()` for proper Svelte integration
753
+ 3. Runtime behavior matches Svelte 5 expectations
754
+ 4. Two-way binding works through `$bindable()` markers