svelte-origin 1.0.0-next.48 → 1.0.0-next.49

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
@@ -1,678 +1,678 @@
1
- # svelte-origin
2
-
3
- Compiler-assisted state and prop ergonomics for **Svelte 5**.
4
-
5
- `svelte-origin` is intentionally **tooling-first**:
6
-
7
- - you write small, declarative “origins” using Svelte 5 runes (`$state`, `$derived`, ...)
8
- - you create instances explicitly **from props** (`$origin.component(...)`) or **programmatically** (`$origin.create(...)`)
9
- - you forward props ergonomically (`$origin.for(...)`)
10
-
11
- Everything "macro-like" compiles down to plain Svelte 5 code.
12
-
13
- ## Quick Overview
14
-
15
- | Macro | Purpose | Context |
16
- |-------|---------|--------|
17
- | `$origin({...})` | Define origin factory | `.svelte.ts` |
18
- | `$origin.props({...})` | Define props schema | Anywhere |
19
- | `$origin.component(F)` | Create from `$props()` | `.svelte` only |
20
- | `$origin.create(F, {...})` | Programmatic creation | ✅ Everywhere |
21
- | `$origin.bind(var)` | Two-way binding | In `$origin.create()` |
22
- | `$origin.for(F)` | Forward props | `.svelte` only |
23
- | `$origin.Props<T>` | Extract props type | Type context |
24
- | `$origin.Of<T>` | Extract instance type | Type context |
25
-
26
-
27
- ## Why this exists
28
-
29
- Svelte 5 already has great primitives.
30
-
31
- The pain is when you want:
32
-
33
- - a *single* place to describe a prop surface (including defaults + bindables)
34
- - predictable, explicit “create state from props” code
35
- - clean prop forwarding without manually spelling every prop
36
- - editor inference that matches what the build does
37
-
38
- `svelte-origin` solves this with a **Svelte preprocessor** (and optionally a Vite plugin).
39
-
40
- ## Installation
41
-
42
- ```bash
43
- npm install svelte-origin
44
- # or
45
- bun add svelte-origin
46
- ```
47
-
48
- ## Setup
49
-
50
- ### 1. Add the Vite plugin
51
-
52
- The plugin works with **Vite**, **Rollup**, and **Rolldown**. It transforms macros in both origin files (`.svelte.ts`) and Svelte components.
53
-
54
- ```ts
55
- // vite.config.ts
56
- import { sveltekit } from '@sveltejs/kit/vite'
57
- import { svelteOrigin } from 'svelte-origin/plugin'
58
-
59
- export default {
60
- plugins: [
61
- svelteOrigin(), // Must come BEFORE sveltekit/svelte
62
- sveltekit()
63
- ]
64
- }
65
- ```
66
-
67
- For **Rollup**:
68
-
69
- ```js
70
- // rollup.config.js
71
- import svelte from 'rollup-plugin-svelte'
72
- import { svelteOrigin } from 'svelte-origin/plugin'
73
-
74
- export default {
75
- plugins: [
76
- svelteOrigin(), // Must come BEFORE svelte
77
- svelte()
78
- ]
79
- }
80
- ```
81
-
82
- ### 2. Add the Svelte preprocessor (optional)
83
-
84
- The Vite/Rollup plugin handles all transformation. The preprocessor is **optional** — it only suppresses editor warnings about macro identifiers (since `$` is reserved for Svelte runes). If your editor shows warnings like "Unknown rune", add the preprocessor:
85
-
86
- ```js
87
- // svelte.config.js
88
- import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
89
- import { svelteOriginPreprocess } from 'svelte-origin/preprocess'
90
-
91
- export default {
92
- preprocess: [
93
- svelteOriginPreprocess(), // Must come BEFORE vitePreprocess
94
- vitePreprocess()
95
- ]
96
- }
97
- ```
98
-
99
- > **Note:** The Vite/Rollup plugin is **required** for transformation — the preprocessor alone is not sufficient.
100
-
101
- ### 3. Add TypeScript globals
102
-
103
- Add `svelte-origin/globals` to your `tsconfig.json` to enable types for `$origin`, `$origin.component`, etc.
104
-
105
- ```json
106
- {
107
- "compilerOptions": {
108
- "types": [
109
- "svelte-origin/globals"
110
- ]
111
- }
112
- }
113
- ```
114
-
115
- ### Plugin options
116
-
117
- ```ts
118
- svelteOrigin({
119
- // File extensions for origin definitions
120
- extensions: ['.svelte.ts', '.svelte.js', '.origin.svelte.ts'],
121
-
122
- // Enable debug logging
123
- debug: false,
124
-
125
- // Output transformed code to .transformed.txt files for debugging
126
- outputTransformed: false,
127
-
128
- // Directory for transformed output files (defaults to same as source)
129
- // The directory will be created automatically if it doesn't exist.
130
- // This setting is shared with the preprocessor, so both .svelte.ts and
131
- // .svelte files will output to the same directory.
132
- outputDir: undefined
133
- })
134
- ```
135
-
136
- ## The core idea
137
-
138
- ### 1) Define an origin
139
-
140
- Write origins in a file where runes are allowed (e.g. `.svelte.ts`).
141
-
142
- ```ts
143
- // counter.svelte.ts
144
- export const Counter = $origin({
145
- props: $origin.props({
146
- /** The label for the counter */
147
- label: 'Counter',
148
- /** The current count value (bindable for two-way binding) */
149
- count: $bindable(0),
150
- /** Step value for increment/decrement (optional) */
151
- step: 1 as number
152
- }),
153
-
154
- /** Internal counter (private - not exposed) */
155
- _incrementCount: $state(0),
156
-
157
- /** Get double the current count */
158
- get double() {
159
- return $derived(this.props.count * 2)
160
- },
161
-
162
- /** Increment by step */
163
- increment() {
164
- this._incrementCount++
165
- this.props.count += this.props.step
166
- },
167
-
168
- /** Decrement by step */
169
- decrement() {
170
- this.props.count -= this.props.step
171
- },
172
-
173
- /** Reset to zero */
174
- reset() {
175
- this.props.count = 0
176
- }
177
- })
178
- ```
179
-
180
- Notes:
181
-
182
- - `$origin(...)` is a **compile-time macro** provided by `svelte-origin` (not a Svelte rune)
183
- - `props: $origin.props({...})` defines the component props schema
184
- - compiles into the origin's prop schema with defaults and bindable handling
185
- - the property name (`props`) is the recommended convention
186
- - `$bindable(...)` inside `props: $origin.props({...})` marks props for two-way binding
187
- - `$state`/`$derived` are real Svelte runes
188
- - Properties prefixed with `_` (like `_incrementCount`) are private and not exposed on the instance type
189
-
190
- ### 2) Create an instance from component props
191
-
192
- Inside a component, be explicit that the instance is derived from the component's props:
193
-
194
- ```svelte
195
- <!-- CounterComponent.svelte -->
196
- <script lang="ts">
197
- import { Counter } from './counter.svelte'
198
-
199
- // Create the counter instance from component props
200
- let counter = $origin.component(Counter)
201
- </script>
202
-
203
- <div class="counter">
204
- <h3>{counter.props.label}</h3>
205
- <p>{counter.props.count}</p>
206
- <p>Double: {counter.double}</p>
207
- <div class="controls">
208
- <button onclick={() => counter.decrement()}>-{counter.props.step}</button>
209
- <button onclick={() => counter.reset()}>Reset</button>
210
- <button onclick={() => counter.increment()}>+{counter.props.step}</button>
211
- </div>
212
- </div>
213
- ```
214
-
215
- Key points:
216
-
217
- - We do **not** rely on "calling `Counter()` magically reads `$props()`"
218
- - We make the props dependency explicit via `$origin.component(Counter)`
219
- - For manual type declarations, use `$origin.Props<typeof Counter>`
220
-
221
- Under the hood, the macro expands to canonical `$props()` usage, so the component's prop types remain inferable.
222
-
223
- ## Passing to subcomponents
224
-
225
- The rule of thumb is:
226
-
227
- - each component owns its own origin instance
228
- - parents forward values via props
229
-
230
- We make forwarding ergonomic using `$origin.for(...)`. This helper forwards the intersection of the parent's props and the child's origin requirements.
231
-
232
- > ⚠️ **Note:** `$origin.for()` only works in `.svelte` files. It throws an error if used in `.svelte.ts` module files.
233
-
234
- ```svelte
235
- <script lang='ts'>
236
- let childProps = $origin.for(ChildOrigin)
237
- </script>
238
-
239
- <Child {...childProps} />
240
- ```
241
-
242
- Notes:
243
-
244
- - `$origin.for(...)` is intentionally about forwarding **from `$props()`**, not from an origin instance
245
-
246
- ## Wrapper components / element props
247
-
248
- If you’re forwarding attributes onto a native element, the type you want is an **attribute type**, not `HTMLInputElement`.
249
-
250
- Svelte’s recommended types live in `svelte/elements`.
251
-
252
- To make this ergonomic, we can provide:
253
-
254
- ```svelte
255
- <script lang='ts'>
256
- let inputProps = $origin.for('input')
257
- </script>
258
-
259
- <input {...inputProps} />
260
- ```
261
-
262
- (`$origin.for` is a macro that expands to a typed `$props()` + rest pattern.)
263
-
264
-
265
-
266
- ## Extending origins
267
-
268
- You can extend existing origins by passing them as an array in the first argument. To access the parent implementation, use `this.super`.
269
-
270
- ```ts
271
- export const Counter = $origin({
272
- // ... base implementation
273
- increment() {
274
- this.props.count += 1
275
- }
276
- })
277
-
278
- export const SuperCounter = $origin([Counter], {
279
- increment() {
280
- // Call the parent implementation
281
- this.super.increment()
282
- console.log('Incremented!')
283
- }
284
- })
285
- ```
286
-
287
- ## Programmatic Origin Creation
288
-
289
- Use `$origin.create()` to create origin instances **programmatically**. Unlike `$origin.component()`, this works **everywhere**:
290
-
291
- - ✅ `.svelte` component scripts
292
- - ✅ `.svelte.ts` / `.svelte.js` module files
293
- - ✅ Inside `$origin()` init callbacks
294
- - ✅ Inside origin method bodies
295
- - ✅ Module scripts (`<script module>`)
296
-
297
- ### Basic Usage
298
-
299
- ```ts
300
- import { Counter } from './counter.svelte'
301
-
302
- // Create an origin instance with initial prop values
303
- const counter = $origin.create(Counter, { count: 10, step: 2 })
304
-
305
- // Reading props works
306
- counter.props.count // ✅ Returns 10
307
- counter.increment() // ✅ Methods work
308
-
309
- // Without props (uses defaults)
310
- const defaultCounter = $origin.create(Counter)
311
- ```
312
-
313
- > **Note:** Props passed directly to `$origin.create()` are **read-only** (getter-only). If you need to set props externally, use `$origin.bind()` to create two-way bindings.
314
-
315
- ### Binding to Existing State (Two-Way Reactivity)
316
-
317
- Use `$origin.bind()` to sync an origin's prop with an existing reactive variable. **This is the only way to get settable props** with `$origin.create()`:
318
-
319
- ```ts
320
- let count = $state(10)
321
-
322
- const counter = $origin.create(Counter, {
323
- count: $origin.bind(count) // Two-way binding
324
- })
325
-
326
- // Both are now in sync:
327
- counter.props.count = 20 // ✅ count is now 20
328
- count = 5 // counter.props.count is now 5
329
- counter.increment() // ✅ Both update
330
- ```
331
-
332
- Mix bound and unbound props as needed:
333
-
334
- ```ts
335
- let sharedCount = $state(0)
336
-
337
- const counter = $origin.create(Counter, {
338
- count: $origin.bind(sharedCount), // Two-way (settable)
339
- label: 'My Counter', // Read-only
340
- step: 2 // Read-only
341
- })
342
- ```
343
-
344
- ### Why Not Call the Factory Directly?
345
-
346
- **Never call the factory directly** — it bypasses the transform and breaks reactivity:
347
-
348
- ```ts
349
- // ❌ BAD - Bypasses macro transformation
350
- const counter = Counter({ count: 0 })
351
- // Props are plain object - no reactivity at all!
352
-
353
- // ✅ GOOD - Use $origin.create() for proper transformation
354
- const counter = $origin.create(Counter, { count: 0 })
355
- // Props are getter-based and can read from reactive sources
356
- ```
357
-
358
- **To set props externally**, you must bind to a reactive variable:
359
-
360
- ```ts
361
- let count = $state(0)
362
-
363
- const counter = $origin.create(Counter, {
364
- count: $origin.bind(count) // Now settable!
365
- })
366
-
367
- counter.props.count = 5 // ✅ Works - updates `count` too
368
- ```
369
-
370
- ## Attachments
371
-
372
- 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.
373
-
374
- ### Defining attachments
375
-
376
- ```ts
377
- // widget.svelte.ts
378
- export const Widget = $origin({
379
- props: $origin.props({
380
- color: 'blue'
381
- }),
382
-
383
- /** Attachment: adds a tooltip to an element */
384
- $tooltip(element: Element) {
385
- const tooltip = document.createElement('div')
386
- tooltip.textContent = 'Tooltip for ' + this.props.color
387
- element.appendChild(tooltip)
388
-
389
- // Return cleanup function
390
- return () => tooltip.remove()
391
- },
392
-
393
- /** Attachment: highlights the element */
394
- $highlight(element: Element) {
395
- element.classList.add('highlighted')
396
- return () => element.classList.remove('highlighted')
397
- }
398
- })
399
- ```
400
-
401
- ### Using attachments
402
-
403
- Spread `$attachments` onto any element to apply all defined attachment behaviors:
404
-
405
- ```svelte
406
- <script lang="ts">
407
- import { Widget } from './widget.svelte'
408
- let widget = $origin.component(Widget)
409
- </script>
410
-
411
- <!-- All attachment methods are applied to this div -->
412
- <div {...widget.$attachments}>
413
- Content with tooltip and highlight
414
- </div>
415
- ```
416
-
417
- ### Forwarding incoming attachments
418
-
419
- When a parent uses Svelte's `{@attach ...}` directive on your component, those attachments are automatically included in `$attachments`:
420
-
421
- ```svelte
422
- <!-- Parent.svelte -->
423
- <WidgetComponent {@attach myCustomAttachment} />
424
- ```
425
-
426
- ```svelte
427
- <!-- WidgetComponent.svelte -->
428
- <script lang="ts">
429
- import { Widget } from './widget.svelte'
430
- let widget = $origin.component(Widget)
431
- </script>
432
-
433
- <!-- Both origin-defined AND incoming attachments are applied -->
434
- <div {...widget.$attachments}>...</div>
435
- ```
436
-
437
- This enables composable attachment behavior where parents can add behaviors to child component elements.
438
-
439
- ## Init Callbacks
440
-
441
- Origins support an optional init callback that runs when the origin instance is created:
442
-
443
- ```ts
444
- export const Timer = $origin({
445
- props: $origin.props({
446
- interval: 1000
447
- }),
448
-
449
- _count: $state(0),
450
-
451
- get count() {
452
- return $derived(this._count)
453
- }
454
- }, function() {
455
- // Runs when the component is created
456
- const id = setInterval(() => {
457
- this._count++
458
- }, this.props.interval)
459
-
460
- // Return cleanup function (runs on unmount)
461
- return () => clearInterval(id)
462
- })
463
- ```
464
-
465
- The callback:
466
- - Runs after the instance is created
467
- - Has access to `this` (the full instance including private members)
468
- - Can return a cleanup function that runs when the component is destroyed
469
-
470
- ### Origins Without Props (State-Only Origins)
471
-
472
- Origins don't require props — you can define pure internal state:
473
-
474
- ```ts
475
- // simple.svelte.ts
476
- export const SimpleCounter = $origin({
477
- count: $state(0),
478
- increment() { this.count++ },
479
- decrement() { this.count-- },
480
- reset() { this.count = 0 }
481
- })
482
- ```
483
-
484
- ### Creating Child Origins
485
-
486
- When an origin needs to create other origin instances, **always use `$origin.create()`**:
487
-
488
- ```ts
489
- // Manager that creates child origins
490
- export const Manager = $origin({
491
- name: $state('Manager'),
492
-
493
- // Pattern 1: Inline initialization
494
- childCounter: $origin.create(SimpleCounter),
495
-
496
- // Pattern 2: Lazy initialization (typed with $origin.Of)
497
- lazyChild: $state(undefined) as undefined | $origin.Of<typeof SimpleCounter>
498
- }, function() {
499
- // Pattern 3: Init callback initialization - ALSO use $origin.create()
500
- this.lazyChild = $origin.create(SimpleCounter)
501
- })
502
- ```
503
-
504
- > **Critical:** Never call the factory directly (e.g., `SimpleCounter({})`). This loses reactivity. Always use `$origin.create()` — it gets transformed at compile time.
505
-
506
- ### Type Utilities
507
-
508
- ```ts
509
- import { Counter } from './counter.svelte'
510
-
511
- // Extract props type (for component prop declarations)
512
- type CounterProps = $origin.Props<typeof Counter>
513
-
514
- // Extract instance type (for variable declarations)
515
- type CounterInstance = $origin.Of<typeof Counter>
516
- let counter: $origin.Of<typeof Counter>
517
- ```
518
-
519
- ```svelte
520
- <script lang="ts">
521
- import { SimpleCounter, Manager } from './simple.svelte'
522
-
523
- // Create instances using $origin.create() for proper reactivity
524
- const counter = $origin.create(SimpleCounter)
525
- const manager = $origin.create(Manager)
526
- </script>
527
-
528
- <!-- SimpleCounter (no props) -->
529
- <p>count = {counter.count}</p>
530
- <button onclick={() => counter.increment()}>Increment</button>
531
-
532
- <!-- Manager (with child created in init) -->
533
- <p>manager.name = {manager.name}</p>
534
- {#if manager.childCounter}
535
- <p>childCounter.count = {manager.childCounter.count}</p>
536
- <button onclick={() => manager.childCounter?.increment()}>Increment Child</button>
537
- {/if}
538
- ```
539
-
540
- ### `$origin.component()` vs `$origin.create()`
541
-
542
- | | `$origin.component()` | `$origin.create()` |
543
- |---|---|---|
544
- | **Use in** | `.svelte` files only | ✅ Everywhere |
545
- | **Props source** | Component's `$props()` | Passed explicitly or omitted |
546
- | **Prop mutability** | Bindable props are settable | Read-only unless using `$origin.bind()` |
547
- | **Two-way binding** | Tied to parent via `$bindable()` props | Use `$origin.bind(variable)` |
548
- | **Typical use** | Component state management | Child origins, tests, modules |
549
- | **Module file behavior** | ❌ Throws error | ✅ Works |
550
-
551
- ```svelte
552
- <!-- Component: uses $origin.component() -->
553
- <script lang="ts">
554
- import { Counter } from './counter.svelte'
555
-
556
- // Props flow from parent component → origin instance
557
- let counter = $origin.component(Counter)
558
- </script>
559
- ```
560
-
561
- ```ts
562
- // Origin file: uses $origin.create()
563
- import { SimpleCounter } from './simple.svelte'
564
-
565
- export const Manager = $origin({
566
- // Child is an independent reactive instance
567
- child: $origin.create(SimpleCounter)
568
- })
569
- ```
570
-
571
- ## Origin Destructuring
572
-
573
- Destructure methods and props from origin instances with automatic handling:
574
-
575
- ### Method Destructuring
576
-
577
- Methods are automatically bound to preserve `this` context:
578
-
579
- ```svelte
580
- <script lang="ts">
581
- import { Counter } from './counter.svelte'
582
- let counter = $origin.component(Counter)
583
-
584
- // Destructure methods - automatically bound
585
- let { increment, decrement } = counter
586
- </script>
587
-
588
- <button onclick={increment}>+</button>
589
- <button onclick={decrement}>-</button>
590
- ```
591
-
592
- ### Props Destructuring
593
-
594
- Props are automatically wrapped in `$derived` for reactivity:
595
-
596
- ```svelte
597
- <script lang="ts">
598
- import { Counter } from './counter.svelte'
599
- let counter = $origin.component(Counter)
600
-
601
- // Destructure props - automatically reactive
602
- let { count, label } = counter.props
603
- </script>
604
-
605
- <p>{label}: {count}</p>
606
- ```
607
-
608
- ### Nested Destructuring
609
-
610
- Combine method and props destructuring:
611
-
612
- ```svelte
613
- <script lang="ts">
614
- let { increment, props: { count, label } } = counter
615
- </script>
616
- ```
617
-
618
- ### Aliases and Defaults
619
-
620
- ```svelte
621
- <script lang="ts">
622
- let { increment: inc } = counter
623
- let { count: counterValue = 0 } = counter.props
624
- </script>
625
- ```
626
-
627
- ## CLI (Library Authors)
628
-
629
- For library authors, the CLI transforms macros in `svelte-package` output and **eliminates the svelte-origin runtime dependency**.
630
-
631
- ### Zero-Dependency Output (Default)
632
-
633
- By default, the CLI inlines the minimal runtime (~4KB minified) into your library's dist folder:
634
-
635
- ```bash
636
- svelte-package && svelte-origin
637
- ```
638
-
639
- This means **your library consumers only need Svelte** - no svelte-origin dependency required. The CLI:
640
-
641
- 1. Transforms any remaining macros in the dist output
642
- 2. Replaces `svelte-origin/runtime` imports with a local inline runtime
643
- 3. Writes `__svelte-origin-runtime.js` to your dist folder
644
-
645
- ### Keeping svelte-origin as Dependency
646
-
647
- If you want consumers to use the full svelte-origin package (e.g., for advanced features or smaller bundle size when multiple libraries use svelte-origin), use `--dependency`:
648
-
649
- ```bash
650
- svelte-package && svelte-origin --dependency
651
- ```
652
-
653
- ### CLI Options
654
-
655
- ```bash
656
- svelte-origin [command] [options]
657
-
658
- Commands:
659
- post-process Post-process svelte-package output (default)
660
- generate-dts Generate .svelte.d.ts files for svelte-check
661
- help Show help for a command
662
-
663
- Post-process options:
664
- -h, --help Show help
665
- -n, --dry-run Show what would be transformed without writing
666
- -v, --verbose Show detailed transformation info
667
- -d, --dependency Keep svelte-origin as a runtime dependency (skip inlining)
668
-
669
- Examples:
670
- svelte-package && svelte-origin # Default: inline runtime
671
- svelte-package && svelte-origin dist # Specify directory
672
- svelte-package && svelte-origin --dependency # Keep dependency
673
- svelte-origin help post-process # Show post-process help
674
- ```
675
-
676
- ## License
677
-
678
- MIT
1
+ # svelte-origin
2
+
3
+ Compiler-assisted state and prop ergonomics for **Svelte 5**.
4
+
5
+ `svelte-origin` is intentionally **tooling-first**:
6
+
7
+ - you write small, declarative “origins” using Svelte 5 runes (`$state`, `$derived`, ...)
8
+ - you create instances explicitly **from props** (`$origin.component(...)`) or **programmatically** (`$origin.create(...)`)
9
+ - you forward props ergonomically (`$origin.for(...)`)
10
+
11
+ Everything "macro-like" compiles down to plain Svelte 5 code.
12
+
13
+ ## Quick Overview
14
+
15
+ | Macro | Purpose | Context |
16
+ |-------|---------|--------|
17
+ | `$origin({...})` | Define origin factory | `.svelte.ts` |
18
+ | `$origin.props({...})` | Define props schema | Anywhere |
19
+ | `$origin.component(F)` | Create from `$props()` | `.svelte` only |
20
+ | `$origin.create(F, {...})` | Programmatic creation | ✅ Everywhere |
21
+ | `$origin.bind(var)` | Two-way binding | In `$origin.create()` |
22
+ | `$origin.for(F)` | Forward props | `.svelte` only |
23
+ | `$origin.Props<T>` | Extract props type | Type context |
24
+ | `$origin.Of<T>` | Extract instance type | Type context |
25
+
26
+
27
+ ## Why this exists
28
+
29
+ Svelte 5 already has great primitives.
30
+
31
+ The pain is when you want:
32
+
33
+ - a *single* place to describe a prop surface (including defaults + bindables)
34
+ - predictable, explicit “create state from props” code
35
+ - clean prop forwarding without manually spelling every prop
36
+ - editor inference that matches what the build does
37
+
38
+ `svelte-origin` solves this with a **Svelte preprocessor** (and optionally a Vite plugin).
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ npm install svelte-origin
44
+ # or
45
+ bun add svelte-origin
46
+ ```
47
+
48
+ ## Setup
49
+
50
+ ### 1. Add the Vite plugin
51
+
52
+ The plugin works with **Vite**, **Rollup**, and **Rolldown**. It transforms macros in both origin files (`.svelte.ts`) and Svelte components.
53
+
54
+ ```ts
55
+ // vite.config.ts
56
+ import { sveltekit } from '@sveltejs/kit/vite'
57
+ import { svelteOrigin } from 'svelte-origin/plugin'
58
+
59
+ export default {
60
+ plugins: [
61
+ svelteOrigin(), // Must come BEFORE sveltekit/svelte
62
+ sveltekit()
63
+ ]
64
+ }
65
+ ```
66
+
67
+ For **Rollup**:
68
+
69
+ ```js
70
+ // rollup.config.js
71
+ import svelte from 'rollup-plugin-svelte'
72
+ import { svelteOrigin } from 'svelte-origin/plugin'
73
+
74
+ export default {
75
+ plugins: [
76
+ svelteOrigin(), // Must come BEFORE svelte
77
+ svelte()
78
+ ]
79
+ }
80
+ ```
81
+
82
+ ### 2. Add the Svelte preprocessor (optional)
83
+
84
+ The Vite/Rollup plugin handles all transformation. The preprocessor is **optional** — it only suppresses editor warnings about macro identifiers (since `$` is reserved for Svelte runes). If your editor shows warnings like "Unknown rune", add the preprocessor:
85
+
86
+ ```js
87
+ // svelte.config.js
88
+ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
89
+ import { svelteOriginPreprocess } from 'svelte-origin/preprocess'
90
+
91
+ export default {
92
+ preprocess: [
93
+ svelteOriginPreprocess(), // Must come BEFORE vitePreprocess
94
+ vitePreprocess()
95
+ ]
96
+ }
97
+ ```
98
+
99
+ > **Note:** The Vite/Rollup plugin is **required** for transformation — the preprocessor alone is not sufficient.
100
+
101
+ ### 3. Add TypeScript globals
102
+
103
+ Add `svelte-origin/globals` to your `tsconfig.json` to enable types for `$origin`, `$origin.component`, etc.
104
+
105
+ ```json
106
+ {
107
+ "compilerOptions": {
108
+ "types": [
109
+ "svelte-origin/globals"
110
+ ]
111
+ }
112
+ }
113
+ ```
114
+
115
+ ### Plugin options
116
+
117
+ ```ts
118
+ svelteOrigin({
119
+ // File extensions for origin definitions
120
+ extensions: ['.svelte.ts', '.svelte.js', '.origin.svelte.ts'],
121
+
122
+ // Enable debug logging
123
+ debug: false,
124
+
125
+ // Output transformed code to .transformed.txt files for debugging
126
+ outputTransformed: false,
127
+
128
+ // Directory for transformed output files (defaults to same as source)
129
+ // The directory will be created automatically if it doesn't exist.
130
+ // This setting is shared with the preprocessor, so both .svelte.ts and
131
+ // .svelte files will output to the same directory.
132
+ outputDir: undefined
133
+ })
134
+ ```
135
+
136
+ ## The core idea
137
+
138
+ ### 1) Define an origin
139
+
140
+ Write origins in a file where runes are allowed (e.g. `.svelte.ts`).
141
+
142
+ ```ts
143
+ // counter.svelte.ts
144
+ export const Counter = $origin({
145
+ props: $origin.props({
146
+ /** The label for the counter */
147
+ label: 'Counter',
148
+ /** The current count value (bindable for two-way binding) */
149
+ count: $bindable(0),
150
+ /** Step value for increment/decrement (optional) */
151
+ step: 1 as number
152
+ }),
153
+
154
+ /** Internal counter (private - not exposed) */
155
+ _incrementCount: $state(0),
156
+
157
+ /** Get double the current count */
158
+ get double() {
159
+ return $derived(this.props.count * 2)
160
+ },
161
+
162
+ /** Increment by step */
163
+ increment() {
164
+ this._incrementCount++
165
+ this.props.count += this.props.step
166
+ },
167
+
168
+ /** Decrement by step */
169
+ decrement() {
170
+ this.props.count -= this.props.step
171
+ },
172
+
173
+ /** Reset to zero */
174
+ reset() {
175
+ this.props.count = 0
176
+ }
177
+ })
178
+ ```
179
+
180
+ Notes:
181
+
182
+ - `$origin(...)` is a **compile-time macro** provided by `svelte-origin` (not a Svelte rune)
183
+ - `props: $origin.props({...})` defines the component props schema
184
+ - compiles into the origin's prop schema with defaults and bindable handling
185
+ - the property name (`props`) is the recommended convention
186
+ - `$bindable(...)` inside `props: $origin.props({...})` marks props for two-way binding
187
+ - `$state`/`$derived` are real Svelte runes
188
+ - Properties prefixed with `_` (like `_incrementCount`) are private and not exposed on the instance type
189
+
190
+ ### 2) Create an instance from component props
191
+
192
+ Inside a component, be explicit that the instance is derived from the component's props:
193
+
194
+ ```svelte
195
+ <!-- CounterComponent.svelte -->
196
+ <script lang="ts">
197
+ import { Counter } from './counter.svelte'
198
+
199
+ // Create the counter instance from component props
200
+ let counter = $origin.component(Counter)
201
+ </script>
202
+
203
+ <div class="counter">
204
+ <h3>{counter.props.label}</h3>
205
+ <p>{counter.props.count}</p>
206
+ <p>Double: {counter.double}</p>
207
+ <div class="controls">
208
+ <button onclick={() => counter.decrement()}>-{counter.props.step}</button>
209
+ <button onclick={() => counter.reset()}>Reset</button>
210
+ <button onclick={() => counter.increment()}>+{counter.props.step}</button>
211
+ </div>
212
+ </div>
213
+ ```
214
+
215
+ Key points:
216
+
217
+ - We do **not** rely on "calling `Counter()` magically reads `$props()`"
218
+ - We make the props dependency explicit via `$origin.component(Counter)`
219
+ - For manual type declarations, use `$origin.Props<typeof Counter>`
220
+
221
+ Under the hood, the macro expands to canonical `$props()` usage, so the component's prop types remain inferable.
222
+
223
+ ## Passing to subcomponents
224
+
225
+ The rule of thumb is:
226
+
227
+ - each component owns its own origin instance
228
+ - parents forward values via props
229
+
230
+ We make forwarding ergonomic using `$origin.for(...)`. This helper forwards the intersection of the parent's props and the child's origin requirements.
231
+
232
+ > ⚠️ **Note:** `$origin.for()` only works in `.svelte` files. It throws an error if used in `.svelte.ts` module files.
233
+
234
+ ```svelte
235
+ <script lang='ts'>
236
+ let childProps = $origin.for(ChildOrigin)
237
+ </script>
238
+
239
+ <Child {...childProps} />
240
+ ```
241
+
242
+ Notes:
243
+
244
+ - `$origin.for(...)` is intentionally about forwarding **from `$props()`**, not from an origin instance
245
+
246
+ ## Wrapper components / element props
247
+
248
+ If you’re forwarding attributes onto a native element, the type you want is an **attribute type**, not `HTMLInputElement`.
249
+
250
+ Svelte’s recommended types live in `svelte/elements`.
251
+
252
+ To make this ergonomic, we can provide:
253
+
254
+ ```svelte
255
+ <script lang='ts'>
256
+ let inputProps = $origin.for('input')
257
+ </script>
258
+
259
+ <input {...inputProps} />
260
+ ```
261
+
262
+ (`$origin.for` is a macro that expands to a typed `$props()` + rest pattern.)
263
+
264
+
265
+
266
+ ## Extending origins
267
+
268
+ You can extend existing origins by passing them as an array in the first argument. To access the parent implementation, use `this.super`.
269
+
270
+ ```ts
271
+ export const Counter = $origin({
272
+ // ... base implementation
273
+ increment() {
274
+ this.props.count += 1
275
+ }
276
+ })
277
+
278
+ export const SuperCounter = $origin([Counter], {
279
+ increment() {
280
+ // Call the parent implementation
281
+ this.super.increment()
282
+ console.log('Incremented!')
283
+ }
284
+ })
285
+ ```
286
+
287
+ ## Programmatic Origin Creation
288
+
289
+ Use `$origin.create()` to create origin instances **programmatically**. Unlike `$origin.component()`, this works **everywhere**:
290
+
291
+ - ✅ `.svelte` component scripts
292
+ - ✅ `.svelte.ts` / `.svelte.js` module files
293
+ - ✅ Inside `$origin()` init callbacks
294
+ - ✅ Inside origin method bodies
295
+ - ✅ Module scripts (`<script module>`)
296
+
297
+ ### Basic Usage
298
+
299
+ ```ts
300
+ import { Counter } from './counter.svelte'
301
+
302
+ // Create an origin instance with initial prop values
303
+ const counter = $origin.create(Counter, { count: 10, step: 2 })
304
+
305
+ // Reading props works
306
+ counter.props.count // ✅ Returns 10
307
+ counter.increment() // ✅ Methods work
308
+
309
+ // Without props (uses defaults)
310
+ const defaultCounter = $origin.create(Counter)
311
+ ```
312
+
313
+ > **Note:** Props passed directly to `$origin.create()` are **read-only** (getter-only). If you need to set props externally, use `$origin.bind()` to create two-way bindings.
314
+
315
+ ### Binding to Existing State (Two-Way Reactivity)
316
+
317
+ Use `$origin.bind()` to sync an origin's prop with an existing reactive variable. **This is the only way to get settable props** with `$origin.create()`:
318
+
319
+ ```ts
320
+ let count = $state(10)
321
+
322
+ const counter = $origin.create(Counter, {
323
+ count: $origin.bind(count) // Two-way binding
324
+ })
325
+
326
+ // Both are now in sync:
327
+ counter.props.count = 20 // ✅ count is now 20
328
+ count = 5 // counter.props.count is now 5
329
+ counter.increment() // ✅ Both update
330
+ ```
331
+
332
+ Mix bound and unbound props as needed:
333
+
334
+ ```ts
335
+ let sharedCount = $state(0)
336
+
337
+ const counter = $origin.create(Counter, {
338
+ count: $origin.bind(sharedCount), // Two-way (settable)
339
+ label: 'My Counter', // Read-only
340
+ step: 2 // Read-only
341
+ })
342
+ ```
343
+
344
+ ### Why Not Call the Factory Directly?
345
+
346
+ **Never call the factory directly** — it bypasses the transform and breaks reactivity:
347
+
348
+ ```ts
349
+ // ❌ BAD - Bypasses macro transformation
350
+ const counter = Counter({ count: 0 })
351
+ // Props are plain object - no reactivity at all!
352
+
353
+ // ✅ GOOD - Use $origin.create() for proper transformation
354
+ const counter = $origin.create(Counter, { count: 0 })
355
+ // Props are getter-based and can read from reactive sources
356
+ ```
357
+
358
+ **To set props externally**, you must bind to a reactive variable:
359
+
360
+ ```ts
361
+ let count = $state(0)
362
+
363
+ const counter = $origin.create(Counter, {
364
+ count: $origin.bind(count) // Now settable!
365
+ })
366
+
367
+ counter.props.count = 5 // ✅ Works - updates `count` too
368
+ ```
369
+
370
+ ## Attachments
371
+
372
+ 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.
373
+
374
+ ### Defining attachments
375
+
376
+ ```ts
377
+ // widget.svelte.ts
378
+ export const Widget = $origin({
379
+ props: $origin.props({
380
+ color: 'blue'
381
+ }),
382
+
383
+ /** Attachment: adds a tooltip to an element */
384
+ $tooltip(element: Element) {
385
+ const tooltip = document.createElement('div')
386
+ tooltip.textContent = 'Tooltip for ' + this.props.color
387
+ element.appendChild(tooltip)
388
+
389
+ // Return cleanup function
390
+ return () => tooltip.remove()
391
+ },
392
+
393
+ /** Attachment: highlights the element */
394
+ $highlight(element: Element) {
395
+ element.classList.add('highlighted')
396
+ return () => element.classList.remove('highlighted')
397
+ }
398
+ })
399
+ ```
400
+
401
+ ### Using attachments
402
+
403
+ Spread `$attachments` onto any element to apply all defined attachment behaviors:
404
+
405
+ ```svelte
406
+ <script lang="ts">
407
+ import { Widget } from './widget.svelte'
408
+ let widget = $origin.component(Widget)
409
+ </script>
410
+
411
+ <!-- All attachment methods are applied to this div -->
412
+ <div {...widget.$attachments}>
413
+ Content with tooltip and highlight
414
+ </div>
415
+ ```
416
+
417
+ ### Forwarding incoming attachments
418
+
419
+ When a parent uses Svelte's `{@attach ...}` directive on your component, those attachments are automatically included in `$attachments`:
420
+
421
+ ```svelte
422
+ <!-- Parent.svelte -->
423
+ <WidgetComponent {@attach myCustomAttachment} />
424
+ ```
425
+
426
+ ```svelte
427
+ <!-- WidgetComponent.svelte -->
428
+ <script lang="ts">
429
+ import { Widget } from './widget.svelte'
430
+ let widget = $origin.component(Widget)
431
+ </script>
432
+
433
+ <!-- Both origin-defined AND incoming attachments are applied -->
434
+ <div {...widget.$attachments}>...</div>
435
+ ```
436
+
437
+ This enables composable attachment behavior where parents can add behaviors to child component elements.
438
+
439
+ ## Init Callbacks
440
+
441
+ Origins support an optional init callback that runs when the origin instance is created:
442
+
443
+ ```ts
444
+ export const Timer = $origin({
445
+ props: $origin.props({
446
+ interval: 1000
447
+ }),
448
+
449
+ _count: $state(0),
450
+
451
+ get count() {
452
+ return $derived(this._count)
453
+ }
454
+ }, function() {
455
+ // Runs when the component is created
456
+ const id = setInterval(() => {
457
+ this._count++
458
+ }, this.props.interval)
459
+
460
+ // Return cleanup function (runs on unmount)
461
+ return () => clearInterval(id)
462
+ })
463
+ ```
464
+
465
+ The callback:
466
+ - Runs after the instance is created
467
+ - Has access to `this` (the full instance including private members)
468
+ - Can return a cleanup function that runs when the component is destroyed
469
+
470
+ ### Origins Without Props (State-Only Origins)
471
+
472
+ Origins don't require props — you can define pure internal state:
473
+
474
+ ```ts
475
+ // simple.svelte.ts
476
+ export const SimpleCounter = $origin({
477
+ count: $state(0),
478
+ increment() { this.count++ },
479
+ decrement() { this.count-- },
480
+ reset() { this.count = 0 }
481
+ })
482
+ ```
483
+
484
+ ### Creating Child Origins
485
+
486
+ When an origin needs to create other origin instances, **always use `$origin.create()`**:
487
+
488
+ ```ts
489
+ // Manager that creates child origins
490
+ export const Manager = $origin({
491
+ name: $state('Manager'),
492
+
493
+ // Pattern 1: Inline initialization
494
+ childCounter: $origin.create(SimpleCounter),
495
+
496
+ // Pattern 2: Lazy initialization (typed with $origin.Of)
497
+ lazyChild: $state(undefined) as undefined | $origin.Of<typeof SimpleCounter>
498
+ }, function() {
499
+ // Pattern 3: Init callback initialization - ALSO use $origin.create()
500
+ this.lazyChild = $origin.create(SimpleCounter)
501
+ })
502
+ ```
503
+
504
+ > **Critical:** Never call the factory directly (e.g., `SimpleCounter({})`). This loses reactivity. Always use `$origin.create()` — it gets transformed at compile time.
505
+
506
+ ### Type Utilities
507
+
508
+ ```ts
509
+ import { Counter } from './counter.svelte'
510
+
511
+ // Extract props type (for component prop declarations)
512
+ type CounterProps = $origin.Props<typeof Counter>
513
+
514
+ // Extract instance type (for variable declarations)
515
+ type CounterInstance = $origin.Of<typeof Counter>
516
+ let counter: $origin.Of<typeof Counter>
517
+ ```
518
+
519
+ ```svelte
520
+ <script lang="ts">
521
+ import { SimpleCounter, Manager } from './simple.svelte'
522
+
523
+ // Create instances using $origin.create() for proper reactivity
524
+ const counter = $origin.create(SimpleCounter)
525
+ const manager = $origin.create(Manager)
526
+ </script>
527
+
528
+ <!-- SimpleCounter (no props) -->
529
+ <p>count = {counter.count}</p>
530
+ <button onclick={() => counter.increment()}>Increment</button>
531
+
532
+ <!-- Manager (with child created in init) -->
533
+ <p>manager.name = {manager.name}</p>
534
+ {#if manager.childCounter}
535
+ <p>childCounter.count = {manager.childCounter.count}</p>
536
+ <button onclick={() => manager.childCounter?.increment()}>Increment Child</button>
537
+ {/if}
538
+ ```
539
+
540
+ ### `$origin.component()` vs `$origin.create()`
541
+
542
+ | | `$origin.component()` | `$origin.create()` |
543
+ |---|---|---|
544
+ | **Use in** | `.svelte` files only | ✅ Everywhere |
545
+ | **Props source** | Component's `$props()` | Passed explicitly or omitted |
546
+ | **Prop mutability** | Bindable props are settable | Read-only unless using `$origin.bind()` |
547
+ | **Two-way binding** | Tied to parent via `$bindable()` props | Use `$origin.bind(variable)` |
548
+ | **Typical use** | Component state management | Child origins, tests, modules |
549
+ | **Module file behavior** | ❌ Throws error | ✅ Works |
550
+
551
+ ```svelte
552
+ <!-- Component: uses $origin.component() -->
553
+ <script lang="ts">
554
+ import { Counter } from './counter.svelte'
555
+
556
+ // Props flow from parent component → origin instance
557
+ let counter = $origin.component(Counter)
558
+ </script>
559
+ ```
560
+
561
+ ```ts
562
+ // Origin file: uses $origin.create()
563
+ import { SimpleCounter } from './simple.svelte'
564
+
565
+ export const Manager = $origin({
566
+ // Child is an independent reactive instance
567
+ child: $origin.create(SimpleCounter)
568
+ })
569
+ ```
570
+
571
+ ## Origin Destructuring
572
+
573
+ Destructure methods and props from origin instances with automatic handling:
574
+
575
+ ### Method Destructuring
576
+
577
+ Methods are automatically bound to preserve `this` context:
578
+
579
+ ```svelte
580
+ <script lang="ts">
581
+ import { Counter } from './counter.svelte'
582
+ let counter = $origin.component(Counter)
583
+
584
+ // Destructure methods - automatically bound
585
+ let { increment, decrement } = counter
586
+ </script>
587
+
588
+ <button onclick={increment}>+</button>
589
+ <button onclick={decrement}>-</button>
590
+ ```
591
+
592
+ ### Props Destructuring
593
+
594
+ Props are automatically wrapped in `$derived` for reactivity:
595
+
596
+ ```svelte
597
+ <script lang="ts">
598
+ import { Counter } from './counter.svelte'
599
+ let counter = $origin.component(Counter)
600
+
601
+ // Destructure props - automatically reactive
602
+ let { count, label } = counter.props
603
+ </script>
604
+
605
+ <p>{label}: {count}</p>
606
+ ```
607
+
608
+ ### Nested Destructuring
609
+
610
+ Combine method and props destructuring:
611
+
612
+ ```svelte
613
+ <script lang="ts">
614
+ let { increment, props: { count, label } } = counter
615
+ </script>
616
+ ```
617
+
618
+ ### Aliases and Defaults
619
+
620
+ ```svelte
621
+ <script lang="ts">
622
+ let { increment: inc } = counter
623
+ let { count: counterValue = 0 } = counter.props
624
+ </script>
625
+ ```
626
+
627
+ ## CLI (Library Authors)
628
+
629
+ For library authors, the CLI transforms macros in `svelte-package` output and **eliminates the svelte-origin runtime dependency**.
630
+
631
+ ### Zero-Dependency Output (Default)
632
+
633
+ By default, the CLI inlines the minimal runtime (~4KB minified) into your library's dist folder:
634
+
635
+ ```bash
636
+ svelte-package && svelte-origin
637
+ ```
638
+
639
+ This means **your library consumers only need Svelte** - no svelte-origin dependency required. The CLI:
640
+
641
+ 1. Transforms any remaining macros in the dist output
642
+ 2. Replaces `svelte-origin/runtime` imports with a local inline runtime
643
+ 3. Writes `__svelte-origin-runtime.js` to your dist folder
644
+
645
+ ### Keeping svelte-origin as Dependency
646
+
647
+ If you want consumers to use the full svelte-origin package (e.g., for advanced features or smaller bundle size when multiple libraries use svelte-origin), use `--dependency`:
648
+
649
+ ```bash
650
+ svelte-package && svelte-origin --dependency
651
+ ```
652
+
653
+ ### CLI Options
654
+
655
+ ```bash
656
+ svelte-origin [command] [options]
657
+
658
+ Commands:
659
+ post-process Post-process svelte-package output (default)
660
+ generate-dts Generate .svelte.d.ts files for svelte-check
661
+ help Show help for a command
662
+
663
+ Post-process options:
664
+ -h, --help Show help
665
+ -n, --dry-run Show what would be transformed without writing
666
+ -v, --verbose Show detailed transformation info
667
+ -d, --dependency Keep svelte-origin as a runtime dependency (skip inlining)
668
+
669
+ Examples:
670
+ svelte-package && svelte-origin # Default: inline runtime
671
+ svelte-package && svelte-origin dist # Specify directory
672
+ svelte-package && svelte-origin --dependency # Keep dependency
673
+ svelte-origin help post-process # Show post-process help
674
+ ```
675
+
676
+ ## License
677
+
678
+ MIT