@helfy/helfy 0.0.8 → 0.0.9

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.
Files changed (40) hide show
  1. package/README.md +264 -109
  2. package/babel-preset.js +2 -1
  3. package/dist/Helfy/Render/DOM.d.ts +2 -0
  4. package/dist/app/createApp.d.ts +3 -1
  5. package/dist/app/index.d.ts +1 -1
  6. package/dist/app/types.d.ts +10 -0
  7. package/dist/compiler/babel.js +20 -13
  8. package/dist/decorators/Context.decorator.d.ts +22 -5
  9. package/dist/decorators/Effect.decorator.d.ts +1 -1
  10. package/dist/decorators/Inject.decorator.d.ts +8 -3
  11. package/dist/decorators/InjectContainer.decorator.d.ts +16 -0
  12. package/dist/decorators/Store.decorator.d.ts +12 -3
  13. package/dist/decorators/UseCase.decorator.d.ts +21 -0
  14. package/dist/di/InjectParam.decorator.d.ts +0 -11
  15. package/dist/di/Injectable.decorator.d.ts +2 -0
  16. package/dist/di/createViewInstance.d.ts +4 -5
  17. package/dist/di/index.d.ts +3 -2
  18. package/dist/http/ApiClient.decorator.d.ts +19 -0
  19. package/dist/http/FetchHttpClient.d.ts +17 -0
  20. package/dist/http/HttpClient.types.d.ts +19 -0
  21. package/dist/http/Mutation.types.d.ts +22 -0
  22. package/dist/http/MutationBuilder.d.ts +17 -0
  23. package/dist/http/Query.types.d.ts +22 -0
  24. package/dist/http/QueryBuilder.d.ts +16 -0
  25. package/dist/http/QueryCache.d.ts +13 -0
  26. package/dist/http/index.d.ts +18 -0
  27. package/dist/http/mutationConfig.decorator.d.ts +19 -0
  28. package/dist/http/queryConfig.decorator.d.ts +9 -0
  29. package/dist/http/useInfiniteQuery.decorator.d.ts +45 -0
  30. package/dist/http/useMutation.decorator.d.ts +21 -0
  31. package/dist/http/useQuery.decorator.d.ts +25 -0
  32. package/dist/index.d.ts +6 -6
  33. package/dist/index.js +2 -2
  34. package/dist/router/core.d.ts +2 -2
  35. package/dist/router/decorators.d.ts +4 -4
  36. package/dist/router.js +1 -1
  37. package/dist/signals.d.ts +7 -1
  38. package/package.json +1 -1
  39. package/dist/decorators/Observe.decorator.d.ts +0 -1
  40. package/dist/decorators/Provide.decorator.d.ts +0 -22
package/README.md CHANGED
@@ -21,16 +21,20 @@ Components are defined as classes with the `@View` decorator, state is managed v
21
21
  - [@for](#for)
22
22
  - [@empty](#empty)
23
23
  - [State Management](#state-management)
24
- - [Creating a Store](#creating-a-store)
25
- - [Subscribing with @observe](#subscribing-with-observe)
26
- - [Store context](#store-context)
24
+ - [Architecture layers (Store, Service, UseCase)](#state-management)
25
+ - [Creating a Store](#creating-a-store-store)
26
+ - [Infrastructure services](#infrastructure-services-service)
27
+ - [Business use cases](#business-use-cases-usecase)
28
+ - [Accessing Store and UseCase from components](#accessing-store-and-usecase-from-components)
29
+ - [Store context (optional)](#store-context-optional)
27
30
  - [Logging (@logger)](#logging-logger)
28
31
  - [Context & DI](#context--di)
29
- - [Provider (@Context, @provide)](#provider-context-provide)
30
- - [Consumer (@ctx)](#consumer-ctx)
32
+ - [Provider (@Context)](#provider-context)
33
+ - [Consumer (@useCtx)](#consumer-usectx)
31
34
  - [Reactive fields](#reactive-fields)
32
35
  - [Optional injection](#optional-injection)
33
- - [DI Container](#di-container-constructor-injection)
36
+ - [DI: props vs dependencies](#di-props-vs-dependencies)
37
+ - [Global services (@inject)](#global-services-inject)
34
38
  - [JSX](#jsx)
35
39
  - [Attributes](#attributes)
36
40
  - [Events](#events)
@@ -78,7 +82,7 @@ Or in `package.json`:
78
82
 
79
83
  ### Build setup
80
84
 
81
- The Babel plugin runs the DI scanner before transformation (no pre-scripts in `package.json` needed). The scanner generates `.helfy/di-tokens.ts` and `.helfy/di-registry.ts`. Add `.helfy` to `.gitignore`.
85
+ The Babel plugin runs the DI scanner and context scanner before transformation (no pre-scripts in `package.json` needed). The scanners generate `.helfy/di-tokens.ts`, `.helfy/di-registry.ts`, and `.helfy/ctx-tokens.ts`. Add `.helfy` to `.gitignore`.
82
86
 
83
87
  ### Babel
84
88
 
@@ -90,9 +94,9 @@ A single preset in `.babelrc` is enough:
90
94
  }
91
95
  ```
92
96
 
93
- The preset includes: JSX runtime, TypeScript, legacy decorators, class properties, babel-plugin-transform-typescript-metadata, **helfy-di** (compile-time DI for `@Injectable<IX>()`, auto-registration at `createApp`, `@logger()` → `@logger("<ClassName>")` transformation).
97
+ The preset includes: JSX runtime, TypeScript, legacy decorators, class properties, babel-plugin-transform-typescript-metadata, **helfy-di** (compile-time DI for `@Injectable<IX>()`, `@Service<IX>()`, `@UseCase<IX>()`, `@Store`, `@inject<IX>()`, `@useCtx<IX>()`, auto-registration at `createApp`, `@logger()` → `@logger("<ClassName>")` transformation).
94
98
 
95
- ### Webpack
99
+ ### Webpack / Rspack
96
100
 
97
101
  To process directives (`@if`, `@for`, `@ref`, `@bind`, `@field`) in `.tsx` files, add the loader:
98
102
 
@@ -106,6 +110,8 @@ To process directives (`@if`, `@for`, `@ref`, `@bind`, `@field`) in `.tsx` files
106
110
  }
107
111
  ```
108
112
 
113
+ The same configuration works with both Webpack and Rspack. Rspack is a faster, Rust-based bundler with webpack-compatible API.
114
+
109
115
  ### TypeScript
110
116
 
111
117
  In `tsconfig.json` set:
@@ -152,7 +158,7 @@ The `@View` decorator automatically:
152
158
  - wraps `this.props` in a reactive Proxy (each prop is a signal)
153
159
  - configures fine-grained effects: when a signal changes, only the specific DOM node that reads it is updated, not the entire component
154
160
 
155
- > **Important:** When inheriting from `@View`, apply the decorator on the child class as well. Otherwise `@ctx`, `scheduleUpdate`, and DOM updates will not work.
161
+ > **Important:** When inheriting from `@View`, apply the decorator on the child class as well. Otherwise `@useCtx`, `scheduleUpdate`, and DOM updates will not work.
156
162
 
157
163
  ### Props
158
164
 
@@ -450,7 +456,7 @@ export class LoginForm {
450
456
  }
451
457
  ```
452
458
 
453
- Wrapper components (TextField, CheckboxField, etc.) accept `field` as a prop: `<TextField field={this.form.email} label="Email" />` and use `<input @field(this.props.field) />` internally.
459
+ Wrapper components (TextField, CheckboxField, etc.) accept `$field` as a writable prop: `<TextField $field={this.form.email} label="Email" />` and use `<input @field(this.props.$field) />` internally. The `$` prefix marks a writable prop — mutable objects like FieldState bypass the default signal-based (readonly) props so form inputs work correctly.
454
460
 
455
461
  **JSX directive `@field(expr)`** — one directive replaces `@bind` + `onblur` + error `class` + `aria-invalid`. The compiler generates:
456
462
 
@@ -555,105 +561,212 @@ The `@empty` block after `@for` renders when the array is empty:
555
561
 
556
562
  ## State Management
557
563
 
558
- ### Creating a Store
564
+ Helfy uses a three-layer architecture: **@Store** (pure state), **@Service** (infrastructure), **@UseCase** (business logic).
565
+
566
+ ### Dependency flow
567
+
568
+ ```
569
+ @UseCase ──→ @Store ←── @Service
570
+ │ ↑
571
+ └─────────────────┘
572
+ ```
573
+
574
+ - **@Store** — pure reactive state; no dependencies, no side effects. Store does not know about anyone.
575
+ - **@Service** — infrastructure (HTTP, validation, storage); can inject Store and other Services.
576
+ - **@UseCase** — orchestrates business scenarios; injects Store + Services.
559
577
 
560
- A Store is a global reactive store. Create a class with the `@Store` decorator; reactive fields use `@observable`:
578
+ ### Creating a Store (@Store)
579
+
580
+ A Store is a global reactive state container. **Requirements:**
581
+ - Empty constructor (no parameters)
582
+ - Only `@state` on fields and `@computed` on getters
583
+ - Mutation methods that only change own state (synchronous; no `await` — async work belongs in @Service)
584
+ - **Forbidden:** `@inject`, `@useCtx`, `@effect`, direct access to HTTP/storage/timers
561
585
 
562
586
  ```typescript
563
- import { Store, observable } from "@helfy/helfy";
587
+ import { Store, state, computed } from "@helfy/helfy";
564
588
 
565
589
  @Store
566
- class UserStore {
567
- @observable name = "Guest";
568
- @observable isLoggedIn = false;
569
-
570
- login(name: string) {
571
- this.name = name;
572
- this.isLoggedIn = true; // subscribers of name and isLoggedIn are notified
590
+ class TodoStore {
591
+ @state todos: Todo[] = [];
592
+ @state filter: "all" | "active" | "completed" = "all";
593
+
594
+ @computed get filteredTodos() {
595
+ switch (this.filter) {
596
+ case "active": return this.todos.filter(t => !t.completed);
597
+ case "completed": return this.todos.filter(t => t.completed);
598
+ default: return this.todos;
573
599
  }
600
+ }
574
601
 
575
- logout() {
576
- this.name = "Guest";
577
- this.isLoggedIn = false;
578
- }
579
- }
602
+ add(title: string) {
603
+ this.todos = [...this.todos, { id: crypto.randomUUID(), title, completed: false }];
604
+ }
580
605
 
581
- export default UserStore;
606
+ toggle(id: string) {
607
+ this.todos = this.todos.map(t =>
608
+ t.id === id ? { ...t, completed: !t.completed } : t
609
+ );
610
+ }
611
+
612
+ setFilter(f: typeof this.filter) {
613
+ this.filter = f;
614
+ }
615
+ }
582
616
  ```
583
617
 
584
- The `@Store` decorator adds:
585
- - `subscribe(field, callback)` — subscribe to field changes, returns unsubscribe function
586
- - `unsubscribe(field, callback)` — unsubscribe
618
+ Fields use `@state` or `@observable` — both create signals.
587
619
 
588
- The `@observable` decorator turns a field into a reactive property with getter/setter. On write, all subscribers are notified.
620
+ ### Infrastructure services (@Service)
589
621
 
590
- ### Subscribing with @observe
622
+ Use `@Service` for infrastructure: HTTP clients, validators, repositories. **Requirements:**
623
+ - Constructor with DI dependencies (other Services, Store)
624
+ - Provides atomic technical capabilities
625
+ - Can directly mutate Store (e.g. after fetching data)
626
+ - **Forbidden:** `@state`, `@computed`, `@effect` on fields
591
627
 
592
- The `@observe` decorator binds a component field to a store field. When the store value changes, the component re-renders:
628
+ ```typescript
629
+ import { Service } from "@helfy/helfy";
593
630
 
594
- ```tsx
595
- import { View, observe } from "@helfy/helfy";
596
- import Stores from "./StoreContext";
597
- import UserStore from "./UserStore";
631
+ export interface ITodoValidateService {
632
+ validateTitle(title: string): ValidationResult;
633
+ }
598
634
 
599
- @View
600
- class Header {
601
- @observe(Stores.userStore, 'name')
602
- private userName: UserStore['name'];
635
+ @Service<ITodoValidateService>()
636
+ export class TodoValidateService implements ITodoValidateService {
637
+ validateTitle(title: string) {
638
+ // ...
639
+ }
640
+ }
641
+ ```
603
642
 
604
- @observe(Stores.userStore, 'isLoggedIn')
605
- private isLoggedIn: UserStore['isLoggedIn'];
643
+ ### Business use cases (@UseCase)
606
644
 
607
- render() {
608
- return (
609
- <header>
610
- @if (this.isLoggedIn) {
611
- <span>Hello, {this.userName}!</span>
612
- } @else {
613
- <span>Sign in</span>
614
- }
615
- </header>
616
- );
617
- }
645
+ Use `@UseCase` to orchestrate business scenarios. **Requirements:**
646
+ - Constructor with DI (Store, Services)
647
+ - Main public method: `execute`, `perform`, or `handle`
648
+ - Typical flow: validation → call services → update Store → side effects (notifications, analytics)
649
+ - **Forbidden:** `@state`, `@computed`, `@effect` on fields
650
+
651
+ ```typescript
652
+ import { UseCase } from "@helfy/helfy";
653
+
654
+ @UseCase<ICreateTodoUseCase>()
655
+ export class CreateTodoUseCase implements ICreateTodoUseCase {
656
+ constructor(
657
+ private store: TodoStore,
658
+ private validateService: ITodoValidateService
659
+ ) {}
660
+
661
+ async execute(dto: CreateTodoDto): Promise<Result<Todo, AppError>> {
662
+ const validation = this.validateService.validateTitle(dto.title);
663
+ if (!validation.valid) return Err(validation.error);
664
+
665
+ const todo = { id: crypto.randomUUID(), ...dto, completed: false };
666
+ this.store.add(todo);
667
+ return Ok(todo);
668
+ }
618
669
  }
619
670
  ```
620
671
 
621
- The field type is inferred from the store via lookup type (`UserStore['name']`), giving full type safety.
672
+ ### HttpClient and ApiClient
622
673
 
623
- ### Store context
674
+ Helfy provides an HTTP client and a Query/Mutation layer (similar to TanStack Query) for API data fetching.
675
+
676
+ **Configure HttpClient** in `createApp`:
677
+
678
+ ```typescript
679
+ createApp({ root: document.getElementById("root")! })
680
+ .http({
681
+ baseUrl: "/api",
682
+ timeout: 10000,
683
+ headers: { "X-App-Version": "1.0" },
684
+ queryCacheMaxSize: 100,
685
+ })
686
+ .router({ routes })
687
+ .mount(App);
688
+ ```
624
689
 
625
- Create a single context for all stores:
690
+ **Define API interface and implementation** with `@ApiClient` and `@queryConfig`:
626
691
 
627
692
  ```typescript
628
- import UserStore from "./UserStore";
629
- import AppStore from "./AppStore";
693
+ import { ApiClient, queryConfig, QueryBuilder, type Query, type HttpClient } from "@helfy/helfy";
694
+
695
+ export interface TodoApi {
696
+ todos(): Query<Todo[]>;
697
+ }
698
+
699
+ @ApiClient<TodoApi>()
700
+ export class TodoApiImpl implements TodoApi {
701
+ constructor(private readonly http: HttpClient) {}
702
+
703
+ @queryConfig("todos")
704
+ todos() {
705
+ return new QueryBuilder<Todo[]>(["todo"])
706
+ .fn(() => this.http.get<Todo[]>("/todos"))
707
+ .staleTime(5 * 60 * 1000)
708
+ .refetchOnWindowFocus(true);
709
+ }
710
+ }
711
+ ```
630
712
 
631
- class Stores {
632
- static readonly userStore = new UserStore();
633
- static readonly appStore = new AppStore();
713
+ **Use `@useQuery` in View** for automatic fetch on mount:
714
+
715
+ ```tsx
716
+ @View
717
+ class TodoList {
718
+ @useQuery<TodoApi>("todos") private todosQuery!: Query<Todo[]>;
719
+
720
+ render() {
721
+ const q = this.todosQuery;
722
+ if (q.isLoading) return <Spinner />;
723
+ if (q.isError) return <ErrorMessage error={q.error} />;
724
+ return <ul>{q.data?.map((t) => <li>{t.text}</li>)}</ul>;
725
+ }
634
726
  }
727
+ ```
728
+
729
+ **Use `@useMutation`** for imperative mutations:
730
+
731
+ ```tsx
732
+ import type { Mutation } from "@helfy/helfy";
733
+
734
+ @useMutation<TodoApi>("create") private createTodo!: Mutation<TodoItem, AddTodoDto>;
635
735
 
636
- export default Stores;
736
+ async handleSubmit() {
737
+ await this.createTodo.mutateAsync({ title: this.title });
738
+ }
637
739
  ```
638
740
 
639
- Usage in components:
741
+ ### Accessing Store and UseCase from components
742
+
743
+ Inject Store and UseCase via `@inject` in View/Context. Prefer UseCase for mutations, Store for reads:
640
744
 
641
745
  ```tsx
642
- import Stores from "./StoreContext";
746
+ import { View, inject } from "@helfy/helfy";
643
747
 
644
- // subscribe via decorator
645
- @observe(Stores.userStore, 'name')
646
- private userName: string;
748
+ @View
749
+ class TodoList {
750
+ @inject<ITodoStore>() private store!: ITodoStore;
751
+ @inject<ICreateTodoUseCase>() private createTodo!: ICreateTodoUseCase;
647
752
 
648
- // direct store method call
649
- Stores.userStore.login("Alice");
753
+ render() {
754
+ return (
755
+ <ul>
756
+ @for (todo of this.store.filteredTodos; track todo.id) {
757
+ <li>{todo.title}</li>
758
+ }
759
+ </ul>
760
+ );
761
+ }
762
+ }
650
763
  ```
651
764
 
652
765
  ---
653
766
 
654
767
  ## Logging (@logger)
655
768
 
656
- The `@logger` decorator injects a logger into View, Context, Store, and Injectable classes. The class name is set at compile time (Babel plugin), keeping readable names even after minification.
769
+ The `@logger` decorator injects a logger into View, Context, Store, Service, and UseCase classes. The class name is set at compile time (Babel plugin), keeping readable names even after minification.
657
770
 
658
771
  ### Usage
659
772
 
@@ -681,7 +794,8 @@ class TodoInput {
681
794
  | Type | Format | Color |
682
795
  |------|--------|-------|
683
796
  | View (components) | `<TodoInput>` | skyblue |
684
- | Injectable (services) | `TodoValidateService()` | pink |
797
+ | Service / Injectable | `TodoValidateService()` | pink |
798
+ | UseCase | `CreateTodoUseCase()` | pink |
685
799
  | Context | `{TodoContext}` | khaki |
686
800
  | Form | `[LoginFormContext]` | bright blue |
687
801
  | Store | `TodoStore[]` | lime green |
@@ -720,23 +834,24 @@ Without a registered logger in the container, a fallback that logs to `console`
720
834
 
721
835
  ## Context & DI
722
836
 
723
- Helfy supports hierarchical Context and Dependency Injection: a provider wraps a subtree in JSX, and child components receive values via `@ctx`. Useful for theme, forms, routing, and other shared dependencies.
837
+ Helfy supports hierarchical Context and Dependency Injection: a provider wraps a subtree in JSX, and child components receive values via `@useCtx`. Useful for theme, forms, routing, and other shared dependencies.
838
+
839
+ ### Provider (@Context)
724
840
 
725
- ### Provider (@Context, @provide)
841
+ A class with the `@Context` decorator is a non-rendering provider: it has no `render()` and only renders its children. Use `@state` for reactive fields, `@computed` for derived values; public methods are exposed automatically.
726
842
 
727
- A class with the `@Context` decorator is a non-rendering provider: it has no `render()` and only renders its children. Fields with `@provide` are available to consumers down the tree.
843
+ **Constructor:** receives only props from JSX. Define your own props interface (same as View). Use `@inject` for container dependencies.
728
844
 
729
845
  ```tsx
730
- import { Context, provide } from "@helfy/helfy";
846
+ import { Context, state } from "@helfy/helfy";
731
847
 
732
848
  export type TThemeMode = "light" | "dark";
733
849
 
734
850
  @Context
735
851
  export class ThemeContext {
736
- @provide({ reactive: true })
852
+ @state
737
853
  mode: TThemeMode = "dark";
738
854
 
739
- @provide()
740
855
  toggle = () => {
741
856
  this.mode = this.mode === "dark" ? "light" : "dark";
742
857
  };
@@ -754,17 +869,31 @@ Usage in JSX — wrap a subtree:
754
869
 
755
870
  The framework only renders the children of `ThemeContext`; the provider itself does not create DOM nodes.
756
871
 
757
- ### Consumer (@ctx)
872
+ **Context with typed props** — define an interface and use it in the constructor (same pattern as View):
873
+
874
+ ```tsx
875
+ interface ApiContextProps {
876
+ baseUrl: string;
877
+ }
878
+
879
+ @Context
880
+ export class ApiContext {
881
+ constructor(readonly props: ApiContextProps) {}
882
+ // ...
883
+ }
884
+ ```
885
+
886
+ ### Consumer (@useCtx)
758
887
 
759
- A component with `@View` can receive context values via `@ctx`:
888
+ A component with `@View` can receive context values via `@useCtx`:
760
889
 
761
890
  ```tsx
762
- import { View, ctx } from "@helfy/helfy";
891
+ import { View, useCtx } from "@helfy/helfy";
763
892
  import { ThemeContext } from "./ThemeContext";
764
893
 
765
894
  @View
766
895
  class ThemeToggle {
767
- @ctx(ThemeContext)
896
+ @useCtx(ThemeContext)
768
897
  private theme!: ThemeContext;
769
898
 
770
899
  render() {
@@ -782,7 +911,7 @@ You can inject only a context field:
782
911
  ```tsx
783
912
  @View
784
913
  class ModeDisplay {
785
- @ctx(ThemeContext, "mode")
914
+ @useCtx(ThemeContext, "mode")
786
915
  private mode!: TThemeMode;
787
916
 
788
917
  render() {
@@ -791,16 +920,26 @@ class ModeDisplay {
791
920
  }
792
921
  ```
793
922
 
923
+ **Interface-based injection** — for contexts with `@Context<IX>()`, use `@useCtx<ITodoContext>()`:
924
+
925
+ ```tsx
926
+ @useCtx<ITodoContext>()
927
+ private ctx!: ITodoContext;
928
+
929
+ @useCtx<ITodoContext>("filteredTodos")
930
+ private filteredTodos!: ITodoContext["filteredTodos"];
931
+ ```
932
+
794
933
  Provider lookup goes up the tree (`_parentView`); the nearest provider with the matching key is used.
795
934
 
796
935
  ### Reactive fields
797
936
 
798
- `@provide({ reactive: true })` makes a field reactive: when it changes, all consumers re-render. For methods, `@provide()` without `reactive` is enough.
937
+ `@state` makes a field reactive: when it changes, all consumers re-render. Public methods are exposed automatically.
799
938
 
800
- **Computed fields** — getters with `@provide({ computed: true, deps: ["field1", "field2"] })` recompute when dependencies change and trigger consumer re-renders:
939
+ **Computed fields** — getters with `@computed` recompute when dependencies change and trigger consumer re-renders:
801
940
 
802
941
  ```tsx
803
- @provide({ computed: true, deps: ["todos", "filter"] })
942
+ @computed
804
943
  get filteredTodos(): Todo[] {
805
944
  return this.filter === "active"
806
945
  ? this.todos.filter(t => !t.completed)
@@ -810,50 +949,60 @@ get filteredTodos(): Todo[] {
810
949
 
811
950
  ### Optional injection
812
951
 
813
- The third argument of `@ctx` — options `{ optional?, defaultValue? }`:
952
+ The third argument of `@useCtx` — options `{ optional?, defaultValue? }`:
814
953
 
815
954
  ```tsx
816
- @ctx(ThemeContext, { optional: true })
955
+ @useCtx(ThemeContext, { optional: true })
817
956
  private theme?: ThemeContext;
818
957
 
819
- @ctx(ThemeContext, "mode", { defaultValue: "light" })
958
+ @useCtx(ThemeContext, "mode", { defaultValue: "light" })
820
959
  private mode = "light";
821
960
  ```
822
961
 
823
962
  With no provider in the tree, `optional: true` yields `undefined`; `defaultValue` is used when the provider is absent.
824
963
 
825
- ### DI Container (constructor injection)
964
+ ### DI: props vs dependencies
965
+
966
+ **View and Context:** constructor receives only props from JSX. Dependencies are injected via field decorators:
967
+ - `@inject<IX>()` — from global container (Store, Service, UseCase)
968
+ - `@useCtx<IX>()` — from tree @Context / @Form providers
826
969
 
827
- Helfy supports a DI container for global services with **compile-time** magic: services are marked with `@Injectable<IX>()`, consumers only specify the type in the constructor — no `@inject` or explicit tokens.
970
+ **@Service and @UseCase:** constructor DI via `@diParams` (added by Babel plugin) for injected dependencies.
828
971
 
829
- **Convention:** the last constructor parameter is props from the parent; preceding parameters are resolved from the container or from @Context up the tree.
972
+ ### Global services (@inject)
830
973
 
831
- **Service** — must use `@Injectable<IX>()` (interface in generic):
974
+ Use `@inject<IX>()` in View/Context to access Store, Service, and UseCase from the container:
832
975
 
833
976
  ```typescript
977
+ // Define interface and implement with @Service
834
978
  export interface ITodoValidateService {
835
979
  validate(title: string): ValidationResult;
836
980
  }
837
981
 
838
- @Injectable<ITodoValidateService>()
982
+ @Service<ITodoValidateService>()
839
983
  export class TodoValidateService implements ITodoValidateService {
840
984
  validate(title: string) { ... }
841
985
  }
842
986
  ```
843
987
 
844
- **Consumer** — only the type in the constructor (plugin injects the token):
988
+ **Consumer (View)** — inject Store or UseCase:
845
989
 
846
990
  ```tsx
847
991
  @View
848
992
  class TodoInput {
849
- constructor(
850
- private readonly validateService: ITodoValidateService,
851
- readonly props: Props
852
- ) {}
993
+ @inject<ITodoValidateService>() private validateService!: ITodoValidateService;
994
+ @inject<ICreateTodoUseCase>() private createTodo!: ICreateTodoUseCase;
995
+
996
+ constructor(readonly props: Props) {}
997
+
998
+ render() {
999
+ // Call UseCase for mutations, Service for validation
1000
+ return <form onsubmit={() => this.createTodo.execute(this.form.getPayload())}>...</form>;
1001
+ }
853
1002
  }
854
1003
  ```
855
1004
 
856
- **Bootstrap** — DI registration is attached automatically at `createApp` (plugin injects `.useDI(registerAllServices)` in the chain):
1005
+ **Bootstrap** — DI registration is automatic at `createApp` (plugin injects `.useDI(registerAllServices)`):
857
1006
 
858
1007
  ```typescript
859
1008
  createApp({ root: document.getElementById("root")! })
@@ -861,9 +1010,9 @@ createApp({ root: document.getElementById("root")! })
861
1010
  .mount(App);
862
1011
  ```
863
1012
 
864
- The scanner is run by the Babel plugin before transformation, finds all `@Injectable<IX>()`, and generates `.helfy/di-tokens.ts` and `.helfy/di-registry.ts`.
1013
+ The DI scanner finds `@Store`, `@Service<IX>()`, `@UseCase<IX>()` and generates `.helfy/di-tokens.ts`, `.helfy/di-registry.ts`.
865
1014
 
866
- **Fallback** — for manual setup: `.configureContainer()`, `@inject(token)`, `@Injectable(token)`:
1015
+ **Fallback** — for manual setup: `.configureContainer()`, `@Injectable(token)`:
867
1016
 
868
1017
  ```typescript
869
1018
  createApp({ root })
@@ -949,19 +1098,25 @@ import styles from './App.module.css';
949
1098
  |-----------|-------|-------------|
950
1099
  | `@View` | Class | Turns a class into a component with `render()`, `view`, `update()` |
951
1100
  | `@state` | Field | Component local state on signals (write updates only this component) |
952
- | `@Store` | Class | Adds `subscribe`/`unsubscribe` and `@observable` field reactivity |
953
- | `@observable` | Field | Makes a store field reactive write notifies subscribers |
954
- | `@observe(store, field)` | Field | Binds a component field to a store field |
1101
+ | `@Store` | Class | Global reactive state. Empty constructor; only `@state`/`@computed`. No `@inject`, `@useCtx`, `@effect`, or external I/O |
1102
+ | `@observable` | Field | Alias for `@state` in Store; makes field reactive |
1103
+ | `@Service<IX>()` | Class | Infrastructure (HTTP, validation, repos). Constructor DI. No `@state`/`@computed`. Can mutate Store |
1104
+ | `@UseCase<IX>()` | Class | Business scenarios. Injects Store + Service. Main method: `execute`/`perform`/`handle`. No `@state`/`@computed` |
955
1105
  | `@Context` | Class | Non-rendering context provider; renders only children |
956
- | `@provide()` / `@provide({ reactive: true })` / `@provide({ computed: true, deps })` | Field | Marks a field as available to `@ctx`. `reactive` consumers re-render on change; `computed` + `deps` for getters |
957
- | `@ctx(ContextClass)` / `@ctx(ContextClass, field)` | Field | Injects context or a context field from the nearest provider up the tree |
958
- | `@Injectable<IX>()` / `@Injectable<IX>('singleton' \| 'transient' \| 'scoped')` / `@Injectable(token)` | Class | Marks a class as injectable. Scope in (): `'singleton'` (default), `'transient'`, `'scoped'`. Fallback: `@Injectable(token)` with Symbol/string |
959
- | `@inject(token)` | Constructor param | Sets the token for the parameter (fallback; with `@Injectable<IX>()` the plugin injects it automatically) |
1106
+ | `@state` / `@computed` | Context field | In `@Context`: `@state` for reactive fields, `@computed` for derived getters. Public methods exposed automatically |
1107
+ | `@inject<IX>()` | Field | Injects Store, Service, or UseCase from container. Use in View/Context |
1108
+ | `@useCtx(ContextClass)` / `@useCtx<IX>()` / `@useCtx(ContextClass, field)` | Field | Injects context or a context field from the nearest provider up the tree |
1109
+ | `@Injectable<IX>()` | Class | Generic injectable. Prefer `@Service` for infrastructure, `@UseCase` for business logic |
960
1110
  | `@logger()` / `@logger("tag")` | Field | Injects ILogger. No arg — compile-time class name and color by type (View/Context/Store/Injectable). With arg — custom tag (gray) |
961
1111
  | `@ref` | Field | Marks a field to receive a DOM or component reference (use with `@ref(this.fieldName)` in JSX) |
962
1112
  | `@expose` | Method | Makes a method available to the parent when using `@ref` on a component (without `@expose` the parent gets the full instance) |
963
1113
  | `@binded(name)` | Field | Binds a field to `@bind:name` from the parent (for custom components) |
964
1114
  | `@bind(expr)` / `@bind:name(expr)` | JSX | Two-way binding with `@state` field (value/checked + oninput/onchange). For components: `@bind:value(expr)` |
1115
+ | `@ApiClient<IX>()` | Class | API client with `@queryConfig` / `@mutationConfig` methods. Registers as Service by interface |
1116
+ | `@queryConfig(keyTemplate)` | Method | Marks method as Query; returns QueryBuilder chain, decorator calls `.build()` |
1117
+ | `@mutationConfig(options?)` | Method | Marks method as Mutation; merges invalidateQueries, optimisticFn into Mutation |
1118
+ | `@useQuery<ApiInterface>(keyOrGetter)` | Field | Injects Query from ApiClient, refetch on mount, reactive data/isLoading/isError |
1119
+ | `@useMutation<ApiInterface>(methodName)` | Field | Injects Mutation from ApiClient by method name |
965
1120
  | `@Form` | Class | Form context with `@field` fields |
966
1121
  | `@field(options)` | FormContext field | Creates FieldState for a form field (value, isTouched, error, isDirty) |
967
1122
  | `@useForm(FormContext)` | Field | Injects the form into a component with subscription to field changes |
package/babel-preset.js CHANGED
@@ -15,7 +15,8 @@ module.exports = (api, opts = {}) => ({
15
15
  "@babel/preset-typescript",
16
16
  ],
17
17
  plugins: [
18
- [diPlugin, opts.di || {}],
18
+ // Plugins run before Presets — di-plugin must run before TS strips constructor param types
19
+ [diPlugin, opts.di || opts || {}],
19
20
  "babel-plugin-transform-typescript-metadata",
20
21
  ["@babel/plugin-proposal-decorators", { legacy: true }],
21
22
  ["@babel/plugin-proposal-class-properties", { loose: true }],
@@ -41,6 +41,8 @@ export default class DOM {
41
41
  /**
42
42
  * Mount a reactive @if / @ifelse. Uses createEffect to reactively
43
43
  * evaluate the condition and mount/unmount the body.
44
+ * Only destroys/remounts when the condition actually changes (prevents
45
+ * form state loss on blur/input when effect re-runs spuriously).
44
46
  */
45
47
  private static mountReactiveIf;
46
48
  /**
@@ -1,5 +1,5 @@
1
1
  import { Container } from "../di/Container";
2
- import type { RouterOptions } from "./types";
2
+ import type { HttpConfig, RouterOptions } from "./types";
3
3
  import type { ViewInstance } from "../decorators/View.decorator";
4
4
  export interface CreateAppOptions {
5
5
  root: Element;
@@ -14,6 +14,8 @@ export interface AppBuilder {
14
14
  configureContainer(fn: (container: Container) => void): AppBuilder;
15
15
  /** Register services. Optional — plugin injects .useDI(registerAllServices) on createApp chains. */
16
16
  useDI(registerAllServices?: RegisterServicesFn): AppBuilder;
17
+ /** Configure HTTP client (baseUrl, headers, timeout, etc.) and register HttpClient in DI. */
18
+ http(config: HttpConfig): AppBuilder;
17
19
  router(options: RouterOptions): AppBuilder;
18
20
  mount(app: new (props?: any) => any): ViewInstance;
19
21
  }
@@ -1,4 +1,4 @@
1
1
  export { createApp } from "./createApp";
2
2
  export { HelfyApp } from "./HelfyApp";
3
3
  export type { AppBuilder, CreateAppOptions, RegisterServicesFn } from "./createApp";
4
- export type { AppConfig, RouterOptions } from "./types";
4
+ export type { AppConfig, HttpConfig, RouterOptions } from "./types";
@@ -1,4 +1,5 @@
1
1
  import type { RouteConfig } from "../router/types";
2
+ import type { HttpClient } from "../http/HttpClient.types";
2
3
  export interface AppConfig {
3
4
  root: Element;
4
5
  routes: RouteConfig[];
@@ -7,3 +8,12 @@ export interface AppConfig {
7
8
  export interface RouterOptions {
8
9
  routes: RouteConfig[];
9
10
  }
11
+ export interface HttpConfig {
12
+ baseUrl?: string;
13
+ headers?: Record<string, string>;
14
+ timeout?: number;
15
+ /** Custom HttpClient implementation (otherwise FetchHttpClient) */
16
+ client?: new (config: HttpConfig) => HttpClient;
17
+ /** QueryCache max entries for LRU eviction. Default 100 */
18
+ queryCacheMaxSize?: number;
19
+ }