@vorra/core 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/di.js ADDED
@@ -0,0 +1,261 @@
1
+ //#region src/di.ts
2
+ const INJECTABLE_META = /* @__PURE__ */ new WeakMap();
3
+ /**
4
+ * Marks a class as injectable and configures where it is provided.
5
+ *
6
+ * @example
7
+ * \@Injectable({ providedIn: 'root' })
8
+ * class UserService {
9
+ * readonly users = signal<User[]>([]);
10
+ * }
11
+ */
12
+ function Injectable(options = {}) {
13
+ return (target) => {
14
+ const ctor = target;
15
+ const existing = INJECTABLE_META.get(ctor);
16
+ INJECTABLE_META.set(ctor, {
17
+ providedIn: options.providedIn ?? "root",
18
+ deps: existing?.deps ?? []
19
+ });
20
+ };
21
+ }
22
+ /**
23
+ * Declares the constructor dependencies for a class, enabling DI without
24
+ * TypeScript decorator metadata (emitDecoratorMetadata).
25
+ *
26
+ * @example
27
+ * \@Injectable({ providedIn: 'root' })
28
+ * \@Inject([HttpClient, AuthService])
29
+ * class UserService {
30
+ * constructor(private http: HttpClient, private auth: AuthService) {}
31
+ * }
32
+ */
33
+ function Inject(deps) {
34
+ return (target) => {
35
+ const ctor = target;
36
+ const existing = INJECTABLE_META.get(ctor);
37
+ if (existing) existing.deps = deps;
38
+ else INJECTABLE_META.set(ctor, {
39
+ providedIn: "root",
40
+ deps
41
+ });
42
+ };
43
+ }
44
+ let tokenIdCounter = 0;
45
+ /**
46
+ * A typed token used to inject values that aren't class instances — configs,
47
+ * primitives, interfaces, or abstract types.
48
+ *
49
+ * @example
50
+ * const API_URL = new InjectionToken<string>('API_URL', {
51
+ * providedIn: 'root',
52
+ * factory: () => 'https://api.example.com',
53
+ * });
54
+ *
55
+ * // Later:
56
+ * const url = inject(API_URL); // → 'https://api.example.com'
57
+ */
58
+ var InjectionToken = class {
59
+ constructor(description, options) {
60
+ this.id = ++tokenIdCounter;
61
+ this.description = description;
62
+ this.options = options;
63
+ }
64
+ toString() {
65
+ return `InjectionToken(${this.description})`;
66
+ }
67
+ };
68
+ const NOT_FOUND = Symbol("NOT_FOUND");
69
+ /**
70
+ * A hierarchical container that resolves and caches provider instances.
71
+ * Child injectors delegate to their parent when a token isn't found locally.
72
+ */
73
+ var Injector = class Injector {
74
+ constructor(providers = [], parent = null) {
75
+ this.parent = parent;
76
+ this.instances = /* @__PURE__ */ new Map();
77
+ this.resolving = /* @__PURE__ */ new Set();
78
+ this.providers = /* @__PURE__ */ new Map();
79
+ for (const p of providers) this.providers.set(p.provide, p);
80
+ }
81
+ get(token, optional = false) {
82
+ const result = this.resolve(token);
83
+ if (result === NOT_FOUND) {
84
+ if (optional) return null;
85
+ throw new Error(`[Vorra DI] No provider found for ${tokenName(token)}. Did you forget @Injectable() or to add it to your providers array?`);
86
+ }
87
+ return result;
88
+ }
89
+ resolve(token) {
90
+ if (this.instances.has(token)) return this.instances.get(token);
91
+ if (this.resolving.has(token)) throw new Error(`[Vorra DI] Circular dependency detected while resolving ${tokenName(token)}.`);
92
+ const provider = this.providers.get(token);
93
+ if (provider) {
94
+ this.resolving.add(token);
95
+ try {
96
+ const instance = this.instantiate(provider);
97
+ this.instances.set(token, instance);
98
+ return instance;
99
+ } finally {
100
+ this.resolving.delete(token);
101
+ }
102
+ }
103
+ if (token instanceof InjectionToken && token.options?.factory) {
104
+ const instance = runInContext(this, token.options.factory);
105
+ this.instances.set(token, instance);
106
+ return instance;
107
+ }
108
+ if (typeof token === "function") {
109
+ const meta = INJECTABLE_META.get(token);
110
+ if (meta) {
111
+ const targetInjector = this.resolveProvidedIn(meta.providedIn);
112
+ if (targetInjector === this) {
113
+ this.resolving.add(token);
114
+ try {
115
+ const instance = this.instantiateClass(token, meta.deps);
116
+ this.instances.set(token, instance);
117
+ return instance;
118
+ } finally {
119
+ this.resolving.delete(token);
120
+ }
121
+ } else if (targetInjector) return targetInjector.resolve(token);
122
+ }
123
+ }
124
+ if (this.parent) return this.parent.resolve(token);
125
+ return NOT_FOUND;
126
+ }
127
+ instantiate(provider) {
128
+ if ("useValue" in provider) return provider.useValue;
129
+ if ("useExisting" in provider) return this.get(provider.useExisting);
130
+ if ("useFactory" in provider) {
131
+ const deps = (provider.deps ?? []).map((dep) => this.get(dep));
132
+ return provider.useFactory(...deps);
133
+ }
134
+ const deps = (provider.deps ?? []).map((dep) => this.get(dep));
135
+ return new provider.useClass(...deps);
136
+ }
137
+ instantiateClass(ctor, deps) {
138
+ return new ctor(...deps.map((dep) => this.get(dep)));
139
+ }
140
+ resolveProvidedIn(providedIn) {
141
+ if (providedIn === "root") return getRootInjector();
142
+ if (providedIn === "component") return this;
143
+ if (providedIn instanceof Injector) return providedIn;
144
+ return null;
145
+ }
146
+ /**
147
+ * Creates a child injector that inherits from this one.
148
+ * Child providers shadow parent providers for the same token.
149
+ */
150
+ createChild(providers = []) {
151
+ return new Injector(providers, this);
152
+ }
153
+ /**
154
+ * Destroys this injector — calls onDestroy() on any instances that
155
+ * implement it, then clears all cached instances.
156
+ */
157
+ destroy() {
158
+ for (const instance of this.instances.values()) if (instance !== null && typeof instance === "object" && typeof instance.onDestroy === "function") instance.onDestroy();
159
+ this.instances.clear();
160
+ this.providers.clear();
161
+ }
162
+ };
163
+ let _rootInjector = null;
164
+ function getRootInjector() {
165
+ if (!_rootInjector) _rootInjector = new Injector();
166
+ return _rootInjector;
167
+ }
168
+ /**
169
+ * Bootstraps the application by creating the root injector with the given
170
+ * providers. Should be called once at app startup.
171
+ *
172
+ * @example
173
+ * bootstrapApp([
174
+ * { provide: API_URL, useValue: 'https://api.example.com' },
175
+ * ]);
176
+ */
177
+ function bootstrapApp(providers = []) {
178
+ _rootInjector = new Injector(providers);
179
+ return _rootInjector;
180
+ }
181
+ /** Resets the root injector — primarily useful in tests. */
182
+ function resetRootInjector() {
183
+ _rootInjector?.destroy();
184
+ _rootInjector = null;
185
+ }
186
+ /**
187
+ * The active injector context for inject() calls.
188
+ * Set during component/service instantiation.
189
+ */
190
+ let activeInjector = null;
191
+ /**
192
+ * Runs a function within a specific injector context, making inject() calls
193
+ * inside resolve against that injector.
194
+ */
195
+ function runInContext(injector, fn) {
196
+ const prev = activeInjector;
197
+ activeInjector = injector;
198
+ try {
199
+ return fn();
200
+ } finally {
201
+ activeInjector = prev;
202
+ }
203
+ }
204
+ /**
205
+ * Returns the currently active injector, or null if called outside an
206
+ * injection context.
207
+ */
208
+ function getActiveInjector() {
209
+ return activeInjector;
210
+ }
211
+ function inject(token, options = {}) {
212
+ if (!activeInjector) throw new Error("[Vorra DI] inject() called outside of an injection context. inject() can only be used during component or service construction.");
213
+ return activeInjector.get(token, options.optional);
214
+ }
215
+ function tokenName(token) {
216
+ if (token instanceof InjectionToken) return token.toString();
217
+ if (typeof token === "function") return token.name || "(anonymous class)";
218
+ return String(token);
219
+ }
220
+ /**
221
+ * Registers a cleanup callback to run when the injector that owns this
222
+ * service instance is destroyed.
223
+ *
224
+ * Call this inside a service constructor (within an injection context).
225
+ *
226
+ * @example
227
+ * \@Injectable({ providedIn: 'root' })
228
+ * class WebSocketService {
229
+ * #ws: WebSocket;
230
+ * constructor() {
231
+ * this.#ws = new WebSocket('wss://...');
232
+ * onDestroy(() => this.#ws.close());
233
+ * }
234
+ * }
235
+ */
236
+ function onDestroy(fn) {
237
+ if (!activeInjector) throw new Error("[Vorra DI] onDestroy() must be called within an injection context.");
238
+ new InjectionToken(`__destroyRef__`);
239
+ getOrCreateDestroyRef(activeInjector).callbacks.push(fn);
240
+ }
241
+ const destroyRefs = /* @__PURE__ */ new WeakMap();
242
+ function getOrCreateDestroyRef(injector) {
243
+ if (!destroyRefs.has(injector)) destroyRefs.set(injector, { callbacks: [] });
244
+ return destroyRefs.get(injector);
245
+ }
246
+ /**
247
+ * Runs all onDestroy callbacks registered against an injector.
248
+ * Called automatically by Injector.destroy(), but also exported for
249
+ * use in the component runtime.
250
+ */
251
+ function runDestroyCallbacks(injector) {
252
+ const ref = destroyRefs.get(injector);
253
+ if (ref) {
254
+ for (const cb of ref.callbacks) cb();
255
+ ref.callbacks = [];
256
+ }
257
+ }
258
+ //#endregion
259
+ export { Inject, Injectable, InjectionToken, Injector, bootstrapApp, getActiveInjector, getRootInjector, inject, onDestroy, resetRootInjector, runDestroyCallbacks, runInContext };
260
+
261
+ //# sourceMappingURL=di.js.map
package/dist/di.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"di.js","names":[],"sources":["../src/di.ts"],"sourcesContent":["// =============================================================================\r\n// Vorra DI System\r\n// Injectable / InjectionToken / inject() / Injector / runInContext()\r\n// =============================================================================\r\n\r\nimport type { WritableSignal, ReadonlySignal } from './reactivity';\r\n\r\n// ---------------------------------------------------------------------------\r\n// Types\r\n// ---------------------------------------------------------------------------\r\n\r\n/** Where a provider is instantiated in the injector tree. */\r\nexport type ProvidedIn = 'root' | 'component' | Injector;\r\n\r\nexport interface InjectableOptions {\r\n providedIn?: ProvidedIn;\r\n}\r\n\r\n/** Describes how to provide a value for a given token. */\r\nexport type Provider<T> =\r\n | ClassProvider<T>\r\n | ValueProvider<T>\r\n | FactoryProvider<T>\r\n | ExistingProvider<T>;\r\n\r\nexport interface ClassProvider<T> {\r\n provide: Token<T>;\r\n useClass: new (...args: unknown[]) => T;\r\n deps?: Token<unknown>[];\r\n}\r\n\r\nexport interface ValueProvider<T> {\r\n provide: Token<T>;\r\n useValue: T;\r\n}\r\n\r\nexport interface FactoryProvider<T> {\r\n provide: Token<T>;\r\n useFactory: (...args: unknown[]) => T;\r\n deps?: Token<unknown>[];\r\n}\r\n\r\nexport interface ExistingProvider<T> {\r\n provide: Token<T>;\r\n useExisting: Token<T>;\r\n}\r\n\r\n/** Anything that can be used as a DI token. */\r\nexport type Token<T> =\r\n | (new (...args: unknown[]) => T)\r\n | InjectionToken<T>\r\n | AbstractToken<T>;\r\n\r\n/** Marker interface for abstract class tokens. */\r\nexport interface AbstractToken<T> {\r\n readonly __tokenType: T;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// INJECTABLE_META — metadata store for @Injectable classes\r\n// ---------------------------------------------------------------------------\r\n\r\nconst INJECTABLE_META = new WeakMap<\r\n new (...args: unknown[]) => unknown,\r\n { providedIn: ProvidedIn; deps: Token<unknown>[] }\r\n>();\r\n\r\n// ---------------------------------------------------------------------------\r\n// @Injectable decorator\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Marks a class as injectable and configures where it is provided.\r\n *\r\n * @example\r\n * \\@Injectable({ providedIn: 'root' })\r\n * class UserService {\r\n * readonly users = signal<User[]>([]);\r\n * }\r\n */\r\nexport function Injectable(options: InjectableOptions = {}): ClassDecorator {\r\n return (target: unknown) => {\r\n const ctor = target as new (...args: unknown[]) => unknown;\r\n const existing = INJECTABLE_META.get(ctor);\r\n INJECTABLE_META.set(ctor, {\r\n providedIn: options.providedIn ?? 'root',\r\n deps: existing?.deps ?? [],\r\n });\r\n };\r\n}\r\n\r\n/**\r\n * Declares the constructor dependencies for a class, enabling DI without\r\n * TypeScript decorator metadata (emitDecoratorMetadata).\r\n *\r\n * @example\r\n * \\@Injectable({ providedIn: 'root' })\r\n * \\@Inject([HttpClient, AuthService])\r\n * class UserService {\r\n * constructor(private http: HttpClient, private auth: AuthService) {}\r\n * }\r\n */\r\nexport function Inject(deps: Token<unknown>[]): ClassDecorator {\r\n return (target: unknown) => {\r\n const ctor = target as new (...args: unknown[]) => unknown;\r\n const existing = INJECTABLE_META.get(ctor);\r\n if (existing) {\r\n existing.deps = deps;\r\n } else {\r\n INJECTABLE_META.set(ctor, { providedIn: 'root', deps });\r\n }\r\n };\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// InjectionToken\r\n// ---------------------------------------------------------------------------\r\n\r\nlet tokenIdCounter = 0;\r\n\r\n/**\r\n * A typed token used to inject values that aren't class instances — configs,\r\n * primitives, interfaces, or abstract types.\r\n *\r\n * @example\r\n * const API_URL = new InjectionToken<string>('API_URL', {\r\n * providedIn: 'root',\r\n * factory: () => 'https://api.example.com',\r\n * });\r\n *\r\n * // Later:\r\n * const url = inject(API_URL); // → 'https://api.example.com'\r\n */\r\nexport class InjectionToken<T> {\r\n readonly __tokenType!: T; // phantom type only — never assigned at runtime\r\n readonly id: number;\r\n readonly description: string;\r\n readonly options?: {\r\n providedIn?: ProvidedIn;\r\n factory?: () => T;\r\n };\r\n\r\n constructor(\r\n description: string,\r\n options?: { providedIn?: ProvidedIn; factory?: () => T }\r\n ) {\r\n this.id = ++tokenIdCounter;\r\n this.description = description;\r\n this.options = options;\r\n }\r\n\r\n toString(): string {\r\n return `InjectionToken(${this.description})`;\r\n }\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Injector\r\n// ---------------------------------------------------------------------------\r\n\r\nconst NOT_FOUND = Symbol('NOT_FOUND');\r\nconst CIRCULAR = Symbol('CIRCULAR');\r\n\r\n/**\r\n * A hierarchical container that resolves and caches provider instances.\r\n * Child injectors delegate to their parent when a token isn't found locally.\r\n */\r\nexport class Injector {\r\n private readonly instances = new Map<Token<unknown>, unknown>();\r\n private readonly resolving = new Set<Token<unknown>>();\r\n private readonly providers = new Map<Token<unknown>, Provider<unknown>>();\r\n\r\n constructor(\r\n providers: Provider<unknown>[] = [],\r\n private readonly parent: Injector | null = null\r\n ) {\r\n for (const p of providers) {\r\n this.providers.set(p.provide, p);\r\n }\r\n }\r\n\r\n /**\r\n * Resolves a token to its instance. Throws if the token cannot be resolved\r\n * and `optional` is false (the default).\r\n */\r\n get<T>(token: Token<T>, optional?: false): T;\r\n get<T>(token: Token<T>, optional: true): T | null;\r\n get<T>(token: Token<T>, optional = false): T | null {\r\n const result = this.resolve(token);\r\n if (result === NOT_FOUND) {\r\n if (optional) return null;\r\n throw new Error(\r\n `[Vorra DI] No provider found for ${tokenName(token)}. ` +\r\n `Did you forget @Injectable() or to add it to your providers array?`\r\n );\r\n }\r\n return result as T;\r\n }\r\n\r\n private resolve<T>(token: Token<T>): T | typeof NOT_FOUND {\r\n // 1. Return cached instance\r\n if (this.instances.has(token)) {\r\n return this.instances.get(token) as T;\r\n }\r\n\r\n // 2. Circular dependency guard\r\n if (this.resolving.has(token)) {\r\n throw new Error(\r\n `[Vorra DI] Circular dependency detected while resolving ${tokenName(token)}.`\r\n );\r\n }\r\n\r\n // 3. Check local provider registry\r\n const provider = this.providers.get(token);\r\n if (provider) {\r\n this.resolving.add(token);\r\n try {\r\n const instance = this.instantiate(provider) as T;\r\n this.instances.set(token, instance);\r\n return instance;\r\n } finally {\r\n this.resolving.delete(token);\r\n }\r\n }\r\n\r\n // 4. Handle InjectionToken with a factory (self-providing tokens)\r\n if (token instanceof InjectionToken && token.options?.factory) {\r\n const instance = runInContext(this, token.options.factory) as T;\r\n this.instances.set(token, instance);\r\n return instance;\r\n }\r\n\r\n // 5. Handle @Injectable classes with providedIn: 'root' / this injector\r\n if (typeof token === 'function') {\r\n const meta = INJECTABLE_META.get(token as new (...args: unknown[]) => unknown);\r\n if (meta) {\r\n const targetInjector = this.resolveProvidedIn(meta.providedIn);\r\n if (targetInjector === this) {\r\n this.resolving.add(token);\r\n try {\r\n const instance = this.instantiateClass(\r\n token as new (...args: unknown[]) => T,\r\n meta.deps\r\n );\r\n this.instances.set(token, instance);\r\n return instance;\r\n } finally {\r\n this.resolving.delete(token);\r\n }\r\n } else if (targetInjector) {\r\n // Delegate to the appropriate injector in the tree\r\n return targetInjector.resolve(token);\r\n }\r\n }\r\n }\r\n\r\n // 6. Delegate to parent\r\n if (this.parent) {\r\n return this.parent.resolve(token);\r\n }\r\n\r\n return NOT_FOUND as typeof NOT_FOUND;\r\n }\r\n\r\n private instantiate<T>(provider: Provider<T>): T {\r\n if ('useValue' in provider) {\r\n return provider.useValue;\r\n }\r\n\r\n if ('useExisting' in provider) {\r\n return this.get(provider.useExisting);\r\n }\r\n\r\n if ('useFactory' in provider) {\r\n const deps = (provider.deps ?? []).map(dep => this.get(dep));\r\n return provider.useFactory(...deps) as T;\r\n }\r\n\r\n // useClass\r\n const deps = (provider.deps ?? []).map(dep => this.get(dep));\r\n return new provider.useClass(...deps) as T;\r\n }\r\n\r\n private instantiateClass<T>(\r\n ctor: new (...args: unknown[]) => T,\r\n deps: Token<unknown>[]\r\n ): T {\r\n const resolved = deps.map(dep => this.get(dep));\r\n return new ctor(...resolved);\r\n }\r\n\r\n private resolveProvidedIn(providedIn: ProvidedIn): Injector | null {\r\n if (providedIn === 'root') return getRootInjector();\r\n if (providedIn === 'component') return this;\r\n if (providedIn instanceof Injector) return providedIn;\r\n return null;\r\n }\r\n\r\n /**\r\n * Creates a child injector that inherits from this one.\r\n * Child providers shadow parent providers for the same token.\r\n */\r\n createChild(providers: Provider<unknown>[] = []): Injector {\r\n return new Injector(providers, this);\r\n }\r\n\r\n /**\r\n * Destroys this injector — calls onDestroy() on any instances that\r\n * implement it, then clears all cached instances.\r\n */\r\n destroy(): void {\r\n for (const instance of this.instances.values()) {\r\n if (\r\n instance !== null &&\r\n typeof instance === 'object' &&\r\n typeof (instance as { onDestroy?: () => void }).onDestroy === 'function'\r\n ) {\r\n (instance as { onDestroy: () => void }).onDestroy();\r\n }\r\n }\r\n this.instances.clear();\r\n this.providers.clear();\r\n }\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Root injector (app-level singleton)\r\n// ---------------------------------------------------------------------------\r\n\r\nlet _rootInjector: Injector | null = null;\r\n\r\nexport function getRootInjector(): Injector {\r\n if (!_rootInjector) {\r\n _rootInjector = new Injector();\r\n }\r\n return _rootInjector;\r\n}\r\n\r\n/**\r\n * Bootstraps the application by creating the root injector with the given\r\n * providers. Should be called once at app startup.\r\n *\r\n * @example\r\n * bootstrapApp([\r\n * { provide: API_URL, useValue: 'https://api.example.com' },\r\n * ]);\r\n */\r\nexport function bootstrapApp(providers: Provider<unknown>[] = []): Injector {\r\n _rootInjector = new Injector(providers);\r\n return _rootInjector;\r\n}\r\n\r\n/** Resets the root injector — primarily useful in tests. */\r\nexport function resetRootInjector(): void {\r\n _rootInjector?.destroy();\r\n _rootInjector = null;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Injection context\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * The active injector context for inject() calls.\r\n * Set during component/service instantiation.\r\n */\r\nlet activeInjector: Injector | null = null;\r\n\r\n/**\r\n * Runs a function within a specific injector context, making inject() calls\r\n * inside resolve against that injector.\r\n */\r\nexport function runInContext<T>(injector: Injector, fn: () => T): T {\r\n const prev = activeInjector;\r\n activeInjector = injector;\r\n try {\r\n return fn();\r\n } finally {\r\n activeInjector = prev;\r\n }\r\n}\r\n\r\n/**\r\n * Returns the currently active injector, or null if called outside an\r\n * injection context.\r\n */\r\nexport function getActiveInjector(): Injector | null {\r\n return activeInjector;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// inject()\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Resolves a token from the current injection context.\r\n * Must be called during component or service construction.\r\n *\r\n * @example\r\n * \\@Injectable({ providedIn: 'root' })\r\n * class DashboardComponent {\r\n * private users = inject(UserService);\r\n * private apiUrl = inject(API_URL);\r\n * }\r\n */\r\nexport function inject<T>(token: Token<T>, options?: { optional?: false }): T;\r\nexport function inject<T>(token: Token<T>, options: { optional: true }): T | null;\r\nexport function inject<T>(\r\n token: Token<T>,\r\n options: { optional?: boolean } = {}\r\n): T | null {\r\n if (!activeInjector) {\r\n throw new Error(\r\n `[Vorra DI] inject() called outside of an injection context. ` +\r\n `inject() can only be used during component or service construction.`\r\n );\r\n }\r\n return activeInjector.get(token, options.optional as true) as T | null;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Helper: resolve token display name for error messages\r\n// ---------------------------------------------------------------------------\r\n\r\nfunction tokenName(token: Token<unknown>): string {\r\n if (token instanceof InjectionToken) return token.toString();\r\n if (typeof token === 'function') return token.name || '(anonymous class)';\r\n return String(token);\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// onDestroy() — lifecycle hook helper for services\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Registers a cleanup callback to run when the injector that owns this\r\n * service instance is destroyed.\r\n *\r\n * Call this inside a service constructor (within an injection context).\r\n *\r\n * @example\r\n * \\@Injectable({ providedIn: 'root' })\r\n * class WebSocketService {\r\n * #ws: WebSocket;\r\n * constructor() {\r\n * this.#ws = new WebSocket('wss://...');\r\n * onDestroy(() => this.#ws.close());\r\n * }\r\n * }\r\n */\r\nexport function onDestroy(fn: () => void): void {\r\n if (!activeInjector) {\r\n throw new Error('[Vorra DI] onDestroy() must be called within an injection context.');\r\n }\r\n // Attach the cleanup to a sentinel object in the injector under a unique token\r\n const token = new InjectionToken<DestroyRef>(`__destroyRef__`);\r\n // We piggyback on a shared DestroyRef list attached to the active injector\r\n getOrCreateDestroyRef(activeInjector).callbacks.push(fn);\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// DestroyRef — internal lifecycle tracking\r\n// ---------------------------------------------------------------------------\r\n\r\ninterface DestroyRef {\r\n callbacks: (() => void)[];\r\n}\r\n\r\nconst destroyRefs = new WeakMap<Injector, DestroyRef>();\r\n\r\nfunction getOrCreateDestroyRef(injector: Injector): DestroyRef {\r\n if (!destroyRefs.has(injector)) {\r\n destroyRefs.set(injector, { callbacks: [] });\r\n }\r\n return destroyRefs.get(injector)!;\r\n}\r\n\r\n/**\r\n * Runs all onDestroy callbacks registered against an injector.\r\n * Called automatically by Injector.destroy(), but also exported for\r\n * use in the component runtime.\r\n */\r\nexport function runDestroyCallbacks(injector: Injector): void {\r\n const ref = destroyRefs.get(injector);\r\n if (ref) {\r\n for (const cb of ref.callbacks) cb();\r\n ref.callbacks = [];\r\n }\r\n}\r\n"],"mappings":"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"}
package/dist/dom.cjs ADDED
@@ -0,0 +1,302 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_reactivity = require("./reactivity.cjs");
3
+ //#region src/dom.ts
4
+ /**
5
+ * Creates a DOM element with the given tag name.
6
+ *
7
+ * @example
8
+ * const div = createElement('div');
9
+ */
10
+ function createElement(tag) {
11
+ return document.createElement(tag);
12
+ }
13
+ /**
14
+ * Sets a static attribute on an element.
15
+ *
16
+ * @example
17
+ * setAttr(el, 'class', 'container');
18
+ */
19
+ function setAttr(el, name, value) {
20
+ el.setAttribute(name, value);
21
+ }
22
+ /**
23
+ * Sets a DOM property directly (e.g. value, checked, disabled).
24
+ *
25
+ * @example
26
+ * setProp(input, 'value', 'hello');
27
+ */
28
+ function setProp(el, name, value) {
29
+ el[name] = value;
30
+ }
31
+ /**
32
+ * Attaches an event listener and returns an EffectHandle to remove it.
33
+ *
34
+ * @example
35
+ * const handle = listen(btn, 'click', () => count.update(n => n + 1));
36
+ * handle.destroy(); // removes listener
37
+ */
38
+ function listen(el, event, handler) {
39
+ el.addEventListener(event, handler);
40
+ return { destroy() {
41
+ el.removeEventListener(event, handler);
42
+ } };
43
+ }
44
+ /**
45
+ * Inserts a child node into a parent, optionally before an anchor node.
46
+ *
47
+ * @example
48
+ * insert(container, textNode);
49
+ * insert(list, item, anchor); // insert before anchor
50
+ */
51
+ function insert(parent, child, anchor) {
52
+ parent.insertBefore(child, anchor ?? null);
53
+ }
54
+ /**
55
+ * Removes a node from the DOM.
56
+ *
57
+ * @example
58
+ * remove(el);
59
+ */
60
+ function remove(node) {
61
+ node.parentNode?.removeChild(node);
62
+ }
63
+ /**
64
+ * Binds a getter to a Text node's content. The text updates surgically
65
+ * whenever the getter's signal dependencies change.
66
+ *
67
+ * @example
68
+ * const t = document.createTextNode('');
69
+ * bindText(t, () => String(count()));
70
+ */
71
+ function bindText(node, getter) {
72
+ return require_reactivity.effect(() => {
73
+ node.nodeValue = getter();
74
+ });
75
+ }
76
+ /**
77
+ * Binds a getter to an element attribute. When the getter returns `null`,
78
+ * the attribute is removed.
79
+ *
80
+ * @example
81
+ * bindAttr(el, 'disabled', () => isDisabled() ? '' : null);
82
+ */
83
+ function bindAttr(el, name, getter) {
84
+ return require_reactivity.effect(() => {
85
+ const value = getter();
86
+ if (value === null) el.removeAttribute(name);
87
+ else el.setAttribute(name, value);
88
+ });
89
+ }
90
+ /**
91
+ * Binds a getter to a DOM property. Updates the property directly whenever
92
+ * the getter's signal dependencies change.
93
+ *
94
+ * @example
95
+ * bindProp(input, 'value', () => name());
96
+ */
97
+ function bindProp(el, name, getter) {
98
+ return require_reactivity.effect(() => {
99
+ el[name] = getter();
100
+ });
101
+ }
102
+ /**
103
+ * Binds a getter to element visibility. When the getter returns `false`,
104
+ * `display: none` is applied; otherwise the inline style is cleared.
105
+ *
106
+ * @example
107
+ * bindShow(el, () => isVisible());
108
+ */
109
+ function bindShow(el, getter) {
110
+ return require_reactivity.effect(() => {
111
+ el.style.display = getter() ? "" : "none";
112
+ });
113
+ }
114
+ /**
115
+ * Binds a getter to a set of CSS classes. Each key in the record is a class
116
+ * name; when the value is `true` the class is added, when `false` it is
117
+ * removed. Classes not in the record are left untouched.
118
+ *
119
+ * @example
120
+ * bindClass(el, () => ({ active: isActive(), disabled: isDisabled() }));
121
+ */
122
+ function bindClass(el, getter) {
123
+ let prevClasses = {};
124
+ return require_reactivity.effect(() => {
125
+ const next = getter();
126
+ for (const name of Object.keys(prevClasses)) if (prevClasses[name] === true && !next[name]) el.classList.remove(name);
127
+ for (const [name, active] of Object.entries(next)) if (active) el.classList.add(name);
128
+ else el.classList.remove(name);
129
+ prevClasses = next;
130
+ });
131
+ }
132
+ /**
133
+ * Reactively renders a list of items before an anchor comment node.
134
+ * Each item receives its own `ComponentContext` scoped under `parentCtx`
135
+ * so that bindings created inside the item template are properly cleaned
136
+ * up whenever the list re-renders.
137
+ *
138
+ * This is the runtime backing for the `@for` template directive.
139
+ *
140
+ * @example
141
+ * // Generated by: <li @for={item of items()}>…</li>
142
+ * const anchor = document.createComment('for');
143
+ * insert(container, anchor);
144
+ * ctx.effects.push(bindList(anchor, ctx, () => items(), (item, i, itemCtx) => {
145
+ * const li = createElement('li');
146
+ * const t = document.createTextNode('');
147
+ * itemCtx.effects.push(bindText(t, () => item.name));
148
+ * insert(li, t);
149
+ * return li;
150
+ * }));
151
+ */
152
+ function bindList(anchor, parentCtx, getter, itemFactory) {
153
+ let activeItems = [];
154
+ return require_reactivity.effect(() => {
155
+ const list = getter();
156
+ const parent = anchor.parentNode;
157
+ if (!parent) return;
158
+ for (const { node, ctx } of activeItems) {
159
+ parent.removeChild(node);
160
+ const idx = parentCtx.children.indexOf(ctx);
161
+ if (idx >= 0) parentCtx.children.splice(idx, 1);
162
+ destroyComponent(ctx);
163
+ }
164
+ activeItems = [];
165
+ for (let i = 0; i < list.length; i++) {
166
+ const itemCtx = createComponent(parentCtx.injector);
167
+ parentCtx.children.push(itemCtx);
168
+ const node = itemFactory(list[i], i, itemCtx);
169
+ parent.insertBefore(node, anchor);
170
+ activeItems.push({
171
+ node,
172
+ ctx: itemCtx
173
+ });
174
+ }
175
+ });
176
+ }
177
+ /**
178
+ * Creates a ComponentContext scoped to a child injector derived from
179
+ * `parentInjector`. The context owns all EffectHandles created during
180
+ * the component's lifetime.
181
+ *
182
+ * @example
183
+ * const ctx = createComponent(app, [{ provide: MY_TOKEN, useValue: 42 }]);
184
+ */
185
+ function createComponent(parentInjector, providers) {
186
+ return {
187
+ injector: parentInjector.createChild(providers ?? []),
188
+ effects: [],
189
+ children: []
190
+ };
191
+ }
192
+ /**
193
+ * Tears down a ComponentContext: destroys all child contexts recursively,
194
+ * then destroys all owned effects, then destroys the injector.
195
+ *
196
+ * @example
197
+ * destroyComponent(ctx);
198
+ */
199
+ function destroyComponent(ctx) {
200
+ for (const child of ctx.children) destroyComponent(child);
201
+ ctx.children.length = 0;
202
+ for (const handle of ctx.effects) handle.destroy();
203
+ ctx.effects.length = 0;
204
+ ctx.injector.destroy();
205
+ }
206
+ /**
207
+ * Mounts a compiled component factory into a container element. The factory
208
+ * receives the ComponentContext, builds the DOM subtree, and returns the
209
+ * root node which is then appended to the container.
210
+ *
211
+ * @example
212
+ * mountComponent(counterFactory, document.getElementById('app')!, ctx);
213
+ */
214
+ function mountComponent(factory, container, ctx) {
215
+ const node = factory(ctx);
216
+ container.appendChild(node);
217
+ }
218
+ /**
219
+ * Instantiates a child component inside a parent's template. Creates a child
220
+ * ComponentContext scoped under the parent, registers it for lifecycle
221
+ * tracking, invokes the factory with the given props, and returns the root
222
+ * DOM node so the caller can insert it into the tree.
223
+ *
224
+ * Props are always passed as getter functions so both static and reactive
225
+ * values share a uniform call-site API: `props['label']()`.
226
+ *
227
+ * @example
228
+ * // In a compiled parent template:
229
+ * const _e1 = mountChild(MyButton, ctx, { label: () => 'Click me' });
230
+ * insert(_e0, _e1);
231
+ */
232
+ function mountChild(factory, parentCtx, props = {}) {
233
+ const childCtx = createComponent(parentCtx.injector);
234
+ parentCtx.children.push(childCtx);
235
+ const node = factory(childCtx, props);
236
+ const hmrId = factory["__hmrId"];
237
+ if (hmrId) {
238
+ const hmr = (typeof window !== "undefined" ? window : null)?.["__vorra_hmr"];
239
+ if (hmr?.instances) {
240
+ const list = hmr.instances.get(hmrId) ?? [];
241
+ list.push({
242
+ node,
243
+ ctx: childCtx,
244
+ props,
245
+ parentCtx
246
+ });
247
+ hmr.instances.set(hmrId, list);
248
+ }
249
+ }
250
+ return node;
251
+ }
252
+ /**
253
+ * Replaces all mounted instances of the component identified by `id` with
254
+ * the output of `newFactory`. Called by the HMR client when a `.vorra` chunk
255
+ * is hot-updated. Not intended for use in application code.
256
+ */
257
+ function hmrAccept(id, newFactory) {
258
+ const hmr = (typeof window !== "undefined" ? window : null)?.["__vorra_hmr"];
259
+ if (!hmr?.instances) return;
260
+ const instances = hmr.instances.get(id) ?? [];
261
+ for (const inst of instances) {
262
+ const parent = inst.node.parentNode;
263
+ if (!parent) continue;
264
+ const nextSibling = inst.node.nextSibling;
265
+ destroyComponent(inst.ctx);
266
+ parent.removeChild(inst.node);
267
+ const newCtx = createComponent(inst.parentCtx.injector);
268
+ const idx = inst.parentCtx.children.indexOf(inst.ctx);
269
+ if (idx >= 0) inst.parentCtx.children[idx] = newCtx;
270
+ const newNode = newFactory(newCtx, inst.props);
271
+ parent.insertBefore(newNode, nextSibling);
272
+ inst.node = newNode;
273
+ inst.ctx = newCtx;
274
+ }
275
+ }
276
+ if (typeof __vorra_dev !== "undefined" && __vorra_dev && typeof window !== "undefined") {
277
+ const w = window;
278
+ if (!w["__vorra_hmr"]) w["__vorra_hmr"] = {};
279
+ const hmr = w["__vorra_hmr"];
280
+ if (!hmr["instances"]) hmr["instances"] = /* @__PURE__ */ new Map();
281
+ hmr["accept"] = hmrAccept;
282
+ }
283
+ //#endregion
284
+ exports.bindAttr = bindAttr;
285
+ exports.bindClass = bindClass;
286
+ exports.bindList = bindList;
287
+ exports.bindProp = bindProp;
288
+ exports.bindShow = bindShow;
289
+ exports.bindText = bindText;
290
+ exports.createComponent = createComponent;
291
+ exports.createElement = createElement;
292
+ exports.destroyComponent = destroyComponent;
293
+ exports.hmrAccept = hmrAccept;
294
+ exports.insert = insert;
295
+ exports.listen = listen;
296
+ exports.mountChild = mountChild;
297
+ exports.mountComponent = mountComponent;
298
+ exports.remove = remove;
299
+ exports.setAttr = setAttr;
300
+ exports.setProp = setProp;
301
+
302
+ //# sourceMappingURL=dom.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dom.cjs","names":["effect"],"sources":["../src/dom.ts"],"sourcesContent":["// =============================================================================\r\n// Vorra Runtime DOM Layer\r\n// Direct DOM operations + reactive bindings that compiled templates call into.\r\n// =============================================================================\r\n\r\nimport { effect } from './reactivity.js';\r\nimport type { EffectHandle } from './reactivity.js';\r\nimport { Injector } from './di.js';\r\nimport type { Provider } from './di.js';\r\n\r\n// ---------------------------------------------------------------------------\r\n// Types\r\n// ---------------------------------------------------------------------------\r\n\r\nexport interface ComponentContext {\r\n injector: Injector;\r\n effects: EffectHandle[];\r\n children: ComponentContext[];\r\n}\r\n\r\n// Injected as a compile-time constant by Rolldown `define` in dev mode.\r\n// `typeof` check is intentional — the variable may not be defined at runtime.\r\ndeclare const __vorra_dev: boolean | undefined;\r\n\r\n/** Internal record of a mounted component instance, used by HMR swapping. */\r\ninterface HMRInstance {\r\n node: Node;\r\n ctx: ComponentContext;\r\n props: Record<string, () => unknown>;\r\n parentCtx: ComponentContext;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// 5.1 Element creation & patching\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Creates a DOM element with the given tag name.\r\n *\r\n * @example\r\n * const div = createElement('div');\r\n */\r\nexport function createElement(tag: string): Element {\r\n return document.createElement(tag);\r\n}\r\n\r\n/**\r\n * Sets a static attribute on an element.\r\n *\r\n * @example\r\n * setAttr(el, 'class', 'container');\r\n */\r\nexport function setAttr(el: Element, name: string, value: string): void {\r\n el.setAttribute(name, value);\r\n}\r\n\r\n/**\r\n * Sets a DOM property directly (e.g. value, checked, disabled).\r\n *\r\n * @example\r\n * setProp(input, 'value', 'hello');\r\n */\r\nexport function setProp(el: Element, name: string, value: unknown): void {\r\n (el as unknown as Record<string, unknown>)[name] = value;\r\n}\r\n\r\n/**\r\n * Attaches an event listener and returns an EffectHandle to remove it.\r\n *\r\n * @example\r\n * const handle = listen(btn, 'click', () => count.update(n => n + 1));\r\n * handle.destroy(); // removes listener\r\n */\r\nexport function listen(\r\n el: Element,\r\n event: string,\r\n handler: EventListener\r\n): EffectHandle {\r\n el.addEventListener(event, handler);\r\n return {\r\n destroy() {\r\n el.removeEventListener(event, handler);\r\n },\r\n };\r\n}\r\n\r\n/**\r\n * Inserts a child node into a parent, optionally before an anchor node.\r\n *\r\n * @example\r\n * insert(container, textNode);\r\n * insert(list, item, anchor); // insert before anchor\r\n */\r\nexport function insert(parent: Node, child: Node, anchor?: Node | null): void {\r\n parent.insertBefore(child, anchor ?? null);\r\n}\r\n\r\n/**\r\n * Removes a node from the DOM.\r\n *\r\n * @example\r\n * remove(el);\r\n */\r\nexport function remove(node: Node): void {\r\n node.parentNode?.removeChild(node);\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// 5.2 Reactive bindings\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Binds a getter to a Text node's content. The text updates surgically\r\n * whenever the getter's signal dependencies change.\r\n *\r\n * @example\r\n * const t = document.createTextNode('');\r\n * bindText(t, () => String(count()));\r\n */\r\nexport function bindText(node: Text, getter: () => string): EffectHandle {\r\n return effect(() => {\r\n node.nodeValue = getter();\r\n });\r\n}\r\n\r\n/**\r\n * Binds a getter to an element attribute. When the getter returns `null`,\r\n * the attribute is removed.\r\n *\r\n * @example\r\n * bindAttr(el, 'disabled', () => isDisabled() ? '' : null);\r\n */\r\nexport function bindAttr(\r\n el: Element,\r\n name: string,\r\n getter: () => string | null\r\n): EffectHandle {\r\n return effect(() => {\r\n const value = getter();\r\n if (value === null) {\r\n el.removeAttribute(name);\r\n } else {\r\n el.setAttribute(name, value);\r\n }\r\n });\r\n}\r\n\r\n/**\r\n * Binds a getter to a DOM property. Updates the property directly whenever\r\n * the getter's signal dependencies change.\r\n *\r\n * @example\r\n * bindProp(input, 'value', () => name());\r\n */\r\nexport function bindProp(\r\n el: Element,\r\n name: string,\r\n getter: () => unknown\r\n): EffectHandle {\r\n return effect(() => {\r\n (el as unknown as Record<string, unknown>)[name] = getter();\r\n });\r\n}\r\n\r\n/**\r\n * Binds a getter to element visibility. When the getter returns `false`,\r\n * `display: none` is applied; otherwise the inline style is cleared.\r\n *\r\n * @example\r\n * bindShow(el, () => isVisible());\r\n */\r\nexport function bindShow(el: Element, getter: () => boolean): EffectHandle {\r\n return effect(() => {\r\n (el as HTMLElement).style.display = getter() ? '' : 'none';\r\n });\r\n}\r\n\r\n/**\r\n * Binds a getter to a set of CSS classes. Each key in the record is a class\r\n * name; when the value is `true` the class is added, when `false` it is\r\n * removed. Classes not in the record are left untouched.\r\n *\r\n * @example\r\n * bindClass(el, () => ({ active: isActive(), disabled: isDisabled() }));\r\n */\r\nexport function bindClass(\r\n el: Element,\r\n getter: () => Record<string, boolean>\r\n): EffectHandle {\r\n let prevClasses: Record<string, boolean> = {};\r\n\r\n return effect(() => {\r\n const next = getter();\r\n\r\n // Remove classes that were previously active but are no longer present or are now false.\r\n for (const name of Object.keys(prevClasses)) {\r\n if (prevClasses[name] === true && !next[name]) {\r\n el.classList.remove(name);\r\n }\r\n }\r\n\r\n // Add/remove based on current values.\r\n for (const [name, active] of Object.entries(next)) {\r\n if (active) {\r\n el.classList.add(name);\r\n } else {\r\n el.classList.remove(name);\r\n }\r\n }\r\n\r\n prevClasses = next;\r\n });\r\n}\r\n\r\n/**\r\n * Reactively renders a list of items before an anchor comment node.\r\n * Each item receives its own `ComponentContext` scoped under `parentCtx`\r\n * so that bindings created inside the item template are properly cleaned\r\n * up whenever the list re-renders.\r\n *\r\n * This is the runtime backing for the `@for` template directive.\r\n *\r\n * @example\r\n * // Generated by: <li @for={item of items()}>…</li>\r\n * const anchor = document.createComment('for');\r\n * insert(container, anchor);\r\n * ctx.effects.push(bindList(anchor, ctx, () => items(), (item, i, itemCtx) => {\r\n * const li = createElement('li');\r\n * const t = document.createTextNode('');\r\n * itemCtx.effects.push(bindText(t, () => item.name));\r\n * insert(li, t);\r\n * return li;\r\n * }));\r\n */\r\nexport function bindList<T>(\r\n anchor: Comment,\r\n parentCtx: ComponentContext,\r\n getter: () => T[],\r\n itemFactory: (item: T, index: number, ctx: ComponentContext) => Node,\r\n): EffectHandle {\r\n let activeItems: Array<{ node: Node; ctx: ComponentContext }> = [];\r\n\r\n return effect(() => {\r\n const list = getter();\r\n const parent = anchor.parentNode;\r\n if (!parent) return;\r\n\r\n // Tear down previous iteration: remove DOM nodes and destroy child contexts.\r\n for (const { node, ctx } of activeItems) {\r\n parent.removeChild(node);\r\n const idx = parentCtx.children.indexOf(ctx);\r\n if (idx >= 0) parentCtx.children.splice(idx, 1);\r\n destroyComponent(ctx);\r\n }\r\n activeItems = [];\r\n\r\n // Render each new item and insert it before the anchor.\r\n for (let i = 0; i < list.length; i++) {\r\n const itemCtx = createComponent(parentCtx.injector);\r\n parentCtx.children.push(itemCtx);\r\n const node = itemFactory(list[i]!, i, itemCtx);\r\n parent.insertBefore(node, anchor);\r\n activeItems.push({ node, ctx: itemCtx });\r\n }\r\n });\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// 5.3 Component lifecycle\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Creates a ComponentContext scoped to a child injector derived from\r\n * `parentInjector`. The context owns all EffectHandles created during\r\n * the component's lifetime.\r\n *\r\n * @example\r\n * const ctx = createComponent(app, [{ provide: MY_TOKEN, useValue: 42 }]);\r\n */\r\nexport function createComponent(\r\n parentInjector: Injector,\r\n providers?: Provider[]\r\n): ComponentContext {\r\n const injector = parentInjector.createChild(providers ?? []);\r\n return {\r\n injector,\r\n effects: [],\r\n children: [],\r\n };\r\n}\r\n\r\n/**\r\n * Tears down a ComponentContext: destroys all child contexts recursively,\r\n * then destroys all owned effects, then destroys the injector.\r\n *\r\n * @example\r\n * destroyComponent(ctx);\r\n */\r\nexport function destroyComponent(ctx: ComponentContext): void {\r\n // Recursively destroy children first.\r\n for (const child of ctx.children) {\r\n destroyComponent(child);\r\n }\r\n ctx.children.length = 0;\r\n\r\n // Destroy all owned effect handles.\r\n for (const handle of ctx.effects) {\r\n handle.destroy();\r\n }\r\n ctx.effects.length = 0;\r\n\r\n // Tear down the scoped injector.\r\n ctx.injector.destroy();\r\n}\r\n\r\n/**\r\n * Mounts a compiled component factory into a container element. The factory\r\n * receives the ComponentContext, builds the DOM subtree, and returns the\r\n * root node which is then appended to the container.\r\n *\r\n * @example\r\n * mountComponent(counterFactory, document.getElementById('app')!, ctx);\r\n */\r\nexport function mountComponent(\r\n factory: (ctx: ComponentContext, props?: Record<string, () => unknown>) => Node,\r\n container: Element,\r\n ctx: ComponentContext\r\n): void {\r\n const node = factory(ctx);\r\n container.appendChild(node);\r\n}\r\n\r\n/**\r\n * Instantiates a child component inside a parent's template. Creates a child\r\n * ComponentContext scoped under the parent, registers it for lifecycle\r\n * tracking, invokes the factory with the given props, and returns the root\r\n * DOM node so the caller can insert it into the tree.\r\n *\r\n * Props are always passed as getter functions so both static and reactive\r\n * values share a uniform call-site API: `props['label']()`.\r\n *\r\n * @example\r\n * // In a compiled parent template:\r\n * const _e1 = mountChild(MyButton, ctx, { label: () => 'Click me' });\r\n * insert(_e0, _e1);\r\n */\r\nexport function mountChild(\r\n factory: (ctx: ComponentContext, props: Record<string, () => unknown>) => Node,\r\n parentCtx: ComponentContext,\r\n props: Record<string, () => unknown> = {},\r\n): Node {\r\n const childCtx = createComponent(parentCtx.injector);\r\n parentCtx.children.push(childCtx);\r\n const node = factory(childCtx, props);\r\n\r\n // Register the instance for HMR swapping when the runtime is active.\r\n const hmrId = (factory as unknown as Record<string, unknown>)['__hmrId'] as string | undefined;\r\n if (hmrId) {\r\n const w = typeof window !== 'undefined' ? (window as unknown as Record<string, unknown>) : null;\r\n const hmr = w?.['__vorra_hmr'] as { instances?: Map<string, HMRInstance[]> } | undefined;\r\n if (hmr?.instances) {\r\n const list = hmr.instances.get(hmrId) ?? [];\r\n list.push({ node, ctx: childCtx, props, parentCtx });\r\n hmr.instances.set(hmrId, list);\r\n }\r\n }\r\n\r\n return node;\r\n}\r\n\r\n/**\r\n * Replaces all mounted instances of the component identified by `id` with\r\n * the output of `newFactory`. Called by the HMR client when a `.vorra` chunk\r\n * is hot-updated. Not intended for use in application code.\r\n */\r\nexport function hmrAccept(\r\n id: string,\r\n newFactory: (ctx: ComponentContext, props: Record<string, () => unknown>) => Node,\r\n): void {\r\n const w = typeof window !== 'undefined' ? (window as unknown as Record<string, unknown>) : null;\r\n const hmr = w?.['__vorra_hmr'] as { instances?: Map<string, HMRInstance[]> } | undefined;\r\n if (!hmr?.instances) return;\r\n\r\n const instances = hmr.instances.get(id) ?? [];\r\n for (const inst of instances) {\r\n const parent = inst.node.parentNode;\r\n if (!parent) continue; // component was unmounted — skip\r\n\r\n const nextSibling = inst.node.nextSibling;\r\n\r\n // Tear down old component subtree.\r\n destroyComponent(inst.ctx);\r\n parent.removeChild(inst.node);\r\n\r\n // Mount the new factory in its place.\r\n const newCtx = createComponent(inst.parentCtx.injector);\r\n const idx = inst.parentCtx.children.indexOf(inst.ctx);\r\n if (idx >= 0) inst.parentCtx.children[idx] = newCtx;\r\n\r\n const newNode = newFactory(newCtx, inst.props);\r\n parent.insertBefore(newNode, nextSibling);\r\n\r\n // Update the instance record for future HMR swaps.\r\n inst.node = newNode;\r\n inst.ctx = newCtx;\r\n }\r\n}\r\n\r\n// Wire up the HMR runtime on the global `window.__vorra_hmr` object so that\r\n// dynamically-imported component chunks can call `window.__vorra_hmr.accept`.\r\n// This block is compiled away in production builds (Rolldown replaces\r\n// `__vorra_dev` with `false` and tree-shakes the dead code).\r\nif (typeof __vorra_dev !== 'undefined' && __vorra_dev && typeof window !== 'undefined') {\r\n const w = window as unknown as Record<string, unknown>;\r\n if (!w['__vorra_hmr']) w['__vorra_hmr'] = {};\r\n const hmr = w['__vorra_hmr'] as Record<string, unknown>;\r\n if (!hmr['instances']) hmr['instances'] = new Map<string, HMRInstance[]>();\r\n hmr['accept'] = hmrAccept;\r\n}\r\n"],"mappings":"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"}