svelte-origin 0.0.0 → 1.0.0-next.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1 +1,412 @@
1
- Something great is being built here✨
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** (`$attrs.origin(...)`)
9
+ - you forward props ergonomically (`$attrs.for(...)`)
10
+
11
+ Everything “macro-like” compiles down to plain Svelte 5 code.
12
+
13
+
14
+ ## Why this exists
15
+
16
+ Svelte 5 already has great primitives.
17
+
18
+ The pain is when you want:
19
+
20
+ - a *single* place to describe a prop surface (including defaults + bindables)
21
+ - predictable, explicit “create state from props” code
22
+ - clean prop forwarding without manually spelling every prop
23
+ - editor inference that matches what the build does
24
+
25
+ `svelte-origin` solves this with a **Svelte preprocessor** (and optionally a Vite plugin).
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ npm install svelte-origin
31
+ # or
32
+ bun add svelte-origin
33
+ ```
34
+
35
+ ## Setup
36
+
37
+ ### 1. Add the Vite plugin
38
+
39
+ The plugin works with **Vite**, **Rollup**, and **Rolldown**. It transforms macros in both origin files (`.svelte.ts`) and Svelte components.
40
+
41
+ ```ts
42
+ // vite.config.ts
43
+ import { sveltekit } from '@sveltejs/kit/vite'
44
+ import { svelteOrigin } from 'svelte-origin/plugin'
45
+
46
+ export default {
47
+ plugins: [
48
+ svelteOrigin(), // Must come BEFORE sveltekit/svelte
49
+ sveltekit()
50
+ ]
51
+ }
52
+ ```
53
+
54
+ For **Rollup**:
55
+
56
+ ```js
57
+ // rollup.config.js
58
+ import svelte from 'rollup-plugin-svelte'
59
+ import { svelteOrigin } from 'svelte-origin/plugin'
60
+
61
+ export default {
62
+ plugins: [
63
+ svelteOrigin(), // Must come BEFORE svelte
64
+ svelte()
65
+ ]
66
+ }
67
+ ```
68
+
69
+ ### 2. Add the Svelte preprocessor
70
+
71
+ The preprocessor is required to suppress editor warnings about `$attrs` being an "illegal variable name" (since `$` is reserved for Svelte runes).
72
+
73
+ ```js
74
+ // svelte.config.js
75
+ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
76
+ import { svelteOriginPreprocess } from 'svelte-origin/preprocess'
77
+
78
+ export default {
79
+ preprocess: [
80
+ svelteOriginPreprocess(), // Must come BEFORE vitePreprocess
81
+ vitePreprocess()
82
+ ]
83
+ }
84
+ ```
85
+
86
+ ### 3. Add TypeScript globals
87
+
88
+ Add `svelte-origin/globals` to your `tsconfig.json` to enable types for `$origin`, `$attrs.origin`, etc.
89
+
90
+ ```json
91
+ {
92
+ "compilerOptions": {
93
+ "types": [
94
+ "svelte-origin/globals"
95
+ ]
96
+ }
97
+ }
98
+ ```
99
+
100
+ ### Plugin options
101
+
102
+ ```ts
103
+ svelteOrigin({
104
+ // File extensions for origin definitions
105
+ extensions: ['.svelte.ts', '.svelte.js', '.origin.svelte.ts'],
106
+
107
+ // Enable debug logging
108
+ debug: false,
109
+
110
+ // Output transformed code to .transformed.txt files for debugging
111
+ outputTransformed: false,
112
+
113
+ // Directory for transformed output files (defaults to same as source)
114
+ outputDir: undefined
115
+ })
116
+ ```
117
+
118
+ ## The core idea
119
+
120
+ ### 1) Define an origin
121
+
122
+ Write origins in a file where runes are allowed (e.g. `.svelte.ts`).
123
+
124
+ This README describes the **macro** style (preferred direction):
125
+
126
+ ```ts
127
+ // counter.svelte.ts
128
+ export const Counter = $origin({
129
+ props: $attrs({
130
+ /** The label for the counter */
131
+ label: 'Counter',
132
+ /** The current count value (bindable for two-way binding) */
133
+ count: $bindable(0),
134
+ /** Step value for increment/decrement (optional) */
135
+ step: 1 as number
136
+ }),
137
+
138
+ /** Internal counter (private - not exposed) */
139
+ _incrementCount: $state(0),
140
+
141
+ /** Get double the current count */
142
+ get double() {
143
+ return $derived(this.props.count * 2)
144
+ },
145
+
146
+ /** Increment by step */
147
+ increment() {
148
+ this._incrementCount++
149
+ this.props.count += this.props.step
150
+ },
151
+
152
+ /** Decrement by step */
153
+ decrement() {
154
+ this.props.count -= this.props.step
155
+ },
156
+
157
+ /** Reset to zero */
158
+ reset() {
159
+ this.props.count = 0
160
+ }
161
+ })
162
+ ```
163
+
164
+ Notes:
165
+
166
+ - `$origin(...)` is a **compile-time macro** provided by `svelte-origin` (not a Svelte rune)
167
+ - `props: $attrs({...})` is also a macro in origin definitions
168
+ - this uses `$attrs` to avoid conflict with Svelte's `$props()` rune
169
+ - it compiles into the origin's prop schema and default/bindable handling
170
+ - the property name (`props`) is arbitrary—you can name it `attrs`, `options`, or anything else
171
+ - `$bindable(...)` inside `props: $attrs({...})` is treated as a marker (macro) and compiles away
172
+ - `$state`/`$derived` are real Svelte runes
173
+ - Properties prefixed with `_` (like `_incrementCount`) are private and not exposed on the instance type
174
+
175
+ ### 2) Create an instance from component props
176
+
177
+ Inside a component, be explicit that the instance is derived from the component's props:
178
+
179
+ ```svelte
180
+ <!-- CounterComponent.svelte -->
181
+ <script lang="ts">
182
+ import { Counter } from './counter.svelte'
183
+
184
+ // Optional: Declare $$Props for svelte-check/IDE type validation
185
+ type $$Props = $attrs.Of<typeof Counter>
186
+
187
+ // Create the counter instance from component props
188
+ let counter = $attrs.origin(Counter)
189
+ </script>
190
+
191
+ <div class="counter">
192
+ <h3>{counter.props.label}</h3>
193
+ <p>{counter.props.count}</p>
194
+ <p>Double: {counter.double}</p>
195
+ <div class="controls">
196
+ <button onclick={() => counter.decrement()}>-{counter.props.step}</button>
197
+ <button onclick={() => counter.reset()}>Reset</button>
198
+ <button onclick={() => counter.increment()}>+{counter.props.step}</button>
199
+ </div>
200
+ </div>
201
+ ```
202
+
203
+ Key points:
204
+
205
+ - We do **not** rely on "calling `Counter()` magically reads `$props()`"
206
+ - We make the props dependency explicit via `$attrs.origin(Counter)`
207
+ - Use `$attrs.Of<typeof Counter>` to get the component's prop types for manual `$$Props` declaration
208
+
209
+ Under the hood, the macro expands to canonical `$props()` usage, so the component's prop types remain inferable.
210
+
211
+ ## Passing to subcomponents
212
+
213
+ The rule of thumb is:
214
+
215
+ - each component owns its own origin instance
216
+ - parents forward values via props
217
+
218
+ We make forwarding ergonomic using `$attrs.for(...)`. This helper forwards the intersection of the parent's props and the child's origin requirements.
219
+
220
+ ```svelte
221
+ <script lang='ts'>
222
+ let childProps = $attrs.for(ChildOrigin)
223
+ </script>
224
+
225
+ <Child {...childProps} />
226
+ ```
227
+
228
+ Notes:
229
+
230
+ - `$attrs.for(...)` is intentionally about forwarding **from `$props()`**, not from an origin instance
231
+
232
+ ## Wrapper components / element props
233
+
234
+ If you’re forwarding attributes onto a native element, the type you want is an **attribute type**, not `HTMLInputElement`.
235
+
236
+ Svelte’s recommended types live in `svelte/elements`.
237
+
238
+ To make this ergonomic, we can provide:
239
+
240
+ ```svelte
241
+ <script lang='ts'>
242
+ let inputProps = $attrs.for('input')
243
+ </script>
244
+
245
+ <input {...inputProps} />
246
+ ```
247
+
248
+ (`$attrs.for` is a macro that expands to a typed `$props()` + rest pattern.)
249
+
250
+
251
+
252
+ ## Extending origins
253
+
254
+ You can extend existing origins by passing them as an array in the first argument. To access the parent implementation, use `this.super`.
255
+
256
+ ```ts
257
+ export const Counter = $origin({
258
+ // ... base implementation
259
+ increment() {
260
+ this.props.count += 1
261
+ }
262
+ })
263
+
264
+ export const SuperCounter = $origin([Counter], {
265
+ increment() {
266
+ // Call the parent implementation
267
+ this.super.increment()
268
+ console.log('Incremented!')
269
+ }
270
+ })
271
+ ```
272
+
273
+ ## Attachments
274
+
275
+ 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.
276
+
277
+ ### Defining attachments
278
+
279
+ ```ts
280
+ // widget.svelte.ts
281
+ export const Widget = $origin({
282
+ props: $attrs({
283
+ color: 'blue'
284
+ }),
285
+
286
+ /** Attachment: adds a tooltip to an element */
287
+ $tooltip(element: Element) {
288
+ const tooltip = document.createElement('div')
289
+ tooltip.textContent = 'Tooltip for ' + this.props.color
290
+ element.appendChild(tooltip)
291
+
292
+ // Return cleanup function
293
+ return () => tooltip.remove()
294
+ },
295
+
296
+ /** Attachment: highlights the element */
297
+ $highlight(element: Element) {
298
+ element.classList.add('highlighted')
299
+ return () => element.classList.remove('highlighted')
300
+ }
301
+ })
302
+ ```
303
+
304
+ ### Using attachments
305
+
306
+ Spread `$attachments` onto any element to apply all defined attachment behaviors:
307
+
308
+ ```svelte
309
+ <script lang="ts">
310
+ import { Widget } from './widget.svelte'
311
+ let widget = $attrs.origin(Widget)
312
+ </script>
313
+
314
+ <!-- All attachment methods are applied to this div -->
315
+ <div {...widget.$attachments}>
316
+ Content with tooltip and highlight
317
+ </div>
318
+ ```
319
+
320
+ ### Forwarding incoming attachments
321
+
322
+ When a parent uses Svelte's `{@attach ...}` directive on your component, those attachments are automatically included in `$attachments`:
323
+
324
+ ```svelte
325
+ <!-- Parent.svelte -->
326
+ <WidgetComponent {@attach myCustomAttachment} />
327
+ ```
328
+
329
+ ```svelte
330
+ <!-- WidgetComponent.svelte -->
331
+ <script lang="ts">
332
+ import { Widget } from './widget.svelte'
333
+ let widget = $attrs.origin(Widget)
334
+ </script>
335
+
336
+ <!-- Both origin-defined AND incoming attachments are applied -->
337
+ <div {...widget.$attachments}>...</div>
338
+ ```
339
+
340
+ This enables composable attachment behavior where parents can add behaviors to child component elements.
341
+
342
+ ## Init Callbacks
343
+
344
+ Origins support an optional init callback that runs when the origin instance is created:
345
+
346
+ ```ts
347
+ export const Timer = $origin({
348
+ props: $attrs({
349
+ interval: 1000
350
+ }),
351
+
352
+ _count: $state(0),
353
+
354
+ get count() {
355
+ return $derived(this._count)
356
+ }
357
+ }, function() {
358
+ // Runs when the component is created
359
+ const id = setInterval(() => {
360
+ this._count++
361
+ }, this.props.interval)
362
+
363
+ // Return cleanup function (runs on unmount)
364
+ return () => clearInterval(id)
365
+ })
366
+ ```
367
+
368
+ The callback:
369
+ - Runs after the instance is created
370
+ - Has access to `this` (the full instance including private members)
371
+ - Can return a cleanup function that runs when the component is destroyed
372
+
373
+ ## CLI Commands
374
+
375
+ The `svelte-origin` CLI provides utilities for library authors and type generation.
376
+
377
+ ### generate-dts
378
+
379
+ Generate `.svelte.d.ts` declaration files for svelte-check compatibility:
380
+
381
+ ```bash
382
+ svelte-origin generate-dts [src-dir]
383
+ svelte-origin generate-dts --clean # Remove generated files
384
+ ```
385
+
386
+ ### post-process
387
+
388
+ Transform macros in svelte-package output for library distribution:
389
+
390
+ ```bash
391
+ svelte-package && svelte-origin post-process [dist-dir]
392
+ ```
393
+
394
+ See `svelte-origin --help` for all options.
395
+
396
+ ## Known limitations
397
+
398
+ ### svelte-check type inference
399
+
400
+ The `svelte-check` CLI analyzes the original source code, not the plugin-transformed output. This means it may report type errors for components using `$attrs.origin()` even though the runtime behavior is correct.
401
+
402
+ **Workarounds:**
403
+
404
+ 1. **Use debug output** - Enable `outputTransformed: true` in the plugin options to see what the code looks like after transformation
405
+ 2. **Suppress errors** with `@ts-ignore` or `@ts-expect-error` comments where needed
406
+ 3. **Runtime works correctly** - Despite any type errors from `svelte-check`, two-way binding, reactivity, and all features work as expected
407
+
408
+ For more details on the root cause, see [SVELTE-TOOLING-ARCHITECTURE.md](.docs/SVELTE-TOOLING-ARCHITECTURE.md).
409
+
410
+ ## License
411
+
412
+ MIT
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Comprehensive alias resolution for SvelteKit and TypeScript projects
3
+ *
4
+ * Supports:
5
+ * - Manual aliases passed via options
6
+ * - SvelteKit kit.alias from svelte.config.js
7
+ * - TypeScript paths from tsconfig.json (with extends support)
8
+ * - Auto-generated .svelte-kit/tsconfig.json
9
+ */
10
+ export interface AliasConfig {
11
+ /** Manually provided aliases (highest priority) */
12
+ aliases?: Record<string, string>;
13
+ /** Project root directory */
14
+ root?: string;
15
+ /** Whether to auto-detect aliases from tsconfig.json and svelte.config.js */
16
+ autoDetect?: boolean;
17
+ /** Path to tsconfig.json (relative to root or absolute) */
18
+ tsconfig?: string;
19
+ /** Path to svelte.config.js (relative to root or absolute) */
20
+ svelteConfig?: string;
21
+ }
22
+ export interface ResolvedAliases {
23
+ /** Merged aliases from all sources */
24
+ aliases: Record<string, string>;
25
+ /** Project root */
26
+ root: string;
27
+ }
28
+ /**
29
+ * Resolve all aliases from various sources
30
+ */
31
+ export declare function resolveAliases(config?: AliasConfig): ResolvedAliases;
32
+ /**
33
+ * Resolve an import path using aliases
34
+ *
35
+ * Handles:
36
+ * - Exact matches: "$lib" -> "src/lib"
37
+ * - Prefix matches: "$lib/utils" -> "src/lib/utils"
38
+ * - Longest match wins: "$lib/components" before "$lib"
39
+ */
40
+ export declare function resolveImportPath(importPath: string, aliases: Record<string, string>, importerDir: string): string | null;
41
+ /**
42
+ * Try to resolve a file path, adding extensions if needed
43
+ */
44
+ export declare function resolveFilePath(basePath: string): string | null;
package/dist/cli.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * svelte-origin CLI
4
+ *
5
+ * Commands:
6
+ * post-process Post-process svelte-package output (default)
7
+ * generate-dts Generate .svelte.d.ts files for svelte-check compatibility
8
+ *
9
+ * Usage:
10
+ * svelte-origin [command] [options]
11
+ * svelte-origin post-process [dist-dir]
12
+ * svelte-origin generate-dts [src-dir]
13
+ *
14
+ * Options:
15
+ * --help Show help
16
+ * --dry-run Show what would be done without writing
17
+ * --verbose Show detailed info
18
+ */
19
+ export {};