rask-ui 0.11.0 → 0.12.1

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 (51) hide show
  1. package/README.md +220 -3
  2. package/dist/batch.d.ts.map +1 -1
  3. package/dist/batch.js +36 -55
  4. package/dist/component.d.ts +2 -1
  5. package/dist/component.d.ts.map +1 -1
  6. package/dist/component.js +33 -22
  7. package/dist/createContext.d.ts +1 -0
  8. package/dist/createContext.d.ts.map +1 -1
  9. package/dist/createContext.js +11 -0
  10. package/dist/createEffect.d.ts.map +1 -1
  11. package/dist/createEffect.js +6 -4
  12. package/dist/createRouter.d.ts +8 -0
  13. package/dist/createRouter.d.ts.map +1 -0
  14. package/dist/createRouter.js +24 -0
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -0
  18. package/dist/jsx-runtime.d.ts +2 -2
  19. package/dist/jsx-runtime.js +2 -2
  20. package/dist/plugin.d.ts +9 -1
  21. package/dist/plugin.d.ts.map +1 -1
  22. package/dist/plugin.js +4 -3
  23. package/dist/tests/batch.test.d.ts +2 -0
  24. package/dist/tests/batch.test.d.ts.map +1 -0
  25. package/dist/tests/batch.test.js +244 -0
  26. package/dist/tests/createComputed.test.d.ts +2 -0
  27. package/dist/tests/createComputed.test.d.ts.map +1 -0
  28. package/dist/tests/createComputed.test.js +257 -0
  29. package/dist/tests/createContext.test.d.ts +2 -0
  30. package/dist/tests/createContext.test.d.ts.map +1 -0
  31. package/dist/tests/createContext.test.js +136 -0
  32. package/dist/tests/createEffect.test.d.ts +2 -0
  33. package/dist/tests/createEffect.test.d.ts.map +1 -0
  34. package/dist/tests/createEffect.test.js +454 -0
  35. package/dist/tests/createState.test.d.ts +2 -0
  36. package/dist/tests/createState.test.d.ts.map +1 -0
  37. package/dist/tests/createState.test.js +144 -0
  38. package/dist/tests/createTask.test.d.ts +2 -0
  39. package/dist/tests/createTask.test.d.ts.map +1 -0
  40. package/dist/tests/createTask.test.js +322 -0
  41. package/dist/tests/createView.test.d.ts +2 -0
  42. package/dist/tests/createView.test.d.ts.map +1 -0
  43. package/dist/tests/createView.test.js +203 -0
  44. package/dist/tests/error.test.d.ts +2 -0
  45. package/dist/tests/error.test.d.ts.map +1 -0
  46. package/dist/tests/error.test.js +168 -0
  47. package/dist/tests/observation.test.d.ts +2 -0
  48. package/dist/tests/observation.test.d.ts.map +1 -0
  49. package/dist/tests/observation.test.js +341 -0
  50. package/package.json +4 -3
  51. package/swc-plugin/target/wasm32-wasip1/release/swc_plugin_rask_component.wasm +0 -0
package/README.md CHANGED
@@ -522,9 +522,9 @@ function LiveData() {
522
522
 
523
523
  **Features:**
524
524
 
525
- - Runs immediately on creation
525
+ - Runs immediately and synchronously on creation during setup
526
526
  - Automatically tracks reactive dependencies accessed during execution
527
- - Re-runs on microtask when dependencies change (prevents synchronous cascades)
527
+ - Re-runs when dependencies change
528
528
  - Automatically cleaned up when component unmounts
529
529
  - Optional dispose function for cleaning up resources before re-execution
530
530
  - Can be used for side effects like logging, syncing to localStorage, managing subscriptions, or updating derived state
@@ -532,7 +532,7 @@ function LiveData() {
532
532
  **Notes:**
533
533
 
534
534
  - Only call during component setup phase (not in render function)
535
- - Effects are queued on microtask to avoid synchronous execution from prop changes
535
+ - Effect runs synchronously during setup, making it predictable and easier to reason about
536
536
  - Be careful with effects that modify state - can cause infinite loops if not careful
537
537
  - Dispose functions run before the effect re-executes, not when the component unmounts
538
538
  - For component unmount cleanup, use `createCleanup()` instead
@@ -988,6 +988,223 @@ Error:
988
988
 
989
989
  ---
990
990
 
991
+ ### Routing
992
+
993
+ #### `createRouter<T>(config, options?)`
994
+
995
+ Creates a reactive router for client-side navigation. Built on [typed-client-router](https://github.com/christianalfoni/typed-client-router), it integrates seamlessly with RASK's reactive system for fully type-safe routing.
996
+
997
+ ```tsx
998
+ import { createRouter } from "rask-ui";
999
+
1000
+ const routes = {
1001
+ home: "/",
1002
+ about: "/about",
1003
+ user: "/users/:id",
1004
+ post: "/posts/:id",
1005
+ } as const;
1006
+
1007
+ function App() {
1008
+ const router = createRouter(routes);
1009
+
1010
+ return () => {
1011
+ // Match current route
1012
+ if (router.route?.name === "home") {
1013
+ return <Home />;
1014
+ }
1015
+
1016
+ if (router.route?.name === "user") {
1017
+ return <User id={router.route.params.id} />;
1018
+ }
1019
+
1020
+ if (router.route?.name === "post") {
1021
+ return <Post id={router.route.params.id} />;
1022
+ }
1023
+
1024
+ return <NotFound />;
1025
+ };
1026
+ }
1027
+ ```
1028
+
1029
+ **Parameters:**
1030
+
1031
+ - `config: T` - Route configuration object mapping route names to path patterns
1032
+ - `options?: { base?: string }` - Optional base path for all routes
1033
+
1034
+ **Returns:** Router object with reactive state and navigation methods:
1035
+
1036
+ **Properties:**
1037
+ - `route?: Route` - Current active route with `name` and `params` properties (reactive)
1038
+ - `queries: Record<string, string>` - Current URL query parameters (reactive)
1039
+
1040
+ **Methods:**
1041
+ - `push(name, params?, query?)` - Navigate to a route by name
1042
+ - `replace(name, params?, query?)` - Replace current route (no history entry)
1043
+ - `setQuery(query)` - Update query parameters
1044
+ - `url(name, params?, query?)` - Generate URL for a route
1045
+
1046
+ **Route Configuration:**
1047
+
1048
+ Define routes with path patterns. Use `:param` for dynamic segments:
1049
+
1050
+ ```tsx
1051
+ const routes = {
1052
+ home: "/",
1053
+ users: "/users",
1054
+ user: "/users/:id",
1055
+ userPosts: "/users/:userId/posts/:postId",
1056
+ } as const;
1057
+ ```
1058
+
1059
+ **Navigation:**
1060
+
1061
+ Navigate programmatically using route names:
1062
+
1063
+ ```tsx
1064
+ function Navigation() {
1065
+ const router = createRouter(routes);
1066
+
1067
+ return () => (
1068
+ <nav>
1069
+ <button onClick={() => router.push("home")}>Home</button>
1070
+ <button onClick={() => router.push("user", { id: "123" })}>
1071
+ User 123
1072
+ </button>
1073
+ <button onClick={() => router.push("home", {}, { tab: "recent" })}>
1074
+ Home (Recent)
1075
+ </button>
1076
+ </nav>
1077
+ );
1078
+ }
1079
+ ```
1080
+
1081
+ **Query Parameters:**
1082
+
1083
+ Access and modify query parameters:
1084
+
1085
+ ```tsx
1086
+ function SearchPage() {
1087
+ const router = createRouter(routes);
1088
+
1089
+ return () => (
1090
+ <div>
1091
+ <p>Search: {router.queries.q || "none"}</p>
1092
+ <input
1093
+ value={router.queries.q || ""}
1094
+ onInput={(e) => router.setQuery({ q: e.target.value })}
1095
+ />
1096
+ </div>
1097
+ );
1098
+ }
1099
+ ```
1100
+
1101
+ **Reactive Routing:**
1102
+
1103
+ The router integrates with RASK's reactivity system. Accessing `router.route` or `router.queries` automatically tracks dependencies:
1104
+
1105
+ ```tsx
1106
+ function App() {
1107
+ const router = createRouter(routes);
1108
+ const state = createState({ posts: [] });
1109
+
1110
+ // Effect runs when route changes
1111
+ createEffect(() => {
1112
+ if (router.route?.name === "user") {
1113
+ // Fetch user data when route changes
1114
+ fetch(`/api/users/${router.route.params.id}`)
1115
+ .then((r) => r.json())
1116
+ .then((data) => (state.posts = data.posts));
1117
+ }
1118
+ });
1119
+
1120
+ return () => (
1121
+ <div>
1122
+ {router.route?.name === "user" && (
1123
+ <div>
1124
+ <h1>User {router.route.params.id}</h1>
1125
+ <ul>
1126
+ {state.posts.map((post) => (
1127
+ <li key={post.id}>{post.title}</li>
1128
+ ))}
1129
+ </ul>
1130
+ </div>
1131
+ )}
1132
+ </div>
1133
+ );
1134
+ }
1135
+ ```
1136
+
1137
+ **Type Safety:**
1138
+
1139
+ Routes are fully type-safe. TypeScript will infer parameter types from route patterns:
1140
+
1141
+ ```tsx
1142
+ const routes = {
1143
+ user: "/users/:id",
1144
+ post: "/posts/:postId/:commentId",
1145
+ } as const;
1146
+
1147
+ const router = createRouter(routes);
1148
+
1149
+ // ✅ Type-safe - id is required
1150
+ router.push("user", { id: "123" });
1151
+
1152
+ // ❌ Type error - missing required params
1153
+ router.push("post", { postId: "1" }); // Error: missing commentId
1154
+
1155
+ // ✅ Type-safe params access
1156
+ if (router.route?.name === "post") {
1157
+ const postId = router.route.params.postId; // string
1158
+ const commentId = router.route.params.commentId; // string
1159
+ }
1160
+ ```
1161
+
1162
+ **Context Pattern:**
1163
+
1164
+ Share router across components using context:
1165
+
1166
+ ```tsx
1167
+ import { createRouter, createContext } from "rask-ui";
1168
+
1169
+ const routes = {
1170
+ home: "/",
1171
+ about: "/about",
1172
+ } as const;
1173
+
1174
+ const RouterContext = createContext<Router<typeof routes>>();
1175
+
1176
+ function App() {
1177
+ const router = createRouter(routes);
1178
+
1179
+ RouterContext.inject(router);
1180
+
1181
+ return () => <Content />;
1182
+ }
1183
+
1184
+ function Content() {
1185
+ const router = RouterContext.get();
1186
+
1187
+ return () => (
1188
+ <nav>
1189
+ <button onClick={() => router.push("home")}>Home</button>
1190
+ <button onClick={() => router.push("about")}>About</button>
1191
+ </nav>
1192
+ );
1193
+ }
1194
+ ```
1195
+
1196
+ **Features:**
1197
+
1198
+ - **Type-safe** - Full TypeScript inference for routes and parameters
1199
+ - **Reactive** - Automatically tracks route changes
1200
+ - **Declarative** - Navigate using route names, not URLs
1201
+ - **Query parameters** - Built-in query string management
1202
+ - **No special components** - Use standard conditionals and component composition
1203
+ - **History API** - Built on the browser's History API
1204
+ - **Automatic cleanup** - Router listener cleaned up when component unmounts
1205
+
1206
+ ---
1207
+
991
1208
  ### Developer Tools
992
1209
 
993
1210
  #### `inspect(root, callback)`
@@ -1 +1 @@
1
- {"version":3,"file":"batch.d.ts","sourceRoot":"","sources":["../src/batch.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG;IAAE,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC;AAgElE,wBAAgB,KAAK,CAAC,EAAE,EAAE,cAAc,QAevC;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,QAgBvC"}
1
+ {"version":3,"file":"batch.d.ts","sourceRoot":"","sources":["../src/batch.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG;IAAE,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC;AA8ClE,wBAAgB,KAAK,CAAC,EAAE,EAAE,cAAc,QAiBvC;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,QAgBvC"}
package/dist/batch.js CHANGED
@@ -1,11 +1,7 @@
1
1
  const asyncQueue = [];
2
- const syncQueue = [];
2
+ const syncQueueStack = [];
3
3
  let inInteractive = 0;
4
4
  let asyncScheduled = false;
5
- let inSyncBatch = 0;
6
- // New: guards against re-entrant flushing
7
- let inAsyncFlush = false;
8
- let inSyncFlush = false;
9
5
  function scheduleAsyncFlush() {
10
6
  if (asyncScheduled)
11
7
  return;
@@ -13,77 +9,62 @@ function scheduleAsyncFlush() {
13
9
  queueMicrotask(flushAsyncQueue);
14
10
  }
15
11
  function flushAsyncQueue() {
16
- if (inAsyncFlush)
17
- return;
18
- inAsyncFlush = true;
19
12
  asyncScheduled = false;
20
- try {
21
- if (!asyncQueue.length)
22
- return;
23
- // Note: we intentionally DO NOT snapshot.
24
- // If callbacks queue more async work, it gets picked up
25
- // in this same loop because length grows.
26
- for (let i = 0; i < asyncQueue.length; i++) {
27
- const cb = asyncQueue[i];
28
- asyncQueue[i] = undefined;
29
- cb();
30
- cb.__queued = false;
31
- }
32
- asyncQueue.length = 0;
33
- }
34
- finally {
35
- inAsyncFlush = false;
13
+ if (!asyncQueue.length)
14
+ return;
15
+ // Note: we intentionally DO NOT snapshot.
16
+ // If callbacks queue more async work, it gets picked up
17
+ // in this same loop because length grows.
18
+ for (let i = 0; i < asyncQueue.length; i++) {
19
+ const cb = asyncQueue[i];
20
+ asyncQueue[i] = undefined;
21
+ cb();
22
+ cb.__queued = false;
36
23
  }
24
+ asyncQueue.length = 0;
37
25
  }
38
- function flushSyncQueue() {
39
- if (inSyncFlush)
26
+ function flushSyncQueue(queue) {
27
+ if (!queue.length)
40
28
  return;
41
- inSyncFlush = true;
42
- try {
43
- if (!syncQueue.length)
44
- return;
45
- // Same pattern as async: no snapshot, just iterate.
46
- // New callbacks queued via syncBatch inside this flush
47
- // will be pushed to syncQueue and picked up by this loop.
48
- for (let i = 0; i < syncQueue.length; i++) {
49
- const cb = syncQueue[i];
50
- syncQueue[i] = undefined;
51
- cb();
52
- cb.__queued = false;
53
- }
54
- syncQueue.length = 0;
55
- }
56
- finally {
57
- inSyncFlush = false;
29
+ // No snapshot, just iterate.
30
+ // New callbacks queued via nested syncBatch will create
31
+ // their own queue on the stack and flush independently.
32
+ for (let i = 0; i < queue.length; i++) {
33
+ const cb = queue[i];
34
+ queue[i] = undefined;
35
+ cb();
36
+ cb.__queued = false;
58
37
  }
38
+ queue.length = 0;
59
39
  }
60
40
  export function queue(cb) {
61
41
  // Optional: uncomment this if you want deduping:
62
42
  // if (cb.__queued) return;
63
43
  cb.__queued = true;
64
- if (inSyncBatch) {
65
- syncQueue.push(cb);
44
+ // If we're in a sync batch, push to the current sync queue
45
+ if (syncQueueStack.length) {
46
+ syncQueueStack[syncQueueStack.length - 1].push(cb);
66
47
  return;
67
48
  }
49
+ // Otherwise, push to async queue
68
50
  asyncQueue.push(cb);
69
51
  if (!inInteractive) {
70
52
  scheduleAsyncFlush();
71
53
  }
72
54
  }
73
55
  export function syncBatch(cb) {
74
- inSyncBatch++;
56
+ // Create a new queue for this sync batch
57
+ const queue = [];
58
+ syncQueueStack.push(queue);
75
59
  try {
76
60
  cb();
77
61
  }
78
62
  catch (e) {
79
- inSyncBatch--;
80
- throw e; // no flush on error
81
- }
82
- inSyncBatch--;
83
- if (!inSyncBatch) {
84
- // Only the outermost syncBatch triggers a flush.
85
- // If this happens *inside* an ongoing flushSyncQueue,
86
- // inSyncFlush will be true and flushSyncQueue will no-op.
87
- flushSyncQueue();
63
+ // Pop the queue even on error, but don't flush
64
+ syncQueueStack.pop();
65
+ throw e;
88
66
  }
67
+ // Pop the queue and flush it
68
+ syncQueueStack.pop();
69
+ flushSyncQueue(queue);
89
70
  }
@@ -17,6 +17,8 @@ export declare class RaskStatefulComponent<P extends Props<any>> extends Compone
17
17
  private reactiveProps?;
18
18
  private observer;
19
19
  private isRendering;
20
+ private willRender;
21
+ private nextProps;
20
22
  effects: Array<{
21
23
  isDirty: boolean;
22
24
  run: () => void;
@@ -32,7 +34,6 @@ export declare class RaskStatefulComponent<P extends Props<any>> extends Compone
32
34
  *
33
35
  */
34
36
  componentWillUpdate(nextProps: any): void;
35
- componentWillReceiveProps(): void;
36
37
  shouldComponentUpdate(nextProps: Props<any>): boolean;
37
38
  render(): any;
38
39
  }
@@ -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;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"}
1
+ {"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAExE,OAAO,EAAsB,QAAQ,EAAU,MAAM,eAAe,CAAC;AAIrE,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,CAKb;IAEH,OAAO,CAAC,WAAW,CAAS;IAE5B,OAAO,CAAC,UAAU,CAAQ;IAG1B,OAAO,CAAC,SAAS,CAAmB;IACpC,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;IAyD3B,iBAAiB,IAAI,IAAI;IAGzB,oBAAoB,IAAI,IAAI;IAG5B;;OAEG;IACH,mBAAmB,CAAC,SAAS,EAAE,GAAG;IAkBlC,qBAAqB,CAAC,SAAS,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO;IAWrD,MAAM;CA0CP;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,SAO9D"}
package/dist/component.js CHANGED
@@ -1,6 +1,7 @@
1
- import { createComponentVNode, Component, } from "inferno";
1
+ import { createComponentVNode, Component } from "inferno";
2
2
  import { getCurrentObserver, Observer, Signal } from "./observation";
3
3
  import { syncBatch } from "./batch";
4
+ import { PROXY_MARKER } from "./createState";
4
5
  export class RaskStatelessComponent extends Component {
5
6
  observer = new Observer(() => {
6
7
  this.forceUpdate();
@@ -44,9 +45,18 @@ export class RaskStatefulComponent extends Component {
44
45
  renderFn;
45
46
  reactiveProps;
46
47
  observer = new Observer(() => {
48
+ if (this.willRender) {
49
+ return;
50
+ }
47
51
  this.forceUpdate();
48
52
  });
53
+ // Flag to prevent props from tracking in render scope (We use props reconciliation)
49
54
  isRendering = false;
55
+ // Flag to prevent observer notifications to cause render during reconciliation
56
+ willRender = true;
57
+ // Since reactive props updates before the reconciliation (without causing a new one), we
58
+ // need to return these from the reactive props
59
+ nextProps = this.props;
50
60
  effects = [];
51
61
  contexts = new Map();
52
62
  getChildContext() {
@@ -64,44 +74,42 @@ export class RaskStatefulComponent extends Component {
64
74
  const reactiveProps = {};
65
75
  const self = this;
66
76
  const signals = new Map();
67
- for (const prop in this.props) {
68
- const value = this.props[prop];
77
+ for (const prop in this.nextProps) {
78
+ const value = this.nextProps[prop];
69
79
  // Skip known non-reactive props
70
- if (typeof value === "function" ||
71
- prop === "children" ||
72
- prop === "key" ||
73
- prop === "ref") {
80
+ if (prop === "key" || prop === "ref") {
74
81
  reactiveProps[prop] = value;
75
82
  continue;
76
83
  }
77
84
  // Skip objects/arrays - they're already reactive if they're proxies
78
85
  // No need to wrap them in additional signals
79
- if (typeof value === "object" && value !== null) {
86
+ if (typeof value === "object" &&
87
+ value !== null &&
88
+ PROXY_MARKER in value) {
80
89
  reactiveProps[prop] = value;
81
90
  continue;
82
91
  }
83
92
  // Only create reactive getters for primitives
84
93
  Object.defineProperty(reactiveProps, prop, {
94
+ enumerable: true,
85
95
  get() {
86
- if (!self.isRendering) {
87
- const observer = getCurrentObserver();
88
- if (observer) {
89
- // Lazy create signal only when accessed in reactive context
90
- let signal = signals.get(prop);
91
- if (!signal) {
92
- signal = new Signal();
93
- signals.set(prop, signal);
94
- }
95
- observer.subscribeSignal(signal);
96
+ const observer = getCurrentObserver();
97
+ if (!self.isRendering && observer) {
98
+ // Lazy create signal only when accessed in reactive context
99
+ let signal = signals.get(prop);
100
+ if (!signal) {
101
+ signal = new Signal();
102
+ signals.set(prop, signal);
96
103
  }
104
+ observer.subscribeSignal(signal);
97
105
  }
98
106
  // @ts-ignore
99
- return self.props[prop];
107
+ return self.nextProps[prop];
100
108
  },
101
109
  set(value) {
102
110
  // Only notify if signal was created (i.e., prop was accessed reactively)
103
111
  const signal = signals.get(prop);
104
- if (signal && self.props[prop] !== value) {
112
+ if (signal) {
105
113
  signal.notify();
106
114
  }
107
115
  },
@@ -119,9 +127,12 @@ export class RaskStatefulComponent extends Component {
119
127
  *
120
128
  */
121
129
  componentWillUpdate(nextProps) {
130
+ this.willRender = true;
131
+ this.nextProps = nextProps;
122
132
  syncBatch(() => {
123
133
  for (const prop in nextProps) {
124
- if (prop === "children") {
134
+ if (prop === "children" ||
135
+ this.props[prop] === nextProps[prop]) {
125
136
  continue;
126
137
  }
127
138
  // @ts-ignore
@@ -129,7 +140,6 @@ export class RaskStatefulComponent extends Component {
129
140
  }
130
141
  });
131
142
  }
132
- componentWillReceiveProps() { }
133
143
  shouldComponentUpdate(nextProps) {
134
144
  // Shallow comparison of props, excluding internal props
135
145
  for (const prop in nextProps) {
@@ -165,6 +175,7 @@ export class RaskStatefulComponent extends Component {
165
175
  this.isRendering = true;
166
176
  result = this.renderFn();
167
177
  this.isRendering = false;
178
+ this.willRender = false;
168
179
  }
169
180
  catch (error) {
170
181
  if (typeof this.context.notifyError !== "function") {
@@ -25,5 +25,6 @@
25
25
  export declare function createContext<T>(): {
26
26
  inject(value: T): void;
27
27
  get(): T;
28
+ hasValue(): boolean;
28
29
  };
29
30
  //# sourceMappingURL=createContext.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createContext.d.ts","sourceRoot":"","sources":["../src/createContext.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,wBAAgB,aAAa,CAAC,CAAC;kBAEb,CAAC;WASR,CAAC;EA0BX"}
1
+ {"version":3,"file":"createContext.d.ts","sourceRoot":"","sources":["../src/createContext.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,wBAAgB,aAAa,CAAC,CAAC;kBAEb,CAAC;WASR,CAAC;;EA2CX"}
@@ -46,6 +46,17 @@ export function createContext() {
46
46
  }
47
47
  return contextValue;
48
48
  },
49
+ hasValue() {
50
+ let currentComponent = getCurrentComponent();
51
+ if (!currentComponent) {
52
+ throw new Error("You can not get context outside component setup");
53
+ }
54
+ if (typeof currentComponent.context.getContext !== "function") {
55
+ return false;
56
+ }
57
+ const contextValue = currentComponent.context.getContext(context);
58
+ return Boolean(contextValue);
59
+ },
49
60
  };
50
61
  return context;
51
62
  }
@@ -1 +1 @@
1
- {"version":3,"file":"createEffect.d.ts","sourceRoot":"","sources":["../src/createEffect.ts"],"names":[],"mappings":"AAGA,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,QA6BzD"}
1
+ {"version":3,"file":"createEffect.d.ts","sourceRoot":"","sources":["../src/createEffect.ts"],"names":[],"mappings":"AAIA,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,QA8BzD"}
@@ -1,3 +1,4 @@
1
+ import { syncBatch } from "./batch";
1
2
  import { getCurrentComponent, createCleanup } from "./component";
2
3
  import { Observer } from "./observation";
3
4
  export function createEffect(cb) {
@@ -10,9 +11,7 @@ export function createEffect(cb) {
10
11
  }
11
12
  let disposer;
12
13
  const observer = new Observer(() => {
13
- // We trigger effects on micro task as synchronous observer notifications
14
- // (Like when components sets props) should not synchronously trigger effects
15
- queueMicrotask(runEffect);
14
+ syncBatch(runEffect);
16
15
  });
17
16
  const runEffect = () => {
18
17
  try {
@@ -26,7 +25,10 @@ export function createEffect(cb) {
26
25
  stopObserving();
27
26
  };
28
27
  if (currentComponent) {
29
- createCleanup(() => observer.dispose());
28
+ createCleanup(() => {
29
+ observer.dispose();
30
+ disposer?.();
31
+ });
30
32
  }
31
33
  runEffect();
32
34
  }
@@ -0,0 +1,8 @@
1
+ import { RoutesConfig, TRouter, TRoutes } from "typed-client-router";
2
+ export type Router<T extends RoutesConfig> = Omit<TRouter<T>, "current" | "listen" | "pathname"> & {
3
+ route?: TRoutes<T>;
4
+ };
5
+ export declare function createRouter<const T extends RoutesConfig>(config: T, options?: {
6
+ base?: string;
7
+ }): Router<T>;
8
+ //# sourceMappingURL=createRouter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createRouter.d.ts","sourceRoot":"","sources":["../src/createRouter.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,YAAY,EACZ,OAAO,EACP,OAAO,EACR,MAAM,qBAAqB,CAAC;AAI7B,MAAM,MAAM,MAAM,CAAC,CAAC,SAAS,YAAY,IAAI,IAAI,CAC/C,OAAO,CAAC,CAAC,CAAC,EACV,SAAS,GAAG,QAAQ,GAAG,UAAU,CAClC,GAAG;IACF,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;CACpB,CAAC;AAEF,wBAAgB,YAAY,CAAC,KAAK,CAAC,CAAC,SAAS,YAAY,EACvD,MAAM,EAAE,CAAC,EACT,OAAO,CAAC,EAAE;IACR,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GACA,MAAM,CAAC,CAAC,CAAC,CAwBX"}
@@ -0,0 +1,24 @@
1
+ import { createRouter as internalCreateRouter, } from "typed-client-router";
2
+ import { getCurrentObserver, Signal } from "./observation";
3
+ import { createCleanup } from "./component";
4
+ export function createRouter(config, options) {
5
+ const router = internalCreateRouter(config, options);
6
+ const signal = new Signal();
7
+ createCleanup(router.listen(() => signal.notify()));
8
+ return {
9
+ get route() {
10
+ const observer = getCurrentObserver();
11
+ if (observer) {
12
+ observer.subscribeSignal(signal);
13
+ }
14
+ return router.current;
15
+ },
16
+ get queries() {
17
+ return router.queries;
18
+ },
19
+ setQuery: router.setQuery,
20
+ push: router.push,
21
+ replace: router.replace,
22
+ url: router.url,
23
+ };
24
+ }
package/dist/index.d.ts CHANGED
@@ -10,5 +10,6 @@ export { createEffect } from "./createEffect";
10
10
  export { createComputed } from "./createComputed";
11
11
  export { syncBatch } from "./batch";
12
12
  export { inspect } from "./inspect";
13
+ export { Router, createRouter } from "./createRouter";
13
14
  export { createVNode, createComponentVNode, createFragment, createTextVNode, normalizeProps, Component, } from "inferno";
14
15
  //# sourceMappingURL=index.d.ts.map
@@ -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,EACL,aAAa,EACb,iBAAiB,EACjB,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,SAAS,IAAI,IAAI,EAAE,MAAM,cAAc,CAAC;AAC7D,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;AAEhD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,SAAS,IAAI,IAAI,EAAE,MAAM,cAAc,CAAC;AAC7D,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;AACpC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAGtD,OAAO,EACL,WAAW,EACX,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,cAAc,EACd,SAAS,GACV,MAAM,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -10,5 +10,6 @@ export { createEffect } from "./createEffect";
10
10
  export { createComputed } from "./createComputed";
11
11
  export { syncBatch } from "./batch";
12
12
  export { inspect } from "./inspect";
13
+ export { createRouter } from "./createRouter";
13
14
  // Re-export Inferno JSX runtime functions so users don't need to install Inferno directly
14
15
  export { createVNode, createComponentVNode, createFragment, createTextVNode, normalizeProps, Component, } from "inferno";
@@ -1,5 +1,5 @@
1
- import 'inferno';
2
- export { Fragment } from 'inferno';
1
+ import "inferno";
2
+ export { Fragment } from "inferno";
3
3
  export declare function jsx(type: any, props: any, key?: any): any;
4
4
  export declare function jsxs(type: any, props: any, key?: any): any;
5
5
  //# sourceMappingURL=jsx-runtime.d.ts.map
@@ -2,6 +2,6 @@
2
2
  // The actual JSX transformation is done by SWC plugin at build time
3
3
  // This file only provides type declarations for TypeScript's "jsx": "react-jsx" mode
4
4
  // Import inferno to get the global JSX namespace
5
- import 'inferno';
5
+ import "inferno";
6
6
  // Re-export Fragment for JSX fragment syntax
7
- export { Fragment } from 'inferno';
7
+ export { Fragment } from "inferno";