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/LLM.md +545 -540
- package/README.md +678 -678
- package/dist/cli.js +46 -46
- package/dist/globals.d.ts +649 -649
- package/dist/index.d.ts +1 -1
- package/dist/index.js +66 -66
- package/dist/plugin.js +39 -39
- package/dist/post-process.js +46 -46
- package/dist/preprocess.js +42 -42
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/origin.d.ts +5 -7
- package/dist/runtime-inline.d.ts +4 -5
- package/dist/runtime-inline.min.js +1 -1
- package/dist/schema-resolver.d.ts +13 -0
- package/dist/transform/schema-codegen.d.ts +5 -0
- package/dist/vite-dts.js +7 -7
- package/package.json +1 -1
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
|