@bull-js/runtime 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bull contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,41 @@
1
- # Temporary Holding Version
1
+ # @bull-js/runtime
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The MVVM runtime of [Bull](https://github.com/nicolasandradedesarrollosit/bull.js): observable state,
4
+ ViewModels, commands, a dependency-injection container and the React bindings.
5
+
6
+ ```bash
7
+ npm install @bull-js/runtime react react-dom
8
+ ```
9
+
10
+ ```ts
11
+ import { ViewModel } from "@bull-js/runtime";
12
+
13
+ export class CounterViewModel extends ViewModel {
14
+ readonly #count = this.state(0);
15
+ readonly count = this.#count.readonly;
16
+ readonly double = this.computed(() => this.#count.value * 2);
17
+ readonly increment = this.command(() => this.#count.update((n) => n + 1));
18
+ }
19
+ ```
20
+
21
+ ```tsx
22
+ import { useBinding, useViewModel } from "@bull-js/runtime/react";
23
+
24
+ export function CounterView() {
25
+ const vm = useViewModel(CounterViewModel);
26
+ const count = useBinding(vm.count);
27
+ return <button onClick={vm.increment.execute}>{count}</button>;
28
+ }
29
+ ```
30
+
31
+ | Entry | Exports |
32
+ |---|---|
33
+ | `@bull-js/runtime` | `ViewModel`, `Command`, `Container`, `Token`, `state`, `computed`, `effect`, `batch`, `untracked` |
34
+ | `@bull-js/runtime/react` | `createBullApp`, `BullProvider`, `useViewModel`, `useChildViewModel`, `useBinding` |
35
+
36
+ The runtime works on its own, but Bull is meant to be used with [`@bull-js/cli`](https://www.npmjs.com/package/@bull-js/cli),
37
+ which refuses to build when the code breaks MVVM.
38
+
39
+ ## License
40
+
41
+ MIT
@@ -0,0 +1,54 @@
1
+ // src/container.ts
2
+ var Container = class {
3
+ #models = /* @__PURE__ */ new Map();
4
+ #viewModels = /* @__PURE__ */ new Map();
5
+ /** Registers a Model (repository, service or shared entity). */
6
+ model(token, factory) {
7
+ this.#models.set(token, { factory });
8
+ return this;
9
+ }
10
+ /** Registers how to build a ViewModel and inject its dependencies. */
11
+ viewModel(token, factory) {
12
+ this.#viewModels.set(token, factory);
13
+ return this;
14
+ }
15
+ /** Gets a registered Model. */
16
+ get(token) {
17
+ const entry = this.#models.get(token);
18
+ if (!entry) {
19
+ if (this.#viewModels.has(token)) {
20
+ throw new Error(
21
+ `[bull] ${token.name} is a ViewModel. ViewModels are not requested from the container: the View creates them with useViewModel(${token.name}).`
22
+ );
23
+ }
24
+ throw new Error(
25
+ `[bull] No Model is registered as ${token.name}.
26
+ Register it in the composition root (src/main.tsx):
27
+ c.model(${token.name}, () => new ${token.name}())`
28
+ );
29
+ }
30
+ if (!("instance" in entry)) entry.instance = entry.factory(this);
31
+ return entry.instance;
32
+ }
33
+ /** Creates a new ViewModel instance. Used by `useViewModel`. */
34
+ createViewModel(token) {
35
+ const factory = this.#viewModels.get(token);
36
+ if (factory) return factory(this);
37
+ if (token.length === 0) return new token();
38
+ throw new Error(
39
+ `[bull] ${token.name} needs dependencies, but no factory is registered.
40
+ Register it in the composition root (src/main.tsx):
41
+ c.viewModel(${token.name}, (c) => new ${token.name}(c.get(MyRepository)))`
42
+ );
43
+ }
44
+ };
45
+
46
+ // src/lifecycle.ts
47
+ var ACTIVATE = /* @__PURE__ */ Symbol.for("bull.viewmodel.activate");
48
+ var DEACTIVATE = /* @__PURE__ */ Symbol.for("bull.viewmodel.deactivate");
49
+
50
+ export {
51
+ Container,
52
+ ACTIVATE,
53
+ DEACTIVATE
54
+ };
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Minimal signals-based reactivity.
3
+ *
4
+ * - `State` mutable source of truth (only the ViewModel writes it).
5
+ * - `Computed` derived value, lazy and cached.
6
+ * - `effect` reaction that re-runs when its dependencies change.
7
+ *
8
+ * Push-pull propagation: sources mark their observers as "dirty" and values
9
+ * are only recomputed when someone reads them. Reactions (effects and
10
+ * subscribers) are queued and run at the end of the batch, so they never see
11
+ * an intermediate state.
12
+ */
13
+ /** Something that can be read and observed. It is all the View ever sees. */
14
+ interface Readable<T> {
15
+ /** Reads the value and tracks the dependency if there is an active observer. */
16
+ readonly value: T;
17
+ /** Reads the value without tracking. */
18
+ peek(): T;
19
+ /** Subscribes to changes. Returns the unsubscribe function. */
20
+ readonly subscribe: (listener: () => void) => () => void;
21
+ }
22
+ /** Read-only view of a `State`. It is what a ViewModel exposes. */
23
+ type ReadonlyState<T> = Readable<T>;
24
+ interface Source {
25
+ observers: Set<Observer>;
26
+ }
27
+ interface Observer {
28
+ sources: Set<Source>;
29
+ /** Called by a source when it changes. */
30
+ invalidate(): void;
31
+ }
32
+ /** Groups several writes: reactions run only once, at the end. */
33
+ declare function batch<T>(fn: () => T): T;
34
+ /** Runs `fn` without tracking dependencies. */
35
+ declare function untracked<T>(fn: () => T): T;
36
+ declare class State<T> implements Readable<T>, Source {
37
+ #private;
38
+ readonly observers: Set<Observer>;
39
+ constructor(initial: T);
40
+ get value(): T;
41
+ peek(): T;
42
+ set(next: T): void;
43
+ update(fn: (current: T) => T): void;
44
+ readonly subscribe: (listener: () => void) => (() => void);
45
+ /** Read-only view: what the ViewModel should expose to the View. */
46
+ get readonly(): ReadonlyState<T>;
47
+ }
48
+ declare class Computed<T> implements Readable<T>, Source, Observer {
49
+ #private;
50
+ readonly observers: Set<Observer>;
51
+ readonly sources: Set<Source>;
52
+ constructor(fn: () => T);
53
+ invalidate(): void;
54
+ get value(): T;
55
+ peek(): T;
56
+ readonly subscribe: (listener: () => void) => (() => void);
57
+ }
58
+ /**
59
+ * Runs `fn` now and every time something it reads changes.
60
+ * `fn` may return a cleanup function. Returns the function that stops the effect.
61
+ */
62
+ declare function effect(fn: () => void | (() => void)): () => void;
63
+ declare function state<T>(initial: T): State<T>;
64
+ declare function computed<T>(fn: () => T): Computed<T>;
65
+
66
+ interface CommandOptions {
67
+ /** Extra condition to be able to execute (for example, a valid form). It is reactive. */
68
+ canExecute?: () => boolean;
69
+ }
70
+ /** What `execute` returns: `undefined` if the command could not run or failed. */
71
+ type CommandResult<R> = R extends PromiseLike<infer U> ? Promise<U | undefined> : R | undefined;
72
+ /**
73
+ * An action the ViewModel exposes to the View.
74
+ *
75
+ * The View never changes state: it calls `vm.something.execute(...)`. The
76
+ * command publishes its own observable state (`isRunning`, `error`,
77
+ * `canExecute`) so the View can disable buttons or show errors without logic.
78
+ */
79
+ declare class Command<A extends unknown[] = [], R = void> {
80
+ #private;
81
+ /** `true` while an async command is running. */
82
+ readonly isRunning: ReadonlyState<boolean>;
83
+ /** The last error, or `undefined`. Cleared on the next execution. */
84
+ readonly error: ReadonlyState<unknown>;
85
+ /** `false` while running or when `options.canExecute` returns `false`. */
86
+ readonly canExecute: ReadonlyState<boolean>;
87
+ constructor(fn: (...args: A) => R, options?: CommandOptions);
88
+ /** Runs the action. It is bound, so it can be passed straight to an `onClick`. */
89
+ readonly execute: (...args: A) => CommandResult<R>;
90
+ }
91
+
92
+ /** Internal lifecycle keys. Used by `useViewModel`; not part of the public API. */
93
+ declare const ACTIVATE: unique symbol;
94
+ declare const DEACTIVATE: unique symbol;
95
+
96
+ type EffectFn = () => void | (() => void);
97
+ /**
98
+ * Base class of every ViewModel.
99
+ *
100
+ * A ViewModel:
101
+ * - receives its Model dependencies through the constructor,
102
+ * - keeps state with `this.state()` and exposes it with `.readonly`,
103
+ * - derives data with `this.computed()`,
104
+ * - exposes actions with `this.command()`,
105
+ * - knows nothing about React, the DOM or how it is drawn.
106
+ *
107
+ * @example
108
+ * export class CounterViewModel extends ViewModel {
109
+ * readonly #count = this.state(0);
110
+ * readonly count = this.#count.readonly;
111
+ * readonly increment = this.command(() => this.#count.update((n) => n + 1));
112
+ * }
113
+ */
114
+ declare abstract class ViewModel {
115
+ #private;
116
+ protected state<T>(initial: T): State<T>;
117
+ protected computed<T>(fn: () => T): Computed<T>;
118
+ protected command<A extends unknown[] = [], R = void>(fn: (...args: A) => R, options?: CommandOptions): Command<A, R>;
119
+ /** Registers a reaction that runs while the associated View is mounted. */
120
+ protected effect(fn: EffectFn): void;
121
+ /**
122
+ * Called when the View that uses this ViewModel mounts. It is the place to
123
+ * load initial data. It may return a cleanup function.
124
+ */
125
+ protected onActivate(): void | (() => void);
126
+ get isActive(): boolean;
127
+ [ACTIVATE](): void;
128
+ [DEACTIVATE](): void;
129
+ }
130
+
131
+ type Token<T> = abstract new (...args: any[]) => T;
132
+ type Factory<T> = (container: Container) => T;
133
+ /**
134
+ * Dependency registry of the composition root (`src/main.tsx`).
135
+ *
136
+ * - Models are singletons: created once and shared.
137
+ * - ViewModels are created once per View that uses them.
138
+ */
139
+ declare class Container {
140
+ #private;
141
+ /** Registers a Model (repository, service or shared entity). */
142
+ model<T>(token: Token<T>, factory: Factory<T>): this;
143
+ /** Registers how to build a ViewModel and inject its dependencies. */
144
+ viewModel<T extends ViewModel>(token: Token<T>, factory: Factory<T>): this;
145
+ /** Gets a registered Model. */
146
+ get<T>(token: Token<T>): T;
147
+ /** Creates a new ViewModel instance. Used by `useViewModel`. */
148
+ createViewModel<T extends ViewModel>(token: Token<T>): T;
149
+ }
150
+
151
+ export { Command, type CommandOptions, type CommandResult, Computed, Container, type Readable, type ReadonlyState, State, type Token, ViewModel, batch, computed, effect, state, untracked };
package/dist/index.js ADDED
@@ -0,0 +1,275 @@
1
+ import {
2
+ ACTIVATE,
3
+ Container,
4
+ DEACTIVATE
5
+ } from "./chunk-QTHQ6FIG.js";
6
+
7
+ // src/reactive.ts
8
+ var activeObserver = null;
9
+ var batchDepth = 0;
10
+ var pendingReactions = /* @__PURE__ */ new Set();
11
+ function track(source) {
12
+ if (!activeObserver) return;
13
+ source.observers.add(activeObserver);
14
+ activeObserver.sources.add(source);
15
+ }
16
+ function untrackAll(observer) {
17
+ for (const source of observer.sources) source.observers.delete(observer);
18
+ observer.sources.clear();
19
+ }
20
+ function runTracked(observer, fn) {
21
+ untrackAll(observer);
22
+ const previous = activeObserver;
23
+ activeObserver = observer;
24
+ try {
25
+ return fn();
26
+ } finally {
27
+ activeObserver = previous;
28
+ }
29
+ }
30
+ function invalidateObservers(source) {
31
+ for (const observer of [...source.observers]) observer.invalidate();
32
+ }
33
+ function flush() {
34
+ while (pendingReactions.size > 0) {
35
+ const reactions = [...pendingReactions];
36
+ pendingReactions.clear();
37
+ for (const reaction of reactions) reaction();
38
+ }
39
+ }
40
+ function batch(fn) {
41
+ batchDepth++;
42
+ try {
43
+ return fn();
44
+ } finally {
45
+ batchDepth--;
46
+ if (batchDepth === 0) flush();
47
+ }
48
+ }
49
+ function untracked(fn) {
50
+ const previous = activeObserver;
51
+ activeObserver = null;
52
+ try {
53
+ return fn();
54
+ } finally {
55
+ activeObserver = previous;
56
+ }
57
+ }
58
+ function subscribeTo(source, listener) {
59
+ const reaction = () => {
60
+ source.peek();
61
+ listener();
62
+ };
63
+ const observer = {
64
+ sources: /* @__PURE__ */ new Set(),
65
+ invalidate: () => {
66
+ pendingReactions.add(reaction);
67
+ if (batchDepth === 0) flush();
68
+ }
69
+ };
70
+ runTracked(observer, () => {
71
+ track(source);
72
+ source.peek();
73
+ });
74
+ return () => {
75
+ untrackAll(observer);
76
+ pendingReactions.delete(reaction);
77
+ };
78
+ }
79
+ var State = class {
80
+ observers = /* @__PURE__ */ new Set();
81
+ #value;
82
+ #readonly;
83
+ constructor(initial) {
84
+ this.#value = initial;
85
+ }
86
+ get value() {
87
+ track(this);
88
+ return this.#value;
89
+ }
90
+ peek() {
91
+ return this.#value;
92
+ }
93
+ set(next) {
94
+ if (Object.is(next, this.#value)) return;
95
+ this.#value = next;
96
+ batch(() => invalidateObservers(this));
97
+ }
98
+ update(fn) {
99
+ this.set(fn(this.#value));
100
+ }
101
+ subscribe = (listener) => subscribeTo(this, listener);
102
+ /** Read-only view: what the ViewModel should expose to the View. */
103
+ get readonly() {
104
+ if (!this.#readonly) {
105
+ const self = this;
106
+ this.#readonly = Object.freeze({
107
+ get value() {
108
+ return self.value;
109
+ },
110
+ peek: () => self.peek(),
111
+ subscribe: self.subscribe
112
+ });
113
+ }
114
+ return this.#readonly;
115
+ }
116
+ };
117
+ var Computed = class {
118
+ observers = /* @__PURE__ */ new Set();
119
+ sources = /* @__PURE__ */ new Set();
120
+ #dirty = true;
121
+ #value;
122
+ #fn;
123
+ constructor(fn) {
124
+ this.#fn = fn;
125
+ }
126
+ invalidate() {
127
+ if (this.#dirty) return;
128
+ this.#dirty = true;
129
+ invalidateObservers(this);
130
+ }
131
+ get value() {
132
+ track(this);
133
+ return this.peek();
134
+ }
135
+ peek() {
136
+ if (this.#dirty) {
137
+ this.#value = runTracked(this, this.#fn);
138
+ this.#dirty = false;
139
+ }
140
+ return this.#value;
141
+ }
142
+ subscribe = (listener) => subscribeTo(this, listener);
143
+ };
144
+ function effect(fn) {
145
+ let cleanup;
146
+ let disposed = false;
147
+ const observer = {
148
+ sources: /* @__PURE__ */ new Set(),
149
+ invalidate: () => {
150
+ if (disposed) return;
151
+ pendingReactions.add(run);
152
+ if (batchDepth === 0) flush();
153
+ }
154
+ };
155
+ function run() {
156
+ if (disposed) return;
157
+ if (typeof cleanup === "function") cleanup();
158
+ cleanup = runTracked(observer, fn);
159
+ }
160
+ run();
161
+ return () => {
162
+ disposed = true;
163
+ untrackAll(observer);
164
+ pendingReactions.delete(run);
165
+ if (typeof cleanup === "function") cleanup();
166
+ };
167
+ }
168
+ function state(initial) {
169
+ return new State(initial);
170
+ }
171
+ function computed(fn) {
172
+ return new Computed(fn);
173
+ }
174
+
175
+ // src/command.ts
176
+ var Command = class {
177
+ #running = new State(false);
178
+ #error = new State(void 0);
179
+ #fn;
180
+ /** `true` while an async command is running. */
181
+ isRunning = this.#running.readonly;
182
+ /** The last error, or `undefined`. Cleared on the next execution. */
183
+ error = this.#error.readonly;
184
+ /** `false` while running or when `options.canExecute` returns `false`. */
185
+ canExecute;
186
+ constructor(fn, options = {}) {
187
+ this.#fn = fn;
188
+ const extra = options.canExecute;
189
+ this.canExecute = new Computed(() => !this.#running.value && (extra ? extra() : true));
190
+ }
191
+ /** Runs the action. It is bound, so it can be passed straight to an `onClick`. */
192
+ execute = (...args) => {
193
+ if (!this.canExecute.peek()) return void 0;
194
+ this.#error.set(void 0);
195
+ let result;
196
+ try {
197
+ result = this.#fn(...args);
198
+ } catch (error) {
199
+ this.#fail(error);
200
+ return void 0;
201
+ }
202
+ if (!isThenable(result)) return result;
203
+ this.#running.set(true);
204
+ return Promise.resolve(result).then(
205
+ (value) => {
206
+ this.#running.set(false);
207
+ return value;
208
+ },
209
+ (error) => {
210
+ this.#running.set(false);
211
+ this.#fail(error);
212
+ return void 0;
213
+ }
214
+ );
215
+ };
216
+ #fail(error) {
217
+ this.#error.set(error);
218
+ console.error("[bull] A command failed:", error);
219
+ }
220
+ };
221
+ function isThenable(value) {
222
+ return typeof value?.then === "function";
223
+ }
224
+
225
+ // src/ViewModel.ts
226
+ var ViewModel = class {
227
+ #effects = [];
228
+ #running = [];
229
+ #activations = 0;
230
+ state(initial) {
231
+ return new State(initial);
232
+ }
233
+ computed(fn) {
234
+ return new Computed(fn);
235
+ }
236
+ command(fn, options) {
237
+ return new Command(fn, options);
238
+ }
239
+ /** Registers a reaction that runs while the associated View is mounted. */
240
+ effect(fn) {
241
+ this.#effects.push(fn);
242
+ if (this.#activations > 0) this.#running.push(effect(fn));
243
+ }
244
+ /**
245
+ * Called when the View that uses this ViewModel mounts. It is the place to
246
+ * load initial data. It may return a cleanup function.
247
+ */
248
+ onActivate() {
249
+ }
250
+ get isActive() {
251
+ return this.#activations > 0;
252
+ }
253
+ [ACTIVATE]() {
254
+ if (this.#activations++ > 0) return;
255
+ this.#running = this.#effects.map((fn) => effect(fn));
256
+ const cleanup = this.onActivate();
257
+ if (typeof cleanup === "function") this.#running.push(cleanup);
258
+ }
259
+ [DEACTIVATE]() {
260
+ if (this.#activations === 0 || --this.#activations > 0) return;
261
+ for (const stop of this.#running.splice(0)) stop();
262
+ }
263
+ };
264
+ export {
265
+ Command,
266
+ Computed,
267
+ Container,
268
+ State,
269
+ ViewModel,
270
+ batch,
271
+ computed,
272
+ effect,
273
+ state,
274
+ untracked
275
+ };
@@ -0,0 +1,38 @@
1
+ import * as react from 'react';
2
+ import { ComponentType, ReactNode } from 'react';
3
+ import { Container, Readable, ViewModel, Token } from './index.js';
4
+
5
+ declare function BullProvider(props: {
6
+ container: Container;
7
+ children?: ReactNode;
8
+ }): react.JSX.Element;
9
+ /**
10
+ * Connects a View with its ViewModel. Creates one instance per component,
11
+ * activates it on mount and deactivates it on unmount.
12
+ */
13
+ declare function useViewModel<T extends ViewModel>(token: Token<T>): T;
14
+ /**
15
+ * Activates a child ViewModel that a parent passed to a View through props
16
+ * (for example, a `TodoItemViewModel` inside a list).
17
+ */
18
+ declare function useChildViewModel<T extends ViewModel>(vm: T): T;
19
+ /** Reads an observable ViewModel value and re-renders when it changes. */
20
+ declare function useBinding<T>(source: Readable<T>): T;
21
+ interface BullAppOptions {
22
+ /** The root View of the application. */
23
+ root: ComponentType;
24
+ /** Registers the Models and ViewModels. It is the app's dependency injection. */
25
+ register?: (container: Container) => void;
26
+ /** Element or selector where the app is mounted. Defaults to `#root`. */
27
+ element?: Element | string;
28
+ /** Wraps the app in `<StrictMode>`. Defaults to `true`. */
29
+ strict?: boolean;
30
+ }
31
+ interface BullApp {
32
+ container: Container;
33
+ unmount(): void;
34
+ }
35
+ /** Mounts the application. Called once, from the composition root. */
36
+ declare function createBullApp(options: BullAppOptions): BullApp;
37
+
38
+ export { type BullApp, type BullAppOptions, BullProvider, Container, createBullApp, useBinding, useChildViewModel, useViewModel };
package/dist/react.js ADDED
@@ -0,0 +1,66 @@
1
+ import {
2
+ ACTIVATE,
3
+ Container,
4
+ DEACTIVATE
5
+ } from "./chunk-QTHQ6FIG.js";
6
+
7
+ // src/react.tsx
8
+ import {
9
+ createContext,
10
+ StrictMode,
11
+ useContext,
12
+ useEffect,
13
+ useState,
14
+ useSyncExternalStore
15
+ } from "react";
16
+ import { createRoot } from "react-dom/client";
17
+ import { jsx } from "react/jsx-runtime";
18
+ var ContainerContext = createContext(null);
19
+ function BullProvider(props) {
20
+ return /* @__PURE__ */ jsx(ContainerContext.Provider, { value: props.container, children: props.children });
21
+ }
22
+ function useViewModel(token) {
23
+ const container = useContext(ContainerContext);
24
+ if (!container) {
25
+ throw new Error(
26
+ `[bull] useViewModel(${token.name}) was called outside a Bull app. Mount the app with createBullApp() in src/main.tsx.`
27
+ );
28
+ }
29
+ const [vm] = useState(() => container.createViewModel(token));
30
+ useLifecycle(vm);
31
+ return vm;
32
+ }
33
+ function useChildViewModel(vm) {
34
+ useLifecycle(vm);
35
+ return vm;
36
+ }
37
+ function useLifecycle(vm) {
38
+ useEffect(() => {
39
+ vm[ACTIVATE]();
40
+ return () => vm[DEACTIVATE]();
41
+ }, [vm]);
42
+ }
43
+ function useBinding(source) {
44
+ const snapshot = () => source.peek();
45
+ return useSyncExternalStore(source.subscribe, snapshot, snapshot);
46
+ }
47
+ function createBullApp(options) {
48
+ const container = new Container();
49
+ options.register?.(container);
50
+ const target = options.element ?? "#root";
51
+ const element = typeof target === "string" ? document.querySelector(target) : target;
52
+ if (!element) throw new Error(`[bull] Element "${String(target)}" was not found in index.html.`);
53
+ const Root = options.root;
54
+ const tree = /* @__PURE__ */ jsx(BullProvider, { container, children: /* @__PURE__ */ jsx(Root, {}) });
55
+ const root = createRoot(element);
56
+ root.render(options.strict === false ? tree : /* @__PURE__ */ jsx(StrictMode, { children: tree }));
57
+ return { container, unmount: () => root.unmount() };
58
+ }
59
+ export {
60
+ BullProvider,
61
+ Container,
62
+ createBullApp,
63
+ useBinding,
64
+ useChildViewModel,
65
+ useViewModel
66
+ };
package/package.json CHANGED
@@ -1,6 +1,53 @@
1
1
  {
2
2
  "name": "@bull-js/runtime",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.0",
4
+ "description": "Bull's MVVM runtime: observable state, ViewModels, commands and React bindings.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/nicolasandradedesarrollosit/bull.js#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/nicolasandradedesarrollosit/bull.js.git",
11
+ "directory": "packages/runtime"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/nicolasandradedesarrollosit/bull.js/issues"
15
+ },
16
+ "keywords": [
17
+ "react",
18
+ "mvvm",
19
+ "viewmodel",
20
+ "signals",
21
+ "reactive",
22
+ "state-management",
23
+ "dependency-injection"
24
+ ],
25
+ "engines": {
26
+ "node": ">=20"
27
+ },
28
+ "sideEffects": false,
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "import": "./dist/index.js"
39
+ },
40
+ "./react": {
41
+ "types": "./dist/react.d.ts",
42
+ "import": "./dist/react.js"
43
+ },
44
+ "./package.json": "./package.json"
45
+ },
46
+ "scripts": {
47
+ "build": "tsup src/index.ts src/react.tsx --format esm --dts --clean --external react --external react-dom"
48
+ },
49
+ "peerDependencies": {
50
+ "react": ">=18",
51
+ "react-dom": ">=18"
52
+ }
53
+ }