bunja 1.0.0 → 2.0.0-alpha.2

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/bunja.ts CHANGED
@@ -1,235 +1,353 @@
1
- export type Dep<T> = Bunja<T> | Scope<T>;
1
+ export interface BunjaFn {
2
+ <T>(init: () => T): Bunja<T>;
3
+ use: BunjaUseFn;
4
+ effect: BunjaEffectFn;
5
+ }
6
+ export const bunja: BunjaFn = bunjaFn;
7
+ function bunjaFn<T>(init: () => T): Bunja<T> {
8
+ return new Bunja(init);
9
+ }
10
+ bunjaFn.use = invalidUse as BunjaUseFn;
11
+ bunjaFn.effect = invalidEffect as BunjaEffectFn;
2
12
 
3
- const bunjaEffectSymbol: unique symbol = Symbol("Bunja.effect");
4
- type BunjaEffectSymbol = typeof bunjaEffectSymbol;
13
+ export type BunjaUseFn = <T>(dep: Dep<T>) => T;
14
+ export type BunjaEffectFn = (callback: BunjaEffectCallback) => void;
15
+ export type BunjaEffectCallback = () => (() => void) | void;
5
16
 
6
- export class Bunja<T> {
7
- public static readonly bunjas: Bunja<any>[] = [];
8
- public readonly id: number;
9
- public debugLabel: string = "";
10
- constructor(
11
- public deps: Dep<any>[], // one depth dependencies
12
- public parents: Bunja<any>[], // one depth parents
13
- public relatedBunjas: Bunja<any>[], // toposorted parents without self
14
- public relatedScopes: Scope<any>[], // deduped
15
- public init: (...args: any[]) => T & BunjaValue,
16
- ) {
17
- this.id = Bunja.bunjas.length;
18
- Bunja.bunjas.push(this);
19
- }
20
- static readonly effect: BunjaEffectSymbol = bunjaEffectSymbol;
21
- toString(): string {
22
- const { id, debugLabel } = this;
23
- return `[Bunja:${id}${debugLabel && ` - ${debugLabel}`}]`;
24
- }
17
+ export function createScope<T>(hash?: HashFn<T>): Scope<T> {
18
+ return new Scope(hash);
25
19
  }
26
20
 
27
- export type HashFn<T = any, U = any> = (value: T) => U;
21
+ export function createBunjaStore(): BunjaStore {
22
+ return new BunjaStore();
23
+ }
28
24
 
29
- export class Scope<T> {
30
- public static readonly scopes: Scope<any>[] = [];
31
- public readonly id: number;
32
- public debugLabel: string = "";
33
- constructor(public readonly hash: HashFn = id) {
34
- this.id = Scope.scopes.length;
35
- Scope.scopes.push(this);
36
- }
37
- toString(): string {
38
- const { id, debugLabel } = this;
39
- return `[Scope:${id}${debugLabel && ` - ${debugLabel}`}]`;
40
- }
25
+ export type Dep<T> = Bunja<T> | Scope<T>;
26
+
27
+ function invalidUse() {
28
+ throw new Error(
29
+ "`bunja.use` can only be used inside a bunja init function.",
30
+ );
31
+ }
32
+ function invalidEffect() {
33
+ throw new Error(
34
+ "`bunja.effect` can only be used inside a bunja init function.",
35
+ );
41
36
  }
42
37
 
43
- export type ReadScope = <T>(scope: Scope<T>) => T;
38
+ interface BunjaStoreGetContext {
39
+ bunjaInstance: BunjaInstance;
40
+ bunjaInstanceMap: BunjaInstanceMap;
41
+ scopeInstanceMap: ScopeInstanceMap;
42
+ }
43
+
44
+ type BunjaInstanceMap = Map<Bunja<unknown>, BunjaInstance>;
45
+ type ScopeInstanceMap = Map<Scope<unknown>, ScopeInstance>;
46
+
47
+ interface BunjaBakingContext {
48
+ currentBunja: Bunja<unknown>;
49
+ }
44
50
 
45
51
  export class BunjaStore {
46
52
  #bunjas: Record<string, BunjaInstance> = {};
47
- #scopes: Map<Scope<any>, Map<any, ScopeInstance>> = new Map();
48
- get<T>(
49
- bunja: Bunja<T>,
50
- readScope: ReadScope,
51
- ): {
52
- value: T;
53
- mount: () => () => void;
54
- deps: any[];
55
- } {
53
+ #scopes: Map<Scope<unknown>, Map<unknown, ScopeInstance>> = new Map();
54
+ #bakingContext: BunjaBakingContext | undefined;
55
+ dispose(): void {
56
+ for (const instance of Object.values(this.#bunjas)) instance.dispose();
57
+ for (const instanceMap of Object.values(this.#scopes)) {
58
+ for (const instance of instanceMap.values()) instance.dispose();
59
+ }
60
+ this.#bunjas = {};
61
+ this.#scopes = new Map();
62
+ }
63
+ get<T>(bunja: Bunja<T>, readScope: ReadScope): BunjaStoreGetResult<T> {
64
+ const originalUse = bunjaFn.use;
65
+ try {
66
+ const { bunjaInstance, bunjaInstanceMap, scopeInstanceMap } = bunja.baked
67
+ ? this.#getBaked(bunja, readScope)
68
+ : this.#getUnbaked(bunja, readScope);
69
+ return {
70
+ value: bunjaInstance.value as T,
71
+ mount: () => {
72
+ bunjaInstanceMap.forEach((instance) => instance.add());
73
+ bunjaInstance.add();
74
+ scopeInstanceMap.forEach((instance) => instance.add());
75
+ const unmount = () => {
76
+ setTimeout(() => {
77
+ bunjaInstanceMap.forEach((instance) => instance.sub());
78
+ bunjaInstance.sub();
79
+ scopeInstanceMap.forEach((instance) => instance.sub());
80
+ });
81
+ };
82
+ return unmount;
83
+ },
84
+ deps: Array.from(scopeInstanceMap.values()).map(({ value }) => value),
85
+ };
86
+ } finally {
87
+ bunjaFn.use = originalUse;
88
+ }
89
+ }
90
+ #getBaked<T>(bunja: Bunja<T>, readScope: ReadScope): BunjaStoreGetContext {
56
91
  const scopeInstanceMap = new Map(
57
92
  bunja.relatedScopes.map((scope) => [
58
93
  scope,
59
94
  this.#getScopeInstance(scope, readScope(scope)),
60
95
  ]),
61
96
  );
62
- const bunjaInstance = this.#getBunjaInstance(bunja, scopeInstanceMap);
63
- const { relatedBunjaInstanceMap } = bunjaInstance; // toposorted
64
- return {
65
- value: bunjaInstance.value as T,
66
- mount() {
67
- relatedBunjaInstanceMap.forEach((related) => related.add());
68
- bunjaInstance.add();
69
- scopeInstanceMap.forEach((scope) => scope.add());
70
- return function unmount(): void {
71
- // concern: reverse order?
72
- relatedBunjaInstanceMap.forEach((related) => related.sub());
73
- bunjaInstance.sub();
74
- scopeInstanceMap.forEach((scope) => scope.sub());
75
- };
76
- },
77
- deps: Array.from(scopeInstanceMap.values()).map(({ value }) => value),
78
- };
79
- }
80
- #getBunjaInstance(
81
- bunja: Bunja<any>,
82
- scopeInstanceMap: Map<Scope<any>, ScopeInstance>,
83
- ): BunjaInstance {
84
- const localScopeInstanceMap = new Map(
85
- bunja.relatedScopes.map((scope) => [scope, scopeInstanceMap.get(scope)!]),
86
- );
87
- const scopeInstanceIds = Array.from(localScopeInstanceMap.values())
88
- .map(({ instanceId }) => instanceId)
89
- .sort((a, b) => a - b);
90
- const bunjaInstanceId = `${bunja.id}:${scopeInstanceIds.join(",")}`;
91
- if (this.#bunjas[bunjaInstanceId]) return this.#bunjas[bunjaInstanceId];
92
- const relatedBunjaInstanceMap = new Map(
97
+ const bunjaInstanceMap = new Map(
93
98
  bunja.relatedBunjas.map((relatedBunja) => [
94
99
  relatedBunja,
95
100
  this.#getBunjaInstance(relatedBunja, scopeInstanceMap),
96
101
  ]),
97
102
  );
98
- const args = bunja.deps.map((dep) => {
99
- if (dep instanceof Bunja) return relatedBunjaInstanceMap.get(dep)!.value;
100
- if (dep instanceof Scope) return localScopeInstanceMap.get(dep)!.value;
101
- throw new Error("Invalid dependency");
102
- });
103
- const bunjaInstance = new BunjaInstance(
104
- () => delete this.#bunjas[bunjaInstanceId],
105
- bunjaInstanceId,
106
- relatedBunjaInstanceMap,
107
- bunja.init.apply(bunja, args),
103
+ bunjaFn.use = <T>(dep: Dep<T>) => {
104
+ if (dep instanceof Bunja) {
105
+ return bunjaInstanceMap.get(dep as Bunja<unknown>)!.value as T;
106
+ }
107
+ if (dep instanceof Scope) {
108
+ return scopeInstanceMap.get(dep as Scope<unknown>)!.value as T;
109
+ }
110
+ throw new Error("`bunja.use` can only be used with Bunja or Scope.");
111
+ };
112
+ const bunjaInstance = this.#getBunjaInstance(bunja, scopeInstanceMap);
113
+ return { bunjaInstance, bunjaInstanceMap, scopeInstanceMap };
114
+ }
115
+ #getUnbaked<T>(bunja: Bunja<T>, readScope: ReadScope): BunjaStoreGetContext {
116
+ const bunjaInstanceMap: BunjaInstanceMap = new Map();
117
+ const scopeInstanceMap: ScopeInstanceMap = new Map();
118
+ function getUse<D extends Dep<unknown>, I extends { value: unknown }>(
119
+ map: Map<D, I>,
120
+ addDep: (D: D) => void,
121
+ getInstance: (dep: D) => I,
122
+ ) {
123
+ return ((dep) => {
124
+ const d = dep as D;
125
+ addDep(d);
126
+ if (map.has(d)) return map.get(d)!.value as T;
127
+ const instance = getInstance(d);
128
+ map.set(d, instance);
129
+ return instance.value as T;
130
+ }) as <T>(dep: Dep<T>) => T;
131
+ }
132
+ const useScope = getUse(
133
+ scopeInstanceMap,
134
+ (dep) => this.#bakingContext!.currentBunja.addScope(dep),
135
+ (dep) => this.#getScopeInstance(dep, readScope(dep)),
136
+ );
137
+ const useBunja = getUse(
138
+ bunjaInstanceMap,
139
+ (dep) => this.#bakingContext!.currentBunja.addParent(dep),
140
+ (dep) => {
141
+ if (dep.baked) {
142
+ for (const scope of dep.relatedScopes) useScope(scope);
143
+ }
144
+ return this.#getBunjaInstance(dep, scopeInstanceMap);
145
+ },
108
146
  );
109
- this.#bunjas[bunjaInstanceId] = bunjaInstance;
110
- return bunjaInstance;
147
+ bunjaFn.use = <T>(dep: Dep<T>) => {
148
+ if (dep instanceof Bunja) return useBunja(dep) as T;
149
+ if (dep instanceof Scope) return useScope(dep) as T;
150
+ throw new Error("`bunja.use` can only be used with Bunja or Scope.");
151
+ };
152
+ try {
153
+ this.#bakingContext = { currentBunja: bunja };
154
+ const bunjaInstance = this.#getBunjaInstance(bunja, scopeInstanceMap);
155
+ return { bunjaInstance, bunjaInstanceMap, scopeInstanceMap };
156
+ } finally {
157
+ this.#bakingContext = undefined;
158
+ }
111
159
  }
112
- #getScopeInstance(scope: Scope<any>, value: any): ScopeInstance {
160
+ #getBunjaInstance<T>(
161
+ bunja: Bunja<T>,
162
+ scopeInstanceMap: ScopeInstanceMap,
163
+ ): BunjaInstance {
164
+ const originalEffect = bunjaFn.effect;
165
+ const prevBunja = this.#bakingContext?.currentBunja;
166
+ try {
167
+ const effects: BunjaEffectCallback[] = [];
168
+ bunjaFn.effect = (callback: BunjaEffectCallback) => {
169
+ effects.push(callback);
170
+ };
171
+ if (this.#bakingContext) this.#bakingContext.currentBunja = bunja;
172
+ if (bunja.baked) {
173
+ const id = bunja.calcInstanceId(scopeInstanceMap);
174
+ if (id in this.#bunjas) return this.#bunjas[id];
175
+ const bunjaInstanceValue = bunja.init();
176
+ return this.#createBunjaInstance(id, bunjaInstanceValue, effects);
177
+ } else {
178
+ const bunjaInstanceValue = bunja.init();
179
+ bunja.bake();
180
+ const id = bunja.calcInstanceId(scopeInstanceMap);
181
+ return this.#createBunjaInstance(id, bunjaInstanceValue, effects);
182
+ }
183
+ } finally {
184
+ bunjaFn.effect = originalEffect;
185
+ if (this.#bakingContext) this.#bakingContext.currentBunja = prevBunja!;
186
+ }
187
+ }
188
+ #getScopeInstance(scope: Scope<unknown>, value: unknown): ScopeInstance {
113
189
  const key = scope.hash(value);
114
- const scopeInstanceMap = this.#scopes.get(scope) ??
190
+ const instanceMap = this.#scopes.get(scope) ??
115
191
  this.#scopes.set(scope, new Map()).get(scope)!;
116
- const init = () =>
117
- new ScopeInstance(
118
- () => scopeInstanceMap.delete(key),
119
- ScopeInstance.counter++,
120
- scope,
121
- value,
122
- );
123
- return (
124
- scopeInstanceMap.get(key) ??
125
- scopeInstanceMap.set(key, init()).get(key)!
192
+ return instanceMap.get(key) ??
193
+ instanceMap.set(
194
+ key,
195
+ new ScopeInstance(value, () => instanceMap.delete(key)),
196
+ ).get(key)!;
197
+ }
198
+ #createBunjaInstance(
199
+ id: string,
200
+ value: unknown,
201
+ effects: BunjaEffectCallback[],
202
+ ): BunjaInstance {
203
+ const effect = () => {
204
+ const cleanups = effects
205
+ .map((effect) => effect())
206
+ .filter(Boolean) as (() => void)[];
207
+ return () => cleanups.forEach((cleanup) => cleanup());
208
+ };
209
+ const dispose = () => delete this.#bunjas[id];
210
+ const bunjaInstance = new BunjaInstance(id, value, effect, dispose);
211
+ this.#bunjas[id] = bunjaInstance;
212
+ return bunjaInstance;
213
+ }
214
+ }
215
+
216
+ export type ReadScope = <T>(scope: Scope<T>) => T;
217
+
218
+ export interface BunjaStoreGetResult<T> {
219
+ value: T;
220
+ mount: () => () => void;
221
+ deps: unknown[];
222
+ }
223
+
224
+ export class Bunja<T> {
225
+ private static counter: number = 0;
226
+ readonly id: string = String(Bunja.counter++);
227
+ debugLabel: string = "";
228
+ #phase: BunjaPhase = { baked: false, parents: new Set(), scopes: new Set() };
229
+ constructor(public init: () => T) {}
230
+ get baked(): boolean {
231
+ return this.#phase.baked;
232
+ }
233
+ get parents(): Bunja<unknown>[] {
234
+ if (this.#phase.baked) return this.#phase.parents;
235
+ return Array.from(this.#phase.parents);
236
+ }
237
+ get relatedBunjas(): Bunja<unknown>[] {
238
+ if (!this.#phase.baked) throw new Error("Bunja is not baked yet.");
239
+ return this.#phase.relatedBunjas;
240
+ }
241
+ get relatedScopes(): Scope<unknown>[] {
242
+ if (!this.#phase.baked) throw new Error("Bunja is not baked yet.");
243
+ return this.#phase.relatedScopes;
244
+ }
245
+ addParent(bunja: Bunja<unknown>): void {
246
+ if (this.#phase.baked) return;
247
+ this.#phase.parents.add(bunja);
248
+ }
249
+ addScope(scope: Scope<unknown>): void {
250
+ if (this.#phase.baked) return;
251
+ this.#phase.scopes.add(scope);
252
+ }
253
+ bake(): void {
254
+ if (this.#phase.baked) throw new Error("Bunja is already baked.");
255
+ const scopes = this.#phase.scopes;
256
+ const parents = this.parents;
257
+ const relatedBunjas = toposort(parents);
258
+ const relatedScopes = Array.from(
259
+ new Set([
260
+ ...relatedBunjas.flatMap((bunja) => bunja.relatedScopes),
261
+ ...scopes,
262
+ ]),
263
+ );
264
+ this.#phase = { baked: true, parents, relatedBunjas, relatedScopes };
265
+ }
266
+ calcInstanceId(scopeInstanceMap: Map<Scope<unknown>, ScopeInstance>): string {
267
+ const scopeInstanceIds = this.relatedScopes.map(
268
+ (scope) => scopeInstanceMap.get(scope)!.id,
126
269
  );
270
+ return `${this.id}:${scopeInstanceIds.join(",")}`;
271
+ }
272
+ toString(): string {
273
+ const { id, debugLabel } = this;
274
+ return `[Bunja:${id}${debugLabel && ` - ${debugLabel}`}]`;
127
275
  }
128
276
  }
129
277
 
130
- export const createBunjaStore = (): BunjaStore => new BunjaStore();
278
+ type BunjaPhase = BunjaPhaseUnbaked | BunjaPhaseBaked;
131
279
 
132
- export type BunjaEffectFn = () => () => void;
133
- export interface BunjaValue {
134
- [Bunja.effect]?: BunjaEffectFn;
280
+ interface BunjaPhaseUnbaked {
281
+ readonly baked: false;
282
+ readonly parents: Set<Bunja<unknown>>;
283
+ readonly scopes: Set<Scope<unknown>>;
135
284
  }
136
285
 
137
- function bunjaImpl<T, const U extends any[]>(
138
- deps: { [K in keyof U]: Dep<U[K]> },
139
- init: (...args: U) => T & BunjaValue,
140
- ): Bunja<T> {
141
- const parents = deps.filter((dep) => dep instanceof Bunja) as Bunja<any>[];
142
- const scopes = deps.filter((dep) => dep instanceof Scope) as Scope<any>[];
143
- const relatedBunjas = toposort(parents);
144
- const relatedScopes = Array.from(
145
- new Set([...scopes, ...parents.flatMap((parent) => parent.relatedScopes)]),
146
- );
147
- return new Bunja(deps, parents, relatedBunjas, relatedScopes, init as any);
148
- }
149
- bunjaImpl.effect = Bunja.effect;
150
-
151
- export const bunja: {
152
- <T>(deps: [], init: () => T & BunjaValue): Bunja<T>;
153
- <T, U>(deps: [Dep<U>], init: (u: U) => T & BunjaValue): Bunja<T>;
154
- <T, U, V>(
155
- deps: [Dep<U>, Dep<V>],
156
- init: (u: U, v: V) => T & BunjaValue,
157
- ): Bunja<T>;
158
- <T, U, V, W>(
159
- deps: [Dep<U>, Dep<V>, Dep<W>],
160
- init: (u: U, v: V, w: W) => T & BunjaValue,
161
- ): Bunja<T>;
162
- <T, U, V, W, X>(
163
- deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>],
164
- init: (u: U, v: V, w: W, x: X) => T & BunjaValue,
165
- ): Bunja<T>;
166
- <T, U, V, W, X, Y>(
167
- deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>, Dep<Y>],
168
- init: (u: U, v: V, w: W, x: X, y: Y) => T & BunjaValue,
169
- ): Bunja<T>;
170
- <T, U, V, W, X, Y, Z>(
171
- deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>, Dep<Y>, Dep<Z>],
172
- init: (u: U, v: V, w: W, x: X, y: Y, z: Z) => T & BunjaValue,
173
- ): Bunja<T>;
174
- readonly effect: BunjaEffectSymbol;
175
- } = bunjaImpl;
176
-
177
- export function createScope<T>(hash?: HashFn): Scope<T> {
178
- return new Scope(hash);
286
+ interface BunjaPhaseBaked {
287
+ readonly baked: true;
288
+ readonly parents: Bunja<unknown>[];
289
+ readonly relatedBunjas: Bunja<unknown>[];
290
+ readonly relatedScopes: Scope<unknown>[];
179
291
  }
180
292
 
181
- abstract class RefCounter {
182
- #disposed = false;
183
- #count = 0;
184
- add() {
185
- this.#count++;
186
- }
187
- sub() {
188
- this.#count--;
189
- setTimeout(() => {
190
- if (this.#disposed) return;
191
- if (this.#count < 1) {
192
- this.#disposed = true;
193
- this.dispose();
194
- }
195
- });
293
+ export class Scope<T> {
294
+ private static counter: number = 0;
295
+ readonly id: string = String(Scope.counter++);
296
+ debugLabel: string = "";
297
+ constructor(public readonly hash: HashFn<T> = Scope.identity) {}
298
+ private static identity<T>(x: T): T {
299
+ return x;
300
+ }
301
+ toString(): string {
302
+ const { id, debugLabel } = this;
303
+ return `[Scope:${id}${debugLabel && ` - ${debugLabel}`}]`;
196
304
  }
197
- abstract dispose(): void;
198
305
  }
199
306
 
200
- const id = <T>(x: T): T => x;
307
+ export type HashFn<T> = (value: T) => unknown;
308
+
201
309
  const noop = () => {};
310
+ abstract class RefCounter {
311
+ #count: number = 0;
312
+ abstract dispose(): void;
313
+ add(): void {
314
+ ++this.#count;
315
+ }
316
+ sub(): void {
317
+ --this.#count;
318
+ if (this.#count < 1) {
319
+ this.dispose();
320
+ this.dispose = noop;
321
+ }
322
+ }
323
+ }
324
+
202
325
  class BunjaInstance extends RefCounter {
203
326
  #cleanup: (() => void) | undefined;
204
- #dispose: () => void;
205
327
  constructor(
206
- dispose: () => void,
207
- public instanceId: string,
208
- public relatedBunjaInstanceMap: Map<Bunja<any>, BunjaInstance>,
209
- public value: BunjaValue,
328
+ public readonly id: string,
329
+ public readonly value: unknown,
330
+ public readonly effect: BunjaEffectCallback,
331
+ private readonly _dispose: () => void,
210
332
  ) {
211
333
  super();
212
- this.#dispose = () => {
213
- this.#cleanup?.();
214
- dispose();
215
- };
216
334
  }
217
- override add() {
218
- this.#cleanup ??= this.value[Bunja.effect]?.() ?? noop;
219
- super.add();
335
+ override dispose(): void {
336
+ this.#cleanup?.();
337
+ this._dispose();
220
338
  }
221
- dispose() {
222
- this.#dispose();
339
+ override add(): void {
340
+ this.#cleanup ??= this.effect() ?? noop;
341
+ super.add();
223
342
  }
224
343
  }
225
344
 
226
345
  class ScopeInstance extends RefCounter {
227
- public static counter = 0;
346
+ private static counter: number = 0;
347
+ readonly id: string = String(ScopeInstance.counter++);
228
348
  constructor(
229
- public dispose: () => void,
230
- public instanceId: number,
231
- public scope: Scope<any>,
232
- public value: any,
349
+ public readonly value: unknown,
350
+ public readonly dispose: () => void,
233
351
  ) {
234
352
  super();
235
353
  }
@@ -0,0 +1,118 @@
1
+ module.exports = function (fileInfo, api) {
2
+ const j = api.jscodeshift;
3
+ const root = j(fileInfo.source);
4
+ let modified = false;
5
+
6
+ root.find(j.CallExpression, { callee: { name: "bunja" } }).forEach((path) => {
7
+ const { node } = path;
8
+ const args = node.arguments;
9
+ if (args.length !== 2) return;
10
+
11
+ const [depsArray, initFn] = args;
12
+ if (depsArray.type !== "ArrayExpression") return;
13
+ if (
14
+ initFn.type !== "ArrowFunctionExpression" &&
15
+ initFn.type !== "FunctionExpression"
16
+ ) return;
17
+ const params = initFn.params;
18
+
19
+ const bodyStatements = initFn.body.type === "BlockStatement"
20
+ ? [...initFn.body.body]
21
+ : [{ type: "ReturnStatement", argument: initFn.body }];
22
+
23
+ const useStatements = depsArray.elements.map((dep, index) => {
24
+ if (index < params.length) {
25
+ return {
26
+ type: "VariableDeclaration",
27
+ kind: "const",
28
+ declarations: [{
29
+ type: "VariableDeclarator",
30
+ id: params[index],
31
+ init: {
32
+ type: "CallExpression",
33
+ callee: {
34
+ type: "MemberExpression",
35
+ object: { type: "Identifier", name: "bunja" },
36
+ property: { type: "Identifier", name: "use" },
37
+ computed: false,
38
+ },
39
+ arguments: [dep],
40
+ },
41
+ }],
42
+ };
43
+ } else {
44
+ return {
45
+ type: "ExpressionStatement",
46
+ expression: {
47
+ type: "CallExpression",
48
+ callee: {
49
+ type: "MemberExpression",
50
+ object: { type: "Identifier", name: "bunja" },
51
+ property: { type: "Identifier", name: "use" },
52
+ computed: false,
53
+ },
54
+ arguments: [dep],
55
+ },
56
+ };
57
+ }
58
+ });
59
+
60
+ for (let i = 0; i < bodyStatements.length; ++i) {
61
+ const statement = bodyStatements[i];
62
+ if (!statement || statement.type !== "ReturnStatement") continue;
63
+ if (statement.argument?.type !== "ObjectExpression") continue;
64
+ const returnObj = statement.argument;
65
+ const props = returnObj.properties;
66
+
67
+ const effectPropIndex = props.findIndex((prop) =>
68
+ prop?.computed &&
69
+ prop.key?.type === "MemberExpression" &&
70
+ prop.key?.object?.name === "bunja" &&
71
+ prop.key?.property?.name === "effect"
72
+ );
73
+ if (effectPropIndex === -1) continue;
74
+
75
+ const prop = props[effectPropIndex];
76
+ let effectFn = prop.value;
77
+
78
+ if (effectFn.type === "FunctionExpression") {
79
+ effectFn = {
80
+ type: "ArrowFunctionExpression",
81
+ params: effectFn.params,
82
+ body: effectFn.body,
83
+ };
84
+ }
85
+
86
+ const effectStatement = {
87
+ type: "ExpressionStatement",
88
+ expression: {
89
+ type: "CallExpression",
90
+ callee: {
91
+ type: "MemberExpression",
92
+ object: { type: "Identifier", name: "bunja" },
93
+ property: { type: "Identifier", name: "effect" },
94
+ computed: false,
95
+ },
96
+ arguments: [effectFn],
97
+ },
98
+ };
99
+
100
+ props.splice(effectPropIndex, 1);
101
+ bodyStatements.splice(i, 0, effectStatement);
102
+ ++i;
103
+ }
104
+
105
+ node.arguments = [{
106
+ type: "ArrowFunctionExpression",
107
+ params: [],
108
+ body: {
109
+ type: "BlockStatement",
110
+ body: [...useStatements, ...bodyStatements],
111
+ },
112
+ }];
113
+
114
+ modified = true;
115
+ });
116
+
117
+ return modified ? root.toSource() : fileInfo.source;
118
+ };