rask-ui 0.8.0 → 0.9.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/README.md CHANGED
@@ -684,12 +684,12 @@ function Child() {
684
684
 
685
685
  #### `createTask<T, P>(task)`
686
686
 
687
- Creates a low-level reactive primitive for managing async operations with loading, error, and result states. This is a generic primitive that gives you full control over async state management without prescribing patterns.
687
+ A low-level reactive primitive for managing any async operation. `createTask` provides the foundation for building data fetching, mutations, polling, debouncing, and any other async pattern you need. It gives you full control without prescribing specific patterns.
688
688
 
689
689
  ```tsx
690
- import { createTask } from "rask-ui";
690
+ import { createTask, createState } from "rask-ui";
691
691
 
692
- // Simple task without parameters - auto-runs on creation
692
+ // Fetching data - auto-runs on creation
693
693
  function UserProfile() {
694
694
  const user = createTask(() => fetch("/api/user").then((r) => r.json()));
695
695
 
@@ -706,71 +706,116 @@ function UserProfile() {
706
706
  };
707
707
  }
708
708
 
709
- // Task with parameters - control when to run
709
+ // Fetching with parameters
710
710
  function Posts() {
711
+ const state = createState({ page: 1 });
712
+
711
713
  const posts = createTask((page: number) =>
712
- fetch(`/api/posts?page=${page}`).then((r) => r.json())
714
+ fetch(`/api/posts?page=${page}&limit=10`).then((r) => r.json())
713
715
  );
714
716
 
715
- const renderPosts = () => {
716
- if (posts.isRunning) {
717
- return <p>Loading...</p>;
718
- }
719
-
720
- if (posts.error) {
721
- return <p>Error: {posts.error}</p>;
722
- }
723
-
724
- if (!posts.result) {
725
- return <p>No posts loaded</p>;
726
- }
727
-
728
- return posts.result.map((post) => (
729
- <article key={post.id}>{post.title}</article>
730
- ));
731
- };
717
+ // Fetch when page changes
718
+ createEffect(() => {
719
+ posts.run(state.page);
720
+ });
732
721
 
733
722
  return () => (
734
723
  <div>
735
- <button onClick={() => posts.run(1)}>Load Page 1</button>
736
- <button onClick={() => posts.rerun(1)}>Reload Page 1</button>
737
- {renderPosts()}
724
+ <h1>Posts - Page {state.page}</h1>
725
+ {posts.isRunning && <p>Loading...</p>}
726
+ {posts.result?.map((post) => (
727
+ <article key={post.id}>{post.title}</article>
728
+ ))}
729
+ <button onClick={() => state.page--}>Previous</button>
730
+ <button onClick={() => state.page++}>Next</button>
738
731
  </div>
739
732
  );
740
733
  }
741
734
 
742
- // Mutation-style usage
735
+ // Mutation - creating data on server
743
736
  function CreatePost() {
744
737
  const state = createState({ title: "", body: "" });
745
738
 
746
- const create = createTask((data: { title: string; body: string }) =>
739
+ const createPost = createTask((data: { title: string; body: string }) =>
747
740
  fetch("/api/posts", {
748
741
  method: "POST",
742
+ headers: { "Content-Type": "application/json" },
749
743
  body: JSON.stringify(data),
750
744
  }).then((r) => r.json())
751
745
  );
752
746
 
753
- const handleSubmit = () => {
754
- create.run({ title: state.title, body: state.body });
747
+ const handleSubmit = async () => {
748
+ await createPost.run({ title: state.title, body: state.body });
749
+ // Clear form on success
750
+ state.title = "";
751
+ state.body = "";
755
752
  };
756
753
 
757
754
  return () => (
758
755
  <form onSubmit={handleSubmit}>
759
756
  <input
757
+ placeholder="Title"
760
758
  value={state.title}
761
759
  onInput={(e) => (state.title = e.target.value)}
762
760
  />
763
761
  <textarea
762
+ placeholder="Body"
764
763
  value={state.body}
765
764
  onInput={(e) => (state.body = e.target.value)}
766
765
  />
767
- <button disabled={create.isRunning}>
768
- {create.isRunning ? "Creating..." : "Create"}
766
+ <button disabled={createPost.isRunning}>
767
+ {createPost.isRunning ? "Creating..." : "Create Post"}
769
768
  </button>
770
- {create.error && <p>Error: {create.error}</p>}
769
+ {createPost.error && <p>Error: {createPost.error}</p>}
770
+ {createPost.result && <p>Post created! ID: {createPost.result.id}</p>}
771
771
  </form>
772
772
  );
773
773
  }
774
+
775
+ // Optimistic updates - instant UI updates with rollback on error
776
+ function TodoList() {
777
+ const state = createState({
778
+ todos: [],
779
+ optimisticTodo: null,
780
+ });
781
+
782
+ const createTodo = createTask((text: string) =>
783
+ fetch("/api/todos", {
784
+ method: "POST",
785
+ headers: { "Content-Type": "application/json" },
786
+ body: JSON.stringify({ text, done: false }),
787
+ }).then((r) => r.json())
788
+ );
789
+
790
+ const addTodo = async (text: string) => {
791
+ // Show optimistically
792
+ state.optimisticTodo = { id: Date.now(), text, done: false };
793
+
794
+ try {
795
+ const savedTodo = await createTodo.run(text);
796
+ state.todos.push(savedTodo);
797
+ state.optimisticTodo = null;
798
+ } catch {
799
+ // Rollback on error
800
+ state.optimisticTodo = null;
801
+ }
802
+ };
803
+
804
+ return () => (
805
+ <div>
806
+ <ul>
807
+ {[...state.todos, state.optimisticTodo]
808
+ .filter(Boolean)
809
+ .map((todo) => (
810
+ <li key={todo.id} style={{ opacity: todo === state.optimisticTodo ? 0.5 : 1 }}>
811
+ {todo.text}
812
+ </li>
813
+ ))}
814
+ </ul>
815
+ <button onClick={() => addTodo("New todo")}>Add Todo</button>
816
+ </div>
817
+ );
818
+ }
774
819
  ```
775
820
 
776
821
  **Type Signatures:**
@@ -833,12 +878,208 @@ Error:
833
878
 
834
879
  **Usage Patterns:**
835
880
 
836
- Use `createTask` as a building block for various async patterns:
837
- - **Queries**: Tasks that fetch data and can be refetched
838
- - **Mutations**: Tasks that modify server state
839
- - **Polling**: Tasks that run periodically
840
- - **Debounced searches**: Tasks that run based on user input
841
- - **File uploads**: Tasks that track upload progress
881
+ `createTask` is a low-level primitive. Use it as a building block for any async pattern:
882
+ - **Data fetching**: Fetch data on mount or based on dependencies
883
+ - **Mutations**: Create, update, or delete data on the server
884
+ - **Optimistic updates**: Update UI instantly with rollback on error
885
+ - **Polling**: Periodically refetch data
886
+ - **Debounced searches**: Wait for user input to settle
887
+ - **Dependent queries**: Chain requests that depend on each other
888
+ - **Parallel requests**: Run multiple requests simultaneously
889
+ - **Custom patterns**: Build your own abstractions (queries, mutations, etc.)
890
+
891
+ ---
892
+
893
+ ### Developer Tools
894
+
895
+ #### `inspect(root, callback)`
896
+
897
+ Enables inspection of reactive state, computed values, and actions for building devtools. This API is **development-only** and has zero overhead in production builds.
898
+
899
+ ```tsx
900
+ import { inspect } from "rask-ui";
901
+
902
+ function DevToolsIntegration() {
903
+ const state = createState({ count: 0, name: "Example" });
904
+ const computed = createComputed({
905
+ double: () => state.count * 2,
906
+ });
907
+
908
+ const actions = {
909
+ increment: () => state.count++,
910
+ reset: () => state.count = 0,
911
+ };
912
+
913
+ const view = createView(state, computed, actions);
914
+
915
+ // Inspect all reactive events
916
+ inspect(view, (event) => {
917
+ console.log(event);
918
+ // Send to devtools panel, logging service, etc.
919
+ });
920
+
921
+ return () => (
922
+ <div>
923
+ <h1>{view.name}: {view.count}</h1>
924
+ <p>Double: {view.double}</p>
925
+ <button onClick={view.increment}>+</button>
926
+ <button onClick={view.reset}>Reset</button>
927
+ </div>
928
+ );
929
+ }
930
+ ```
931
+
932
+ **Parameters:**
933
+
934
+ - `root: any` - The reactive object to inspect (state, view, computed, etc.)
935
+ - `callback: (event: InspectEvent) => void` - Callback receiving inspection events
936
+
937
+ **Event Types:**
938
+
939
+ ```tsx
940
+ type InspectEvent =
941
+ | {
942
+ type: "mutation";
943
+ path: string[]; // Property path, e.g., ["user", "name"]
944
+ value: any; // New value
945
+ }
946
+ | {
947
+ type: "action";
948
+ path: string[]; // Function name path, e.g., ["increment"]
949
+ params: any[]; // Function parameters
950
+ }
951
+ | {
952
+ type: "computed";
953
+ path: string[]; // Computed property path
954
+ isDirty: boolean; // true when invalidated, false when recomputed
955
+ value: any; // Current or recomputed value
956
+ };
957
+ ```
958
+
959
+ **Features:**
960
+
961
+ - **Zero production overhead** - Completely eliminated in production builds via tree-shaking
962
+ - **Deep tracking** - Tracks nested state mutations with full property paths
963
+ - **Action tracking** - Captures function calls with parameters
964
+ - **Computed lifecycle** - Observes when computed values become dirty and when they recompute
965
+ - **Nested view support** - Compose deeply nested state trees and track full paths
966
+ - **JSON serialization** - Use `JSON.stringify()` to extract state snapshots
967
+ - **Flexible integration** - Build custom devtools, time-travel debugging, or logging systems
968
+
969
+ **Use Cases:**
970
+
971
+ - Building browser devtools extensions
972
+ - Creating debugging panels for development
973
+ - Implementing time-travel debugging
974
+ - Logging state changes for debugging
975
+ - Integrating with external monitoring tools
976
+ - Building replay systems for bug reports
977
+
978
+ **Production Builds:**
979
+
980
+ The inspector is automatically stripped from production builds using Vite's `import.meta.env.DEV` constant. This means:
981
+
982
+ - No runtime checks in production code
983
+ - No function wrapping overhead
984
+ - No symbol property lookups
985
+ - Smaller bundle size
986
+ - Zero performance impact
987
+
988
+ ```tsx
989
+ // In development
990
+ inspect(view, console.log); // ✅ Works, logs all events
991
+
992
+ // In production build
993
+ inspect(view, console.log); // No-op, zero overhead, removed by tree-shaking
994
+ ```
995
+
996
+ **Nested State Trees:**
997
+
998
+ The inspector automatically tracks nested property paths. Compose deeply nested views to create organized state trees:
999
+
1000
+ ```tsx
1001
+ function App() {
1002
+ // Create nested state structure
1003
+ const userState = createState({
1004
+ profile: { name: "Alice", email: "alice@example.com" },
1005
+ preferences: { theme: "dark", notifications: true },
1006
+ });
1007
+
1008
+ const cartState = createState({
1009
+ items: [],
1010
+ total: 0,
1011
+ });
1012
+
1013
+ // Compose into a state tree
1014
+ const appState = createView({
1015
+ user: createView(userState),
1016
+ cart: createView(cartState),
1017
+ });
1018
+
1019
+ inspect(appState, (event) => {
1020
+ console.log(event);
1021
+ });
1022
+
1023
+ // When you do: appState.user.profile.name = "Bob"
1024
+ // You receive: { type: "mutation", path: ["user", "profile", "name"], value: "Bob" }
1025
+
1026
+ return () => (
1027
+ <div>
1028
+ <h1>Welcome, {appState.user.profile.name}!</h1>
1029
+ <p>Theme: {appState.user.preferences.theme}</p>
1030
+ <p>Cart items: {appState.cart.items.length}</p>
1031
+ </div>
1032
+ );
1033
+ }
1034
+ ```
1035
+
1036
+ **JSON Serialization:**
1037
+
1038
+ Use `JSON.stringify()` to extract state snapshots for devtools, persistence, or debugging:
1039
+
1040
+ ```tsx
1041
+ function App() {
1042
+ const state = createState({
1043
+ user: { id: 1, name: "Alice" },
1044
+ todos: [
1045
+ { id: 1, text: "Learn RASK", done: false },
1046
+ { id: 2, text: "Build app", done: true },
1047
+ ],
1048
+ });
1049
+
1050
+ const computed = createComputed({
1051
+ completedCount: () => state.todos.filter((t) => t.done).length,
1052
+ });
1053
+
1054
+ const view = createView(state, computed);
1055
+
1056
+ // Serialize to JSON - includes computed values
1057
+ const snapshot = JSON.stringify(view);
1058
+ // {
1059
+ // "user": { "id": 1, "name": "Alice" },
1060
+ // "todos": [...],
1061
+ // "completedCount": 1
1062
+ // }
1063
+
1064
+ // Send initial state and updates to devtools
1065
+ if (import.meta.env.DEV) {
1066
+ window.postMessage({
1067
+ type: "RASK_DEVTOOLS_INIT",
1068
+ initialState: JSON.parse(snapshot),
1069
+ }, "*");
1070
+
1071
+ inspect(view, (event) => {
1072
+ window.postMessage({
1073
+ type: "RASK_DEVTOOLS_EVENT",
1074
+ event,
1075
+ snapshot: JSON.parse(JSON.stringify(view)),
1076
+ }, "*");
1077
+ });
1078
+ }
1079
+
1080
+ return () => <div>{/* Your app */}</div>;
1081
+ }
1082
+ ```
842
1083
 
843
1084
  ---
844
1085
 
@@ -1,10 +1,18 @@
1
1
  import { VNode, Component, Props } from "inferno";
2
- export declare function getCurrentComponent(): RaskComponent<any>;
2
+ import { Observer } from "./observation";
3
+ export type RaskStatelessFunctionComponent<P extends Props<any>> = (() => VNode) | ((props: P) => VNode);
4
+ export declare class RaskStatelessComponent extends Component {
5
+ renderFn: RaskStatelessFunctionComponent<any>;
6
+ observer: Observer;
7
+ shouldComponentUpdate(nextProps: Props<any>): boolean;
8
+ render(): VNode;
9
+ }
10
+ export declare function getCurrentComponent(): RaskStatefulComponent<any>;
3
11
  export declare function createMountEffect(cb: () => void): void;
4
12
  export declare function createCleanup(cb: () => void): void;
5
- export type RaskFunctionComponent<P extends Props<any>> = (() => () => VNode) | ((props: P) => () => VNode);
6
- export declare class RaskComponent<P extends Props<any>> extends Component<P> {
7
- setup: RaskFunctionComponent<P>;
13
+ export type RaskStatefulFunctionComponent<P extends Props<any>> = (() => () => VNode) | ((props: P) => () => VNode);
14
+ export declare class RaskStatefulComponent<P extends Props<any>> extends Component<P> {
15
+ setup: RaskStatefulFunctionComponent<P>;
8
16
  private renderFn?;
9
17
  private reactiveProps?;
10
18
  private observer;
@@ -1 +1 @@
1
- {"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,EACL,SAAS,EACT,KAAK,EAEN,MAAM,SAAS,CAAC;AAQjB,wBAAgB,mBAAmB,uBAMlC;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,IAAI,QAM/C;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,IAAI,QAM3C;AAED,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,IAClD,CAAC,MAAM,MAAM,KAAK,CAAC,GACnB,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,KAAK,CAAC,CAAC;AAEhC,qBAAa,aAAa,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,CAAE,SAAQ,SAAS,CAAC,CAAC,CAAC;IAC3D,KAAK,EAAE,qBAAqB,CAAC,CAAC,CAAC,CAAC;IACxC,OAAO,CAAC,QAAQ,CAAC,CAAc;IAC/B,OAAO,CAAC,aAAa,CAAC,CAAa;IACnC,OAAO,CAAC,QAAQ,CAEb;IACH,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC,CAAM;IAC3D,QAAQ,gBAAa;IACrB,eAAe;IAUf,QAAQ,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAM;IACjC,UAAU,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAM;IACnC,OAAO,CAAC,mBAAmB;IA2D3B,iBAAiB,IAAI,IAAI;IAGzB,oBAAoB,IAAI,IAAI;IAG5B;;OAEG;IACH,mBAAmB,CAAC,SAAS,EAAE,GAAG;IAYlC,yBAAyB,IAAI,IAAI;IACjC,qBAAqB,CAAC,SAAS,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO;IAerD,MAAM;CAyCP;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,SAO9D"}
1
+ {"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,EACL,SAAS,EACT,KAAK,EAEN,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAsB,QAAQ,EAAU,MAAM,eAAe,CAAC;AAGrE,MAAM,MAAM,8BAA8B,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,IAC3D,CAAC,MAAM,KAAK,CAAC,GACb,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,KAAK,CAAC,CAAC;AAE1B,qBAAa,sBAAuB,SAAQ,SAAS;IAC3C,QAAQ,EAAE,8BAA8B,CAAC,GAAG,CAAC,CAAC;IACtD,QAAQ,WAEL;IACH,qBAAqB,CAAC,SAAS,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO;IAUrD,MAAM;CAMP;AAID,wBAAgB,mBAAmB,+BAMlC;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,IAAI,QAM/C;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,IAAI,QAM3C;AAED,MAAM,MAAM,6BAA6B,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,IAC1D,CAAC,MAAM,MAAM,KAAK,CAAC,GACnB,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,KAAK,CAAC,CAAC;AAEhC,qBAAa,qBAAqB,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,CAAE,SAAQ,SAAS,CAAC,CAAC,CAAC;IACnE,KAAK,EAAE,6BAA6B,CAAC,CAAC,CAAC,CAAC;IAChD,OAAO,CAAC,QAAQ,CAAC,CAAc;IAC/B,OAAO,CAAC,aAAa,CAAC,CAAa;IACnC,OAAO,CAAC,QAAQ,CAEb;IACH,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC,CAAM;IAC3D,QAAQ,gBAAa;IACrB,eAAe;IAUf,QAAQ,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAM;IACjC,UAAU,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAM;IACnC,OAAO,CAAC,mBAAmB;IA2D3B,iBAAiB,IAAI,IAAI;IAGzB,oBAAoB,IAAI,IAAI;IAG5B;;OAEG;IACH,mBAAmB,CAAC,SAAS,EAAE,GAAG;IAYlC,yBAAyB,IAAI,IAAI;IACjC,qBAAqB,CAAC,SAAS,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO;IAWrD,MAAM;CAyCP;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,SAO9D"}
package/dist/component.js CHANGED
@@ -1,6 +1,26 @@
1
1
  import { createComponentVNode, Component, } from "inferno";
2
2
  import { getCurrentObserver, Observer, Signal } from "./observation";
3
3
  import { syncBatch } from "./batch";
4
+ export class RaskStatelessComponent extends Component {
5
+ observer = new Observer(() => {
6
+ this.forceUpdate();
7
+ });
8
+ shouldComponentUpdate(nextProps) {
9
+ for (const prop in nextProps) {
10
+ // @ts-ignore
11
+ if (this.props[prop] !== nextProps[prop]) {
12
+ return true;
13
+ }
14
+ }
15
+ return false;
16
+ }
17
+ render() {
18
+ const stopObserving = this.observer.observe();
19
+ const result = this.renderFn(this.props);
20
+ stopObserving();
21
+ return result;
22
+ }
23
+ }
4
24
  let currentComponent;
5
25
  export function getCurrentComponent() {
6
26
  if (!currentComponent) {
@@ -20,7 +40,7 @@ export function createCleanup(cb) {
20
40
  }
21
41
  currentComponent.onCleanups.push(cb);
22
42
  }
23
- export class RaskComponent extends Component {
43
+ export class RaskStatefulComponent extends Component {
24
44
  renderFn;
25
45
  reactiveProps;
26
46
  observer = new Observer(() => {
@@ -113,9 +133,6 @@ export class RaskComponent extends Component {
113
133
  shouldComponentUpdate(nextProps) {
114
134
  // Shallow comparison of props, excluding internal props
115
135
  for (const prop in nextProps) {
116
- if (prop === "__component") {
117
- continue;
118
- }
119
136
  // @ts-ignore
120
137
  if (this.props[prop] !== nextProps[prop]) {
121
138
  return true;
@@ -162,5 +179,5 @@ export class RaskComponent extends Component {
162
179
  }
163
180
  }
164
181
  export function createComponent(props, key) {
165
- return createComponentVNode(4 /* VNodeFlags.ComponentClass */, RaskComponent, props, key);
182
+ return createComponentVNode(4 /* VNodeFlags.ComponentClass */, RaskStatefulComponent, props, key);
166
183
  }
@@ -1 +1 @@
1
- {"version":3,"file":"createComputed.d.ts","sourceRoot":"","sources":["../src/createComputed.ts"],"names":[],"mappings":"AAGA,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,EAChE,QAAQ,EAAE,CAAC,GACV;KACA,CAAC,IAAI,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACjC,CA4CA"}
1
+ {"version":3,"file":"createComputed.d.ts","sourceRoot":"","sources":["../src/createComputed.ts"],"names":[],"mappings":"AAIA,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,EAChE,QAAQ,EAAE,CAAC,GACV;KACA,CAAC,IAAI,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACjC,CAiFA"}
@@ -1,4 +1,5 @@
1
1
  import { getCurrentComponent, createCleanup } from "./component";
2
+ import { INSPECT_MARKER, INSPECTOR_ENABLED } from "./inspect";
2
3
  import { getCurrentObserver, Observer, Signal } from "./observation";
3
4
  export function createComputed(computed) {
4
5
  let currentComponent;
@@ -9,6 +10,7 @@ export function createComputed(computed) {
9
10
  currentComponent = undefined;
10
11
  }
11
12
  const proxy = {};
13
+ let notifyInspectorRef = {};
12
14
  for (const prop in computed) {
13
15
  let isDirty = true;
14
16
  let value;
@@ -16,11 +18,21 @@ export function createComputed(computed) {
16
18
  const computedObserver = new Observer(() => {
17
19
  isDirty = true;
18
20
  signal.notify();
21
+ if (INSPECTOR_ENABLED) {
22
+ notifyInspectorRef.current?.notify({
23
+ type: "computed",
24
+ path: notifyInspectorRef.current.path.concat(prop),
25
+ isDirty: true,
26
+ value,
27
+ });
28
+ }
19
29
  });
20
30
  if (currentComponent) {
21
31
  createCleanup(() => computedObserver.dispose());
22
32
  }
23
33
  Object.defineProperty(proxy, prop, {
34
+ enumerable: true,
35
+ configurable: true,
24
36
  get() {
25
37
  const currentObserver = getCurrentObserver();
26
38
  if (currentObserver) {
@@ -31,11 +43,35 @@ export function createComputed(computed) {
31
43
  value = computed[prop]();
32
44
  stopObserving();
33
45
  isDirty = false;
46
+ if (INSPECTOR_ENABLED) {
47
+ notifyInspectorRef.current?.notify({
48
+ type: "computed",
49
+ path: notifyInspectorRef.current.path.concat(prop),
50
+ isDirty: false,
51
+ value,
52
+ });
53
+ }
34
54
  return value;
35
55
  }
36
56
  return value;
37
57
  },
38
58
  });
39
59
  }
60
+ if (INSPECTOR_ENABLED) {
61
+ Object.defineProperty(proxy, INSPECT_MARKER, {
62
+ enumerable: false,
63
+ configurable: false,
64
+ get() {
65
+ return !notifyInspectorRef.current;
66
+ },
67
+ set: (value) => {
68
+ Object.defineProperty(notifyInspectorRef, "current", {
69
+ get() {
70
+ return value.current;
71
+ },
72
+ });
73
+ },
74
+ });
75
+ }
40
76
  return proxy;
41
77
  }
@@ -1,4 +1,4 @@
1
- import { INSPECT_MARKER } from "./inspect";
1
+ import { INSPECT_MARKER, INSPECTOR_ENABLED } from "./inspect";
2
2
  import { getCurrentObserver, Signal } from "./observation";
3
3
  /**
4
4
  * Creates a reactive state object that tracks property access and notifies observers on changes.
@@ -25,11 +25,11 @@ import { getCurrentObserver, Signal } from "./observation";
25
25
  * @returns A reactive proxy of the state object
26
26
  */
27
27
  export function createState(state) {
28
- return getProxy(state);
28
+ return getProxy(state, {});
29
29
  }
30
30
  const proxyCache = new WeakMap();
31
31
  export const PROXY_MARKER = Symbol("isProxy");
32
- function getProxy(value, notifyInspector, path) {
32
+ function getProxy(value, notifyInspectorRef) {
33
33
  // Check if already a proxy to avoid double-wrapping
34
34
  if (PROXY_MARKER in value) {
35
35
  return value;
@@ -44,13 +44,19 @@ function getProxy(value, notifyInspector, path) {
44
44
  if (key === PROXY_MARKER) {
45
45
  return true;
46
46
  }
47
+ if (INSPECTOR_ENABLED && key === INSPECT_MARKER) {
48
+ return true;
49
+ }
47
50
  return Reflect.has(target, key);
48
51
  },
49
52
  get(target, key) {
50
53
  // Mark this as a proxy to prevent double-wrapping
51
- if (key === PROXY_MARKER || key === INSPECT_MARKER) {
54
+ if (key === PROXY_MARKER) {
52
55
  return true;
53
56
  }
57
+ if (INSPECTOR_ENABLED && key === INSPECT_MARKER) {
58
+ return !notifyInspectorRef.current;
59
+ }
54
60
  const value = Reflect.get(target, key);
55
61
  if (typeof key === "symbol" || typeof value === "function") {
56
62
  return value;
@@ -62,14 +68,24 @@ function getProxy(value, notifyInspector, path) {
62
68
  }
63
69
  if (Array.isArray(value) ||
64
70
  (typeof value === "object" && value !== null)) {
65
- return getProxy(value, notifyInspector, notifyInspector ? (path ? path.concat(key) : [key]) : undefined);
71
+ return getProxy(value, INSPECTOR_ENABLED && notifyInspectorRef.current
72
+ ? {
73
+ current: {
74
+ notify: notifyInspectorRef.current.notify,
75
+ path: notifyInspectorRef.current.path.concat(key),
76
+ },
77
+ }
78
+ : notifyInspectorRef);
66
79
  }
67
80
  return value;
68
81
  },
69
82
  set(target, key, newValue) {
70
- if (key === INSPECT_MARKER) {
71
- notifyInspector = newValue.fn;
72
- path = newValue.path;
83
+ if (INSPECTOR_ENABLED && key === INSPECT_MARKER) {
84
+ Object.defineProperty(notifyInspectorRef, "current", {
85
+ get() {
86
+ return newValue.current;
87
+ },
88
+ });
73
89
  return Reflect.set(target, key, newValue);
74
90
  }
75
91
  if (typeof key === "symbol") {
@@ -82,11 +98,13 @@ function getProxy(value, notifyInspector, path) {
82
98
  const signal = signals[key];
83
99
  signal?.notify();
84
100
  }
85
- notifyInspector?.({
86
- type: "mutation",
87
- path: path ? path.concat(key) : [key],
88
- value: newValue,
89
- });
101
+ if (INSPECTOR_ENABLED) {
102
+ notifyInspectorRef.current?.notify({
103
+ type: "mutation",
104
+ path: notifyInspectorRef.current.path,
105
+ value: newValue,
106
+ });
107
+ }
90
108
  return setResult;
91
109
  },
92
110
  deleteProperty(target, key) {
@@ -1 +1 @@
1
- {"version":3,"file":"createView.d.ts","sourceRoot":"","sources":["../src/createView.ts"],"names":[],"mappings":"AAGA,KAAK,QAAQ,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAAE,GAAG,EAAE,CAAC;AAEjD,KAAK,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,IAAI,QAAQ,CAC1D,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,CACrB,CAAC;AAEF,KAAK,SAAS,CAAC,CAAC,SAAS,SAAS,MAAM,EAAE,IAAI,CAAC,SAAS;IACtD,MAAM,CAAC,SAAS,MAAM;IACtB,GAAG,MAAM,CAAC,SAAS,MAAM,EAAE;CAC5B,GACG,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,GACzB,EAAE,CAAC;AAEP;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,SAAS,MAAM,EAAE,EACpD,GAAG,IAAI,EAAE,CAAC,GACT,SAAS,CAAC,CAAC,CAAC,CA4Dd"}
1
+ {"version":3,"file":"createView.d.ts","sourceRoot":"","sources":["../src/createView.ts"],"names":[],"mappings":"AAGA,KAAK,QAAQ,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAAE,GAAG,EAAE,CAAC;AAEjD,KAAK,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,IAAI,QAAQ,CAC1D,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,CACrB,CAAC;AAEF,KAAK,SAAS,CAAC,CAAC,SAAS,SAAS,MAAM,EAAE,IAAI,CAAC,SAAS;IACtD,MAAM,CAAC,SAAS,MAAM;IACtB,GAAG,MAAM,CAAC,SAAS,MAAM,EAAE;CAC5B,GACG,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,GACzB,EAAE,CAAC;AAEP;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,SAAS,MAAM,EAAE,EACpD,GAAG,IAAI,EAAE,CAAC,GACT,SAAS,CAAC,CAAC,CAAC,CAyEd"}
@@ -1,4 +1,4 @@
1
- import { INSPECT_MARKER } from "./inspect";
1
+ import { INSPECT_MARKER, INSPECTOR_ENABLED } from "./inspect";
2
2
  /**
3
3
  * Creates a view that merges multiple objects (reactive or not) into a single object while
4
4
  * maintaining reactivity through getters. Properties from later arguments override earlier ones.
@@ -45,9 +45,12 @@ import { INSPECT_MARKER } from "./inspect";
45
45
  export function createView(...args) {
46
46
  const result = {};
47
47
  const seen = new Set();
48
- let notifyInspector;
48
+ let notifyInspectorRef = {};
49
49
  for (let i = args.length - 1; i >= 0; i--) {
50
50
  const src = args[i];
51
+ if (INSPECTOR_ENABLED && src[INSPECT_MARKER]) {
52
+ src[INSPECT_MARKER] = notifyInspectorRef;
53
+ }
51
54
  // mimic Object.assign: only enumerable own property keys
52
55
  for (const key of Reflect.ownKeys(src)) {
53
56
  if (seen.has(key))
@@ -61,20 +64,22 @@ export function createView(...args) {
61
64
  configurable: true,
62
65
  get: () => {
63
66
  const value = src[key];
64
- if (!notifyInspector) {
67
+ if (!INSPECTOR_ENABLED || !notifyInspectorRef.current) {
65
68
  return value;
66
69
  }
67
70
  if (value?.[INSPECT_MARKER]) {
68
71
  value[INSPECT_MARKER] = {
69
- fn: notifyInspector.fn,
70
- path: notifyInspector.path.concat(key),
72
+ current: {
73
+ notify: notifyInspectorRef.current.notify,
74
+ path: notifyInspectorRef.current.path.concat(key),
75
+ },
71
76
  };
72
77
  }
73
78
  else if (typeof value === "function") {
74
79
  return (...params) => {
75
- notifyInspector.fn({
80
+ notifyInspectorRef.current.notify({
76
81
  type: "action",
77
- path: notifyInspector.path.concat(key),
82
+ path: notifyInspectorRef.current.path.concat(key),
78
83
  params,
79
84
  });
80
85
  return value(...params);
@@ -86,15 +91,21 @@ export function createView(...args) {
86
91
  seen.add(key);
87
92
  }
88
93
  }
89
- Object.defineProperty(result, INSPECT_MARKER, {
90
- enumerable: false,
91
- configurable: false,
92
- get() {
93
- return true;
94
- },
95
- set: (value) => {
96
- notifyInspector = value;
97
- },
98
- });
94
+ if (INSPECTOR_ENABLED) {
95
+ Object.defineProperty(result, INSPECT_MARKER, {
96
+ enumerable: false,
97
+ configurable: false,
98
+ get() {
99
+ return !notifyInspectorRef.current;
100
+ },
101
+ set: (value) => {
102
+ Object.defineProperty(notifyInspectorRef, "current", {
103
+ get() {
104
+ return value.current;
105
+ },
106
+ });
107
+ },
108
+ });
109
+ }
99
110
  return result;
100
111
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { render } from "./render";
2
- export { createCleanup, createMountEffect, RaskComponent } from "./component";
2
+ export { createCleanup, createMountEffect, RaskStatefulComponent, RaskStatelessComponent, } from "./component";
3
3
  export { createContext } from "./createContext";
4
4
  export { createState } from "./createState";
5
5
  export { createTask } from "./createTask";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,OAAO,EACL,WAAW,EACX,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,cAAc,EACd,SAAS,GACV,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,OAAO,EACL,WAAW,EACX,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,cAAc,EACd,SAAS,GACV,MAAM,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { render } from "./render";
2
- export { createCleanup, createMountEffect, RaskComponent } from "./component";
2
+ export { createCleanup, createMountEffect, RaskStatefulComponent, RaskStatelessComponent, } from "./component";
3
3
  export { createContext } from "./createContext";
4
4
  export { createState } from "./createState";
5
5
  export { createTask } from "./createTask";
package/dist/inspect.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export declare const INSPECT_MARKER: unique symbol;
2
+ export declare const INSPECTOR_ENABLED: boolean;
2
3
  export type InspectEvent = {
3
4
  type: "mutation";
4
5
  path: string[];
@@ -7,7 +8,18 @@ export type InspectEvent = {
7
8
  type: "action";
8
9
  path: string[];
9
10
  params: any[];
11
+ } | {
12
+ type: "computed";
13
+ path: string[];
14
+ isDirty: boolean;
15
+ value: any;
10
16
  };
11
17
  export type InspectorCallback = (event: InspectEvent) => void;
18
+ export type InspectorRef = {
19
+ current?: {
20
+ notify: InspectorCallback;
21
+ path: string[];
22
+ };
23
+ };
12
24
  export declare function inspect(root: any, cb: InspectorCallback): void;
13
25
  //# sourceMappingURL=inspect.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../src/inspect.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,eAAoB,CAAC;AAEhD,MAAM,MAAM,YAAY,GACpB;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,KAAK,EAAE,GAAG,CAAC;CACZ,GACD;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,EAAE,GAAG,EAAE,CAAC;CACf,CAAC;AAEN,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;AAE9D,wBAAgB,OAAO,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,iBAAiB,QAKvD"}
1
+ {"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../src/inspect.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,eAAoB,CAAC;AAGhD,eAAO,MAAM,iBAAiB,SAAsB,CAAC;AAErD,MAAM,MAAM,YAAY,GACpB;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,KAAK,EAAE,GAAG,CAAC;CACZ,GACD;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,EAAE,GAAG,EAAE,CAAC;CACf,GACD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,GAAG,CAAC;CACZ,CAAC;AAEN,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;AAE9D,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,CAAC,EAAE;QAAE,MAAM,EAAE,iBAAiB,CAAC;QAAC,IAAI,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;CACzD,CAAC;AAEF,wBAAgB,OAAO,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,iBAAiB,QAWvD"}
package/dist/inspect.js CHANGED
@@ -1,7 +1,14 @@
1
1
  export const INSPECT_MARKER = Symbol("INSPECT");
2
+ // Flag to check if inspector is enabled (only in development)
3
+ export const INSPECTOR_ENABLED = import.meta.env.DEV;
2
4
  export function inspect(root, cb) {
5
+ if (!INSPECTOR_ENABLED) {
6
+ return;
7
+ }
3
8
  root[INSPECT_MARKER] = {
4
- fn: cb,
5
- path: [],
9
+ current: {
10
+ notify: cb,
11
+ path: [],
12
+ },
6
13
  };
7
14
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rask-ui",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -15,14 +15,16 @@ pub struct Config {
15
15
 
16
16
  pub struct RaskComponentTransform {
17
17
  config: Config,
18
- import_rask_component: Option<Ident>,
18
+ import_rask_stateful_component: Option<Ident>,
19
+ import_rask_stateless_component: Option<Ident>,
19
20
  }
20
21
 
21
22
  impl RaskComponentTransform {
22
23
  fn new(config: Config) -> Self {
23
24
  RaskComponentTransform {
24
25
  config,
25
- import_rask_component: None,
26
+ import_rask_stateful_component: None,
27
+ import_rask_stateless_component: None,
26
28
  }
27
29
  }
28
30
 
@@ -100,7 +102,27 @@ impl RaskComponentTransform {
100
102
  }
101
103
  }
102
104
 
103
- /// Check if a function body returns an arrow function with VNode calls
105
+ /// Check if a function body directly returns VNode calls (stateless component)
106
+ fn is_stateless_component(&self, func: &Function) -> bool {
107
+ if let Some(body) = &func.body {
108
+ for stmt in &body.stmts {
109
+ if let Stmt::Return(ret_stmt) = stmt {
110
+ if let Some(ret_arg) = &ret_stmt.arg {
111
+ // Check if directly returning VNode (not arrow function)
112
+ if self.has_vnode_call(ret_arg) {
113
+ // Make sure it's NOT an arrow function
114
+ if !matches!(&**ret_arg, Expr::Arrow(_)) {
115
+ return true;
116
+ }
117
+ }
118
+ }
119
+ }
120
+ }
121
+ }
122
+ false
123
+ }
124
+
125
+ /// Check if a function body returns an arrow function with VNode calls (stateful component)
104
126
  fn is_rask_component(&self, func: &Function) -> bool {
105
127
  if let Some(body) = &func.body {
106
128
  for stmt in &body.stmts {
@@ -135,14 +157,14 @@ impl RaskComponentTransform {
135
157
  false
136
158
  }
137
159
 
138
- /// Transform a function declaration to a RaskComponent class
139
- fn transform_to_class(&mut self, name: Ident, func: Function) -> Decl {
140
- // Ensure we have the RaskComponent import
141
- if self.import_rask_component.is_none() {
142
- self.import_rask_component = Some(private_ident!("RaskComponent"));
160
+ /// Transform a function declaration to a RaskStatefulComponent class
161
+ fn transform_to_stateful_class(&mut self, name: Ident, func: Function) -> Decl {
162
+ // Ensure we have the RaskStatefulComponent import
163
+ if self.import_rask_stateful_component.is_none() {
164
+ self.import_rask_stateful_component = Some(private_ident!("RaskStatefulComponent"));
143
165
  }
144
166
 
145
- let super_class_ident = self.import_rask_component.as_ref().unwrap().clone();
167
+ let super_class_ident = self.import_rask_stateful_component.as_ref().unwrap().clone();
146
168
 
147
169
  // Create the class property: setup = function name() { ... }
148
170
  let setup_prop = ClassMember::ClassProp(ClassProp {
@@ -181,6 +203,52 @@ impl RaskComponentTransform {
181
203
  })
182
204
  }
183
205
 
206
+ /// Transform a function declaration to a RaskStatelessComponent class
207
+ fn transform_to_stateless_class(&mut self, name: Ident, func: Function) -> Decl {
208
+ // Ensure we have the RaskStatelessComponent import
209
+ if self.import_rask_stateless_component.is_none() {
210
+ self.import_rask_stateless_component = Some(private_ident!("RaskStatelessComponent"));
211
+ }
212
+
213
+ let super_class_ident = self.import_rask_stateless_component.as_ref().unwrap().clone();
214
+
215
+ // Create the class property: renderFn = function name() { ... }
216
+ let render_prop = ClassMember::ClassProp(ClassProp {
217
+ span: Default::default(),
218
+ key: PropName::Ident(quote_ident!("renderFn").into()),
219
+ value: Some(Box::new(Expr::Fn(FnExpr {
220
+ ident: Some(name.clone()),
221
+ function: Box::new(func),
222
+ }))),
223
+ type_ann: None,
224
+ is_static: false,
225
+ decorators: vec![],
226
+ accessibility: None,
227
+ is_abstract: false,
228
+ is_optional: false,
229
+ is_override: false,
230
+ readonly: false,
231
+ declare: false,
232
+ definite: false,
233
+ });
234
+
235
+ Decl::Class(ClassDecl {
236
+ ident: name,
237
+ declare: false,
238
+ class: Box::new(Class {
239
+ span: Default::default(),
240
+ ctxt: Default::default(),
241
+ decorators: vec![],
242
+ body: vec![render_prop],
243
+ super_class: Some(Box::new(Expr::Ident(super_class_ident))),
244
+ is_abstract: false,
245
+ type_params: None,
246
+ super_type_params: None,
247
+ implements: vec![],
248
+ }),
249
+ })
250
+ }
251
+
184
252
  /// Rewrite imports from "inferno" to the configured import source
185
253
  fn rewrite_inferno_imports(&mut self, module: &mut Module) {
186
254
  let import_source = self
@@ -204,12 +272,8 @@ impl RaskComponentTransform {
204
272
  }
205
273
  }
206
274
 
207
- /// Inject the RaskComponent import at the top of the module
275
+ /// Inject the RaskStatefulComponent and/or RaskStatelessComponent imports at the top of the module
208
276
  fn inject_runtime(&mut self, module: &mut Module) {
209
- if self.import_rask_component.is_none() {
210
- return;
211
- }
212
-
213
277
  let import_source = self
214
278
  .config
215
279
  .import_source
@@ -217,47 +281,93 @@ impl RaskComponentTransform {
217
281
  .map(|s| s.as_str())
218
282
  .unwrap_or("rask-ui");
219
283
 
220
- let rask_component_ident = self.import_rask_component.as_ref().unwrap();
284
+ let mut specifiers = vec![];
285
+
286
+ // Add RaskStatefulComponent if needed
287
+ if let Some(stateful_ident) = &self.import_rask_stateful_component {
288
+ // Check if import already exists
289
+ let mut exists = false;
290
+ for item in &module.body {
291
+ if let ModuleItem::ModuleDecl(ModuleDecl::Import(import)) = item {
292
+ if &*import.src.value == import_source {
293
+ for spec in &import.specifiers {
294
+ if let ImportSpecifier::Named(named) = spec {
295
+ if let Some(ModuleExportName::Ident(imported)) = &named.imported {
296
+ if &*imported.sym == "RaskStatefulComponent" {
297
+ exists = true;
298
+ break;
299
+ }
300
+ } else if &*named.local.sym == "RaskStatefulComponent" {
301
+ exists = true;
302
+ break;
303
+ }
304
+ }
305
+ }
306
+ }
307
+ }
308
+ }
221
309
 
222
- // Check if import already exists
223
- for item in &module.body {
224
- if let ModuleItem::ModuleDecl(ModuleDecl::Import(import)) = item {
225
- if &*import.src.value == import_source {
226
- for spec in &import.specifiers {
227
- if let ImportSpecifier::Named(named) = spec {
228
- if let Some(ModuleExportName::Ident(imported)) = &named.imported {
229
- if &*imported.sym == "RaskComponent" {
230
- return; // Import already exists
310
+ if !exists {
311
+ specifiers.push(ImportSpecifier::Named(ImportNamedSpecifier {
312
+ span: Default::default(),
313
+ local: stateful_ident.clone(),
314
+ imported: Some(ModuleExportName::Ident(quote_ident!("RaskStatefulComponent").into())),
315
+ is_type_only: false,
316
+ }));
317
+ }
318
+ }
319
+
320
+ // Add RaskStatelessComponent if needed
321
+ if let Some(stateless_ident) = &self.import_rask_stateless_component {
322
+ // Check if import already exists
323
+ let mut exists = false;
324
+ for item in &module.body {
325
+ if let ModuleItem::ModuleDecl(ModuleDecl::Import(import)) = item {
326
+ if &*import.src.value == import_source {
327
+ for spec in &import.specifiers {
328
+ if let ImportSpecifier::Named(named) = spec {
329
+ if let Some(ModuleExportName::Ident(imported)) = &named.imported {
330
+ if &*imported.sym == "RaskStatelessComponent" {
331
+ exists = true;
332
+ break;
333
+ }
334
+ } else if &*named.local.sym == "RaskStatelessComponent" {
335
+ exists = true;
336
+ break;
231
337
  }
232
- } else if &*named.local.sym == "RaskComponent" {
233
- return; // Import already exists
234
338
  }
235
339
  }
236
340
  }
237
341
  }
238
342
  }
343
+
344
+ if !exists {
345
+ specifiers.push(ImportSpecifier::Named(ImportNamedSpecifier {
346
+ span: Default::default(),
347
+ local: stateless_ident.clone(),
348
+ imported: Some(ModuleExportName::Ident(quote_ident!("RaskStatelessComponent").into())),
349
+ is_type_only: false,
350
+ }));
351
+ }
239
352
  }
240
353
 
241
- // Create new import
242
- let import = ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
243
- span: Default::default(),
244
- specifiers: vec![ImportSpecifier::Named(ImportNamedSpecifier {
245
- span: Default::default(),
246
- local: rask_component_ident.clone(),
247
- imported: Some(ModuleExportName::Ident(quote_ident!("RaskComponent").into())),
248
- is_type_only: false,
249
- })],
250
- src: Box::new(Str {
354
+ // Only create import if we have specifiers to add
355
+ if !specifiers.is_empty() {
356
+ let import = ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
251
357
  span: Default::default(),
252
- value: Wtf8Atom::from(import_source),
253
- raw: None,
254
- }),
255
- type_only: false,
256
- with: None,
257
- phase: Default::default(),
258
- }));
259
-
260
- module.body.insert(0, import);
358
+ specifiers,
359
+ src: Box::new(Str {
360
+ span: Default::default(),
361
+ value: Wtf8Atom::from(import_source),
362
+ raw: None,
363
+ }),
364
+ type_only: false,
365
+ with: None,
366
+ phase: Default::default(),
367
+ }));
368
+
369
+ module.body.insert(0, import);
370
+ }
261
371
  }
262
372
  }
263
373
 
@@ -278,11 +388,19 @@ impl VisitMut for RaskComponentTransform {
278
388
  fn visit_mut_module_item(&mut self, item: &mut ModuleItem) {
279
389
  match item {
280
390
  ModuleItem::Stmt(Stmt::Decl(Decl::Fn(fn_decl))) => {
281
- // Check if this function should be transformed
391
+ // Check for stateful component first (returns arrow function)
282
392
  if self.is_rask_component(&fn_decl.function) {
283
393
  let name = fn_decl.ident.clone();
284
394
  let func = (*fn_decl.function).clone();
285
- let class_decl = self.transform_to_class(name, func);
395
+ let class_decl = self.transform_to_stateful_class(name, func);
396
+ *item = ModuleItem::Stmt(Stmt::Decl(class_decl));
397
+ return;
398
+ }
399
+ // Then check for stateless component (directly returns VNode)
400
+ else if self.is_stateless_component(&fn_decl.function) {
401
+ let name = fn_decl.ident.clone();
402
+ let func = (*fn_decl.function).clone();
403
+ let class_decl = self.transform_to_stateless_class(name, func);
286
404
  *item = ModuleItem::Stmt(Stmt::Decl(class_decl));
287
405
  return;
288
406
  }
@@ -295,16 +413,27 @@ impl VisitMut for RaskComponentTransform {
295
413
  // but transform the pattern internally if needed
296
414
  // For now, we'll skip transforming default exports
297
415
  // as they need special handling
416
+ } else if self.is_stateless_component(&fn_expr.function) {
417
+ // Same for stateless default exports
298
418
  }
299
419
  }
300
420
  }
301
421
  ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => {
302
422
  // Handle: export function MyComponent() { return () => <div /> }
303
423
  if let Decl::Fn(fn_decl) = &mut export.decl {
424
+ // Check for stateful component first
304
425
  if self.is_rask_component(&fn_decl.function) {
305
426
  let name = fn_decl.ident.clone();
306
427
  let func = (*fn_decl.function).clone();
307
- let class_decl = self.transform_to_class(name, func);
428
+ let class_decl = self.transform_to_stateful_class(name, func);
429
+ export.decl = class_decl;
430
+ return;
431
+ }
432
+ // Then check for stateless component
433
+ else if self.is_stateless_component(&fn_decl.function) {
434
+ let name = fn_decl.ident.clone();
435
+ let func = (*fn_decl.function).clone();
436
+ let class_decl = self.transform_to_stateless_class(name, func);
308
437
  export.decl = class_decl;
309
438
  return;
310
439
  }