diff --git a/.changeset/fresh-camels-clap.md b/.changeset/fresh-camels-clap.md new file mode 100644 index 0000000..c32d941 --- /dev/null +++ b/.changeset/fresh-camels-clap.md @@ -0,0 +1,6 @@ +--- +'@effector/model-react': patch +'@effector/model': patch +--- + +Implement callback api for keyval: `keyval(() => ({state, api, key}))` diff --git a/.github/workflows/changes.yml b/.github/workflows/changes.yml index 5c138d4..3109145 100644 --- a/.github/workflows/changes.yml +++ b/.github/workflows/changes.yml @@ -9,13 +9,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: pnpm/action-setup@v2 - - uses: actions/setup-node@v3 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' cache: 'pnpm' - run: pnpm install --frozen-lockfile - - run: pnpm lint:changes + - run: pnpm lint:changes --since='origin/main' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a27fa7d..071e33c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,9 +7,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' cache: 'pnpm' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f5ca36a..c7ed1c7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,9 +9,9 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' cache: 'pnpm' diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index 86537a7..dd3c69b 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -10,9 +10,9 @@ jobs: version: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' cache: 'pnpm' diff --git a/package.json b/package.json index 8392207..ef39b82 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,9 @@ "dependencies": { "@typescript-eslint/eslint-plugin": "^8.0.1", "@typescript-eslint/parser": "^8.0.1", - "effector": "^23.2.2", + "effector": "^23.3.0", "effector-react": "^23.2.1", + "patronum": "^2.3.0", "react": "^18.3.1", "react-dom": "^18.3.1" }, @@ -48,7 +49,7 @@ "@vitest/ui": "^2.0.5", "bytes-iec": "^3.1.1", "dotenv": "^16.0.1", - "effector": "^23.2.2", + "effector": "^23.3.0", "eslint": "^9.9.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-typescript": "^18.0.0", diff --git a/packages/core/package.json b/packages/core/package.json index 4d282e4..ff5ea7e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -3,6 +3,6 @@ "version": "0.0.2", "type": "commonjs", "peerDependencies": { - "effector": "^23.2.2" + "effector": "^23.3.0" } } diff --git a/packages/core/src/__tests__/keyval/edit.test.ts b/packages/core/src/__tests__/keyval/edit.test.ts index b2980cf..d90168a 100644 --- a/packages/core/src/__tests__/keyval/edit.test.ts +++ b/packages/core/src/__tests__/keyval/edit.test.ts @@ -1,18 +1,17 @@ import { expect, test, describe } from 'vitest'; -import { combine } from 'effector'; -import { keyval, define } from '@effector/model'; +import { createStore, combine } from 'effector'; +import { keyval } from '@effector/model'; function createEntities(fill?: Array<{ id: string }>) { - const entities = keyval({ - key: 'id', - props: { - id: define.store(), - }, - create({ id: $id }) { - return { + const entities = keyval(() => { + const $id = createStore(''); + return { + key: 'id', + state: { + id: $id, idSize: combine($id, (id) => id.length), - }; - }, + }, + }; }); if (fill) { entities.edit.replaceAll(fill); @@ -23,16 +22,14 @@ function createEntities(fill?: Array<{ id: string }>) { function createUpdatableEntities( fill?: Array<{ id: string; count: number; tag: string }>, ) { - const entities = keyval({ - key: 'id', - props: { - id: define.store(), - count: define.store(), - tag: define.store(), - }, - create() { - return {}; - }, + const entities = keyval(() => { + const $id = createStore(''); + const $count = createStore(0); + const $tag = createStore(''); + return { + key: 'id', + state: { id: $id, count: $count, tag: $tag }, + }; }); if (fill) { entities.edit.replaceAll(fill); @@ -170,24 +167,23 @@ describe('edit.replaceAll', () => { expect(entities.$items.getState()).toEqual([{ id: 'baz', idSize: 3 }]); }); test('nested replaceAll', () => { - const entities = keyval({ - key: 'id', - props: { - id: define.store(), - }, - create() { - const childs = keyval({ + const entities = keyval(() => { + const $id = createStore(''); + const childs = keyval(() => { + const $id = createStore(''); + return { key: 'id', - props: { - id: define.store(), - }, - create() { - return {}; - }, - }); - return { childs }; - }, + state: { id: $id }, + }; + }); + return { + key: 'id', + state: { id: $id, childs }, + optional: ['childs'], + }; }); + entities.edit.replaceAll([{ id: 'foo', childs: [{ id: 'fooA' }] }]); + entities.edit.replaceAll([{ id: 'bar' }]); entities.edit.replaceAll([ { id: 'foo', childs: [{ id: 'fooA' }] }, { id: 'bar' }, diff --git a/packages/core/src/__tests__/keyval/index.test.ts b/packages/core/src/__tests__/keyval/index.test.ts index 62abaee..0ea5360 100644 --- a/packages/core/src/__tests__/keyval/index.test.ts +++ b/packages/core/src/__tests__/keyval/index.test.ts @@ -1,61 +1,58 @@ -import { expect, test, describe } from 'vitest'; -import { keyval, define } from '@effector/model'; +import { expect, test, describe, vi } from 'vitest'; +import { keyval } from '@effector/model'; import { createEvent, createStore } from 'effector'; +import { readonly } from 'patronum'; describe('support nested keyval', () => { test('nested keyval becomes an array', () => { - const entities = keyval({ - key: 'id', - props: { - id: define.store(), - }, - create() { - const childs = keyval({ + const entities = keyval(() => { + const $id = createStore(''); + const childs = keyval(() => { + const $id = createStore(''); + const $count = createStore(0); + return { key: 'id', - props: { - id: define.store(), - count: define.store(), + state: { + id: $id, + count: $count, }, - create() { - return {}; - }, - }); - return { childs }; - }, + }; + }); + return { + key: 'id', + state: { id: $id, childs }, + optional: ['childs'], + }; }); entities.edit.add({ id: 'baz' }); expect(entities.$items.getState()).toEqual([{ id: 'baz', childs: [] }]); }); test('updates nested keyval', () => { - const entities = keyval({ - key: 'id', - props: { - id: define.store(), - }, - create() { - const childs = keyval({ - key: 'id', - props: { - id: define.store(), - }, - create() { - const sum = createEvent(); - const $count = createStore(0); - $count.on(sum, (x, y) => x + y); - return { - state: { count: $count }, - api: { sum }, - }; - }, - }); + const entities = keyval(() => { + const childs = keyval(() => { + const $id = createStore(''); + const sum = createEvent(); + const $count = createStore(0); + $count.on(sum, (x, y) => x + y); return { - state: { childs }, - api: { - sum: childs.api.sum, - addChild: childs.edit.add, + key: 'id', + state: { + id: $id, + count: readonly($count), }, + api: { sum }, }; - }, + }); + const $id = createStore(''); + return { + key: 'id', + state: { id: $id, childs }, + api: { + sum: childs.api.sum, + addChild: childs.edit.add, + }, + optional: ['childs'], + }; }); entities.edit.add([{ id: 'foo' }, { id: 'bar' }]); entities.api.addChild({ @@ -93,20 +90,16 @@ describe('support nested keyval', () => { }); test('api support', () => { - const entities = keyval({ - key: 'id', - props: { - id: define.store(), - }, - create() { - const incBy = createEvent(); - const $count = createStore(0); - $count.on(incBy, (x, y) => x + y); - return { - state: { count: $count }, - api: { incBy }, - }; - }, + const entities = keyval(() => { + const $id = createStore(''); + const incBy = createEvent(); + const $count = createStore(0); + $count.on(incBy, (x, y) => x + y); + return { + key: 'id', + state: { id: $id, count: readonly($count) }, + api: { incBy }, + }; }); entities.edit.add([{ id: 'foo' }, { id: 'bar' }]); entities.api.incBy({ key: 'foo', data: 2 }); @@ -115,3 +108,22 @@ test('api support', () => { { id: 'bar', count: 0 }, ]); }); + +test('onMount support', () => { + const fn = vi.fn(); + const entities = keyval(() => { + const $id = createStore(0); + const onMount = createEvent(); + onMount.watch(() => fn()); + return { + key: 'id', + state: { id: $id }, + onMount, + }; + }); + expect(fn).toBeCalledTimes(0); + entities.edit.add({ id: 1 }); + expect(fn).toBeCalledTimes(1); + entities.edit.add({ id: 2 }); + expect(fn).toBeCalledTimes(2); +}); diff --git a/packages/core/src/__tests__/keyval/key.test.ts b/packages/core/src/__tests__/keyval/key.test.ts index e8a9d7a..4da4fd7 100644 --- a/packages/core/src/__tests__/keyval/key.test.ts +++ b/packages/core/src/__tests__/keyval/key.test.ts @@ -1,38 +1,15 @@ import { expect, test } from 'vitest'; -import { keyval, define } from '@effector/model'; - -test('key function', () => { - const entities = keyval({ - key: ({ id }) => id, - props: { - id: define.store(), - count: define.store(), - }, - create() { - return {}; - }, - }); - entities.edit.add([ - { id: 'foo', count: 0 }, - { id: 'bar', count: 0 }, - ]); - entities.edit.update({ id: 'foo', count: 1 }); - expect(entities.$items.getState()).toEqual([ - { id: 'foo', count: 1 }, - { id: 'bar', count: 0 }, - ]); -}); +import { createStore } from 'effector'; +import { keyval } from '@effector/model'; test('key string', () => { - const entities = keyval({ - key: 'id', - props: { - id: define.store(), - count: define.store(), - }, - create() { - return {}; - }, + const entities = keyval(() => { + const $id = createStore(''); + const $count = createStore(0); + return { + key: 'id', + state: { id: $id, count: $count }, + }; }); entities.edit.add([ { id: 'foo', count: 0 }, diff --git a/packages/core/src/__tests__/keyval/lens.test.ts b/packages/core/src/__tests__/keyval/lens.test.ts index 5afc532..a75b740 100644 --- a/packages/core/src/__tests__/keyval/lens.test.ts +++ b/packages/core/src/__tests__/keyval/lens.test.ts @@ -1,20 +1,18 @@ import { expect, test, describe } from 'vitest'; import { createStore, createEvent, sample } from 'effector'; -import { keyval, define, lens } from '@effector/model'; +import { keyval, lens } from '@effector/model'; function createUpdatableEntities( fill?: Array<{ id: string; count: number; tag: string }>, ) { - const entities = keyval({ - key: 'id', - props: { - id: define.store(), - count: define.store(), - tag: define.store(), - }, - create() { - return {}; - }, + const entities = keyval(() => { + const $id = createStore(''); + const $count = createStore(0); + const $tag = createStore(''); + return { + key: 'id', + state: { id: $id, count: $count, tag: $tag }, + }; }); if (fill) { entities.edit.replaceAll(fill); @@ -31,32 +29,24 @@ function createNestedEntities( }[]; }>, ) { - const entities = keyval({ - key: 'id', - props: { - id: define.store(), - }, - create() { - const childs = keyval({ - key: 'id', - props: { - id: define.store(), - }, - create() { - const updateCount = createEvent(); - const $count = createStore(0); - $count.on(updateCount, (_, upd) => upd); - return { - state: { count: $count }, - api: { updateCount }, - }; - }, - }); + const entities = keyval(() => { + const $id = createStore(''); + const childs = keyval(() => { + const $id = createStore(''); + const updateCount = createEvent(); + const $count = createStore(0); + $count.on(updateCount, (_, upd) => upd); return { - state: { childs }, - api: { updateCount: childs.api.updateCount }, + key: 'id', + state: { id: $id, count: $count }, + api: { updateCount }, }; - }, + }); + return { + key: 'id', + state: { id: $id, childs }, + api: { updateCount: childs.api.updateCount }, + }; }); if (fill) { entities.edit.replaceAll(fill); diff --git a/packages/core/src/example.ts b/packages/core/src/example.ts index 174288f..222f3e9 100644 --- a/packages/core/src/example.ts +++ b/packages/core/src/example.ts @@ -11,78 +11,76 @@ const $email = createStore(''); const triggerValidation = createEvent(); const validateDefaultFx = createEffect((age: number): boolean => true); -const profile = model({ - props: { - age: 0, - email: $email, - triggerValidation, - validateDefaultFx, - validateFx: (email: string): boolean => true, - click: define.event(), - }, - create({ age, email, triggerValidation, validateDefaultFx, validateFx }) { - return { - foo: createStore(0), - }; - }, -}); +// const profile = model({ +// props: { +// age: 0, +// email: $email, +// triggerValidation, +// validateDefaultFx, +// validateFx: (email: string): boolean => true, +// click: define.event(), +// }, +// create({ age, email, triggerValidation, validateDefaultFx, validateFx }) { +// return { +// foo: createStore(0), +// }; +// }, +// }); -const aliceProfile = spawn(profile, { - age: createStore(18), - email: '', - validateDefaultFx: (age) => age > 18, - validateFx: createEffect((email: string) => email.length > 0), - // triggerValidation: createEvent(), - click: createEvent(), -}); +// const aliceProfile = spawn(profile, { +// age: createStore(18), +// email: '', +// validateDefaultFx: (age) => age > 18, +// validateFx: createEffect((email: string) => email.length > 0), +// // triggerValidation: createEvent(), +// click: createEvent(), +// }); -const bobProfile = spawn(profile, { - age: 20, - email: createStore(''), - validateDefaultFx: createEffect((age: number) => age > 18), - validateFx: async (email) => email.length > 0, - click: createEvent(), -}); +// const bobProfile = spawn(profile, { +// age: 20, +// email: createStore(''), +// validateDefaultFx: createEffect((age: number) => age > 18), +// validateFx: async (email) => email.length > 0, +// click: createEvent(), +// }); type Field = { name: string; value: string; }; -const fieldList1 = keyval({ - key: 'name', - props: { - name: define.store(), - value: define.store(), - }, - create({ value }) { - const submit = createEvent(); - const $isValid = combine(value, (value) => value.length > 0); - return { - state: { - isValid: $isValid, - }, - api: { - submit, - }, - }; - }, +const fieldList1 = keyval(() => { + const $name = createStore(''); + const $value = createStore(''); + const submit = createEvent(); + const $isValid = combine($value, (value) => value.length > 0); + return { + key: 'name', + state: { + name: $name, + value: $value, + isValid: $isValid, + }, + api: { + submit, + }, + }; }); fieldList1.api.submit; lens(fieldList1, $email).isValid.store; -const fieldList2 = keyval({ - key: 'name', - props: { - name: define.store(), - value: define.store(), - }, - create({ value }) { - const $isValid = combine(value, (value) => value.length > 0); - return { +const fieldList2 = keyval(() => { + const $name = createStore(''); + const $value = createStore(''); + const $isValid = combine($value, (value) => value.length > 0); + return { + key: 'name', + state: { + name: $name, + value: $value, isValid: $isValid, - }; - }, + }, + }; }); diff --git a/packages/core/src/keyval.ts b/packages/core/src/keyval.ts index 6101cb6..93065bc 100644 --- a/packages/core/src/keyval.ts +++ b/packages/core/src/keyval.ts @@ -1,35 +1,29 @@ import { createStore, createEvent, - createEffect, attach, sample, Store, StoreWritable, withRegion, - combine, clearNode, launch, EventCallable, Event, - Effect, - is, } from 'effector'; import type { Keyval, Model, StoreDef, - EventDef, - EffectDef, InstanceOf, Show, ConvertToLensShape, StructKeyval, } from './types'; import { spawn } from './spawn'; -import { isDefine, isKeyval } from './define'; import { model } from './model'; +import type { SetOptional } from './setOptional'; type ToPlainShape = { [K in { @@ -67,143 +61,56 @@ type ToPlainShape = { // > export function keyval< - Input extends { - [key: string]: - | Store - | Event - | Effect - | StoreDef - | EventDef - | EffectDef - | unknown; - }, - Output extends { - [key: string]: Store | Keyval | unknown; - } = {}, - OutputUnwrap = Output extends { state: infer T } - ? T - : Output extends { api: unknown } - ? {} - : Output, - Api extends { - [key: string]: Event | Effect; - } = {}, - InputPlain extends { - [K in keyof Input]: Input[K] extends - | Event - | Effect - | EventDef - | EffectDef - | ((params: unknown) => unknown) - ? never - : Input[K] extends Store - ? V - : Input[K] extends StoreDef - ? V - : Input[K]; - } = { - [K in keyof Input]: Input[K] extends - | Event - | Effect - | EventDef - | EffectDef - | ((params: unknown) => unknown) - ? never - : Input[K] extends Store - ? V - : Input[K] extends StoreDef - ? V - : Input[K]; - }, - OutputPlain extends { - [K in keyof OutputUnwrap]: OutputUnwrap[K] extends Keyval< - any, - infer V, - any, - any - > - ? V[] - : OutputUnwrap[K] extends Store - ? V - : never; - } = { - [K in keyof OutputUnwrap]: OutputUnwrap[K] extends Keyval< + ReactiveState, + FullState extends { + [K in keyof ReactiveState]: ReactiveState[K] extends Keyval< any, infer V, any, any > ? V[] - : OutputUnwrap[K] extends Store + : ReactiveState[K] extends Store ? V : never; }, - OutputWritable = { + WritableState extends { [K in { - [P in keyof OutputUnwrap]: OutputUnwrap[P] extends Keyval< + [P in keyof ReactiveState]: ReactiveState[P] extends Keyval< any, any, any, any > ? P - : OutputUnwrap[P] extends StoreWritable + : ReactiveState[P] extends StoreWritable ? P : never; - }[keyof OutputUnwrap]]: OutputUnwrap[K] extends Keyval< - any, + }[keyof ReactiveState]]: ReactiveState[K] extends Keyval< infer V, any, + any, any > ? V[] - : OutputUnwrap[K] extends StoreWritable + : ReactiveState[K] extends StoreWritable ? V : never; }, ->(options: { - key: ((entity: InputPlain) => string | number) | keyof Input; - props: Input; - create: ( - props: { - [K in keyof Input]: Input[K] extends - | Store - | Event - | Effect - ? Input[K] - : Input[K] extends StoreDef - ? Store - : Input[K] extends EventDef - ? Event - : Input[K] extends EffectDef - ? Effect - : Input[K] extends (params: infer P) => infer R - ? Effect> - : Store; - } & { - [K in { - [P in keyof Input]: Input[P] extends Store | StoreDef - ? P - : never; - }[keyof Input] as K extends string - ? `$${K}` - : never]: Input[K] extends Store - ? Input[K] - : Input[K] extends StoreDef - ? Store - : never; - }, - config: { onMount: Event }, - ) => - | { state: Output; api: Api } - | { state?: never; api: Api } - | { state: Output; api?: never } - | Output; -}): Keyval< - Show>, - Show, + Api = {}, + OptionalFields extends keyof WritableState = never, +>( + create: (config: { onMount: Event }) => { + state: ReactiveState; + api?: Api; + key: keyof ReactiveState; + optional?: ReadonlyArray; + }, +): Keyval< + SetOptional, + FullState, Api, - Show> + Show> >; // export function keyval(options: { // key: ((entity: Input) => string | number) | keyof Input; @@ -225,17 +132,32 @@ export function keyval(options: { export function keyval(options: { key: ((entity: T) => string | number) | keyof T; }): Keyval; -export function keyval({ - key: getKeyRaw, - shape = {} as Shape, - props, - create, -}: { - key: ((entity: Input) => string | number) | keyof Input; - shape?: Shape; - props?: any; - create?: any; -}): Keyval { +export function keyval( + options: + | { + key: ((entity: Input) => string | number) | keyof Input; + shape?: Shape; + props?: any; + create?: any; + } + | Function, +): Keyval { + let create: + | void + | ((config: { onMount: Event }) => { + state: unknown; + api?: unknown; + key: string; + optional?: string[]; + }); + // @ts-expect-error bad implementation + let getKeyRaw; + let shape: Shape; + if (typeof options === 'function') { + create = options as any; + } else { + ({ key: getKeyRaw, shape = {} as Shape, create } = options); + } type Enriched = Input & ModelEnhance; let kvModel: | Model< @@ -251,19 +173,22 @@ export function keyval({ Shape > | undefined; - if (props && create) { + if (create) { // @ts-expect-error typecast - kvModel = model({ props, create }); + kvModel = model({ create }); } type ListState = { items: Enriched[]; + // @ts-expect-error type mismatch instances: Array>>; keys: Array; }; - const getKey = - typeof getKeyRaw === 'function' + const getKey = !kvModel + ? typeof getKeyRaw === 'function' ? getKeyRaw - : (entity: Input) => entity[getKeyRaw] as string | number; + : // @ts-expect-error bad implementation + (entity: Input) => entity[getKeyRaw] as string | number + : (entity: Input) => entity[kvModel.keyField] as string | number; const $entities = createStore({ items: [], instances: [], @@ -319,19 +244,11 @@ export function keyval({ // @ts-expect-error some issues with types const instance = spawn(kvModel, item1); withRegion(instance.region, () => { - const storeOutputs = {} as Record>; - for (const key in instance.props) { - const value = instance.props[key]; - storeOutputs[key] = isKeyval(value) - ? value.$items - : (value as Store); - } - const $enriching = combine(storeOutputs); // obviosly dirty hack, wont make it way to release - const enriching = $enriching.getState(); - freshState.items.push({ ...inputItem, ...enriching } as Enriched); + const enriching = instance.output.getState(); + freshState.items.push(enriching as Enriched); sample({ - source: $enriching, + source: instance.output, fn: (partial) => ({ key, partial: partial as Partial, @@ -363,6 +280,13 @@ export function keyval({ }); // @ts-expect-error some issues with types freshState.instances.push(instance); + if (instance.onMount) { + launch({ + target: instance.onMount, + params: undefined, + defer: true, + }); + } } else { // typecast, there is no kvModel so Input === Enriched freshState.items.push(inputItem as Enriched); @@ -384,21 +308,6 @@ export function keyval({ ...inputUpdate, }; freshState.items[idx] = newItem; - if (kvModel) { - const instance = freshState.instances[idx]; - for (const key in instance.inputs) { - // @ts-expect-error cannot read newItem[key], but its ok - const upd = newItem[key]; - // @ts-expect-error cannot read oldItem[key], but its ok - if (upd !== oldItem[key]) { - launch({ - target: instance.inputs[key], - params: upd, - defer: true, - }); - } - } - } } const addFx = attach({ @@ -442,7 +351,6 @@ export function keyval({ if (kvModel) { for (const instance of oldState.instances) { clearNode(instance.region); - instance.unmount(); } } const state: ListState = { @@ -453,19 +361,18 @@ export function keyval({ for (const item of newItems) { const key = getKey(item); runNewItemInstance(state, key, item); - /** new instance is always last */ - const instance = state.instances[state.instances.length - 1]; - for (const key in item) { - if (key in writableOutputs) { - launch({ - target: - writableOutputs[key] === 'keyval' - ? (instance.props[key] as any).edit.replaceAll - : // @ts-expect-error typescript is broken here - instance.props[key], - params: item[key], - defer: true, - }); + if (kvModel) { + /** new instance is always last */ + const instance = state.instances[state.instances.length - 1]; + for (const field of kvModel.keyvalFields) { + // @ts-expect-error type mismatch, item is iterable + if (field in item) { + launch({ + target: instance.keyvalShape[field].edit.replaceAll, + params: (item as any)[field], + defer: true, + }); + } } } } @@ -536,7 +443,6 @@ export function keyval({ const [instance] = state.instances.splice(idx, 1); if (instance) { clearNode(instance.region); - instance.unmount(); } } return state; @@ -572,28 +478,13 @@ export function keyval({ }); const api = {} as Record>; - const writableOutputs = {} as Record; let structShape: any = null; if (kvModel) { - const initShape = {} as Record; - /** for in leads to typescript errors */ - Object.entries(kvModel.shape).forEach(([key, def]) => { - if (isDefine.store(def)) { - initShape[key] = createStore({}); - } else if (isDefine.event(def)) { - initShape[key] = createEvent(); - } else if (isDefine.effect(def)) { - initShape[key] = createEffect(() => {}); - } - }); - const consoleError = console.error; - console.error = () => {}; // @ts-expect-error type issues - const instance = spawn(kvModel, initShape); - console.error = consoleError; - instance.unmount(); + const instance = spawn(kvModel, {}); + clearNode(instance.region); structShape = { type: 'structKeyval', getKey, @@ -633,22 +524,14 @@ export function keyval({ return state; }); } - for (const key in instance.props) { - const value = instance.props[key]; - if (isKeyval(value)) { - writableOutputs[key] = 'keyval'; - } else if (is.store(value) && is.targetable(value)) { - writableOutputs[key] = 'store'; - } - } } return { type: 'keyval', api: api as any, + // @ts-expect-error bad implementation __lens: shape, __struct: structShape, - __getKey: getKey, $items: $entities.map(({ items }) => items), $keys: $entities.map(({ keys }) => keys), edit: { diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index 8d70479..d7bb50f 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -1,4 +1,15 @@ -import { Store, Event, Effect, createStore, createEffect, is } from 'effector'; +import { + Store, + Event, + Effect, + createNode, + withRegion, + createEvent, + clearNode, + is, + Node, + EventCallable, +} from 'effector'; import type { Model, @@ -8,8 +19,10 @@ import type { Keyval, Show, ConvertToLensShape, + FactoryPathMap, + StructShape, } from './types'; -import { define, isDefine } from './define'; +import { define, isKeyval } from './define'; export function model< Input extends { @@ -32,45 +45,15 @@ export function model< [key: string]: Event | Effect; } = {}, >({ - props, create, }: { - props: Input; - create: ( - props: { - [K in keyof Input]: Input[K] extends - | Store - | Event - | Effect - ? Input[K] - : Input[K] extends StoreDef - ? Store - : Input[K] extends EventDef - ? Event - : Input[K] extends EffectDef - ? Effect - : Input[K] extends (params: infer P) => infer R - ? Effect> - : Store; - } & { - [K in { - [P in keyof Input]: Input[P] extends Store | StoreDef - ? P - : never; - }[keyof Input] as K extends string - ? `$${K}` - : never]: Input[K] extends Store - ? Input[K] - : Input[K] extends StoreDef - ? Store - : never; - }, - config: { onMount: Event }, - ) => - | { state: Output; api: Api } - | { state?: never; api: Api } - | { state: Output; api?: never } - | Output; + create: () => { + state: Output; + api?: Api; + key: string; + optional?: string[]; + onMount?: EventCallable; + }; }): Model< Input, Show<{ @@ -88,29 +71,130 @@ export function model< // }[keyof Output]]: Output[K]; // } > { + const region = createNode({ regional: true }); + const { + state = {} as Output, + key, + api = {} as Api, + optional = [], + onMount, + ...rest + } = withRegion(region, () => { + return create(); + }); + + if (Object.keys(rest).length > 0) { + throw Error( + `create should return only fields 'key', 'state', 'api', 'optional' or 'onMount'`, + ); + } else if (typeof key !== 'string') { + throw Error(`key field should be a string`); + } else if (!(key in state)) { + throw Error(`key field "${key}" should be in state`); + } else if (!is.store(state[key]) || !is.targetable(state[key])) { + throw Error(`key field "${key}" should be writable store`); + } else if (optional.includes(key)) { + throw Error(`key field "${key}" cannot be optional`); + } + if (onMount !== undefined && (!is.unit(onMount) || !is.targetable(onMount))) { + throw Error('onMount should be callable event or effect'); + } + + const requiredStateFields = Object.keys(state).filter( + (field: keyof Output) => + is.store(state[field]) && + is.targetable(state[field]) && + !optional.includes(field as string), + ) as Array; + + const factoryStatePaths = collectFactoryPaths(state, region); + + const keyvalFields = Object.keys(state).filter((field: keyof Output) => + isKeyval(state[field]), + ) as Array; + const shape = {} as any; - for (const key in props) { - const value = props[key]; - if (isDefine.any(value)) { - shape[key] = value; - } else if (is.store(value)) { - shape[key] = define.store(); - } else if (is.event(value)) { - shape[key] = define.event(); - } else if (is.effect(value) || typeof value === 'function') { - shape[key] = define.effect(); - } else { - shape[key] = define.store(); - } + const structShape: StructShape = { + type: 'structShape', + shape: {}, + }; + for (const key in state) { + shape[key] = define.store(); + structShape.shape[key] = isKeyval(state[key]) + ? state[key].__struct + : { + type: 'structUnit', + unit: 'store', + }; } + for (const key in api) { + const value = api[key]; + shape[key] = is.event(value) + ? define.event() + : define.effect(); + structShape.shape[key] = { + type: 'structUnit', + unit: is.event(value) ? 'event' : 'effect', + }; + } + + clearNode(region); return { type: 'model', create, - propsConfig: props, - output: null as unknown as any, - api: null as unknown as any, + keyField: key, + requiredStateFields, + keyvalFields, + factoryStatePaths, shape, - shapeInited: false, __lens: {} as any, + __struct: structShape, }; } + +function collectFactoryPaths(state: Record, initRegion: Node) { + const factoryPathToStateKey: FactoryPathMap = new Map(); + for (const key in state) { + const value = state[key]; + if (is.store(value) && is.targetable(value)) { + const path = findNodeInTree((value as any).graphite, initRegion); + if (path) { + let nestedFactoryPathMap = factoryPathToStateKey; + for (let i = 0; i < path.length; i++) { + const step = path[i]; + const isLastStep = i === path.length - 1; + if (isLastStep) { + nestedFactoryPathMap.set(step, key); + } else { + let childFactoryPathMap = nestedFactoryPathMap.get(step); + if (!childFactoryPathMap) { + childFactoryPathMap = new Map(); + nestedFactoryPathMap.set(step, childFactoryPathMap); + } + nestedFactoryPathMap = childFactoryPathMap as FactoryPathMap; + } + } + } + } + } + return factoryPathToStateKey; +} + +function findNodeInTree( + searchNode: Node, + currentNode: Node, + path: number[] = [], +): number[] | void { + const idx = currentNode.family.links.findIndex((e) => e === searchNode); + if (idx !== -1) { + return [...path, idx]; + } else { + for (let i = 0; i < currentNode.family.links.length; i++) { + const linkNode = currentNode.family.links[i]; + if (linkNode.meta.isRegion) { + const result = findNodeInTree(searchNode, linkNode, [...path, i]); + if (result) return result; + } + } + } +} diff --git a/packages/core/src/setOptional.ts b/packages/core/src/setOptional.ts new file mode 100644 index 0000000..9cc2464 --- /dev/null +++ b/packages/core/src/setOptional.ts @@ -0,0 +1,116 @@ +import type { Show } from './types'; + +type IsEqual = + (() => G extends (A & G) | G ? 1 : 2) extends () => G extends + | (B & G) + | G + ? 1 + : 2 + ? true + : false; + +/** +Filter out keys from an object. + +Returns `never` if `Exclude` is strictly equal to `Key`. +Returns `never` if `Key` extends `Exclude`. +Returns `Key` otherwise. + +@example +``` +type Filtered = Filter<'foo', 'foo'>; +//=> never +``` + +@example +``` +type Filtered = Filter<'bar', string>; +//=> never +``` + +@example +``` +type Filtered = Filter<'bar', 'foo'>; +//=> 'bar' +``` + +@see {Except} +*/ +type Filter = + IsEqual extends true + ? never + : KeyType extends ExcludeType + ? never + : KeyType; + +/** +Create a type from an object type without certain keys. + +We recommend setting the `requireExactProps` option to `true`. + +This type is a stricter version of [`Omit`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-5.html#the-omit-helper-type). The `Omit` type does not restrict the omitted keys to be keys present on the given type, while `Except` does. The benefits of a stricter type are avoiding typos and allowing the compiler to pick up on rename refactors automatically. + +This type was proposed to the TypeScript team, which declined it, saying they prefer that libraries implement stricter versions of the built-in types ([microsoft/TypeScript#30825](https://github.com/microsoft/TypeScript/issues/30825#issuecomment-523668235)). + +@example +``` +import type {Except} from 'type-fest'; + +type Foo = { + a: number; + b: string; +}; + +type FooWithoutA = Except; +//=> {b: string} + +const fooWithoutA: FooWithoutA = {a: 1, b: '2'}; +//=> errors: 'a' does not exist in type '{ b: string; }' + +type FooWithoutB = Except; +//=> {a: number} & Partial> + +const fooWithoutB: FooWithoutB = {a: 1, b: '2'}; +//=> errors at 'b': Type 'string' is not assignable to type 'undefined'. + +// The `Omit` utility type doesn't work when omitting specific keys from objects containing index signatures. + +// Consider the following example: + +type UserData = { + [metadata: string]: string; + email: string; + name: string; + role: 'admin' | 'user'; +}; + +// `Omit` clearly doesn't behave as expected in this case: +type PostPayload = Omit; +//=> type PostPayload = { [x: string]: string; [x: number]: string; } + +// In situations like this, `Except` works better. +// It simply removes the `email` key while preserving all the other keys. +type PostPayload = Except; +//=> type PostPayload = { [x: string]: string; name: string; role: 'admin' | 'user'; } +``` + +@category Object +*/ +type Except = { + [KeyType in keyof ObjectType as Filter< + KeyType, + KeysType + >]: ObjectType[KeyType]; +} & {}; + +export type SetOptional< + BaseType, + Keys extends keyof BaseType, +> = BaseType extends unknown // To distribute `BaseType` when it's a union type. + ? Show< + // Pick just the keys that are readonly from the base type. + Except & + // Pick the keys that should be mutable from the base type and make them mutable. + Partial> + > + : never; diff --git a/packages/core/src/spawn.ts b/packages/core/src/spawn.ts index 51a03bb..10be2c2 100644 --- a/packages/core/src/spawn.ts +++ b/packages/core/src/spawn.ts @@ -7,9 +7,11 @@ import { is, createNode, withRegion, - clearNode, createEvent, launch, + Node, + combine, + EventCallable, } from 'effector'; import type { @@ -19,10 +21,10 @@ import type { EventDef, EffectDef, AnyDef, - StructUnit, - StructShape, + FactoryPathMap, + Keyval, } from './types'; -import { define, isDefine, isKeyval } from './define'; +import { isKeyval } from './define'; type ParamsNormalize< T extends { @@ -109,198 +111,88 @@ export function spawn< : never; }, ): Instance { - const region = createNode(); - const normProps = {} as { - [K in keyof T]: T[K] extends - | Store - | Event - | Effect - ? T[K] - : T[K] extends StoreDef - ? Store - : T[K] extends EventDef - ? Event - : T[K] extends EffectDef - ? Effect - : T[K] extends (params: infer P) => infer R - ? Effect> - : Store; - }; - let onMount: Event; - withRegion(region, () => { - onMount = createEvent(); - for (const key in model.propsConfig) { - const propModel = model.propsConfig[key]; - //@ts-expect-error - const propParams = params[key]; - const propParamsExist = key in params; - if (is.store(propModel)) { - if (propParamsExist) { - //@ts-expect-error - normProps[key] = getStoreParams(propParams); - } else { - //@ts-expect-error - normProps[key] = propModel; - } - } else if (is.event(propModel)) { - if (propParamsExist) { - /** Maybe we should allow to pass any unit to event field */ - if ( - is.event(propParams) || - is.effect(propParams) || - is.store(propParams) - ) { - normProps[key] = propParams; - } else { - throw Error(`spawn field "${key}" expect event`); - } - } else { - //@ts-expect-error - normProps[key] = propModel; - } - } else if (is.effect(propModel)) { - if (propParamsExist) { - //@ts-expect-error - normProps[key] = getEffectParams(key, propParams); - } else { - //@ts-expect-error - normProps[key] = propModel; - } - } else if (isDefine.store(propModel)) { - if (propParamsExist) { - //@ts-expect-error - normProps[key] = getStoreParams(propParams); - } else { - throw Error(`spawn field "${key}" expect store or value`); - } - } else if (isDefine.event(propModel)) { - if (propParamsExist && is.event(propParams)) { - normProps[key] = propParams; - } else { - throw Error(`spawn field "${key}" expect event`); - } - } else if (isDefine.effect(propModel)) { - if (propParamsExist) { - //@ts-expect-error - normProps[key] = getEffectParams(key, propParams); - } else { - throw Error(`spawn field "${key}" expect effect or function`); - } - } else if (typeof propModel === 'function') { - if (propParamsExist) { - //@ts-expect-error - normProps[key] = getEffectParams(key, propParams); - } else { - //@ts-expect-error - normProps[key] = createEffect(propModel); - } - } else { - if (propParamsExist) { - //@ts-expect-error - normProps[key] = getStoreParams(propParams); - } else { - //@ts-expect-error - normProps[key] = createStore(propModel); - } - } - } - }); + const region = createNode({ regional: true }); + installStateHooks(params as any, region, model.factoryStatePaths); const parentTracking = childInstancesTracking; childInstancesTracking = []; - const outputs = model.create(normProps, { onMount: onMount! }); - const childInstances = childInstancesTracking; + const outputs = withRegion(region, () => model.create()); childInstancesTracking = parentTracking; - let storeOutputs = {} as any; - let apiOutputs = {} as any; - /** its ok to not return anything */ - if (outputs !== undefined) { - if (typeof outputs !== 'object' || outputs === null) { - throw Error(`model body should return object or undefined`); - } - if ( - (outputs.state && !is.unit(outputs.state)) || - (outputs.api && !is.unit(outputs.api)) - ) { - storeOutputs = outputs.state ?? {}; - apiOutputs = outputs.api ?? {}; - } else { - storeOutputs = outputs; - } - for (const key in storeOutputs) { - const value = storeOutputs[key]; - if (!is.store(value) && !isKeyval(value)) { - throw Error(`model body in state key "${key}" should return store`); - } - } - for (const key in apiOutputs) { - const value = apiOutputs[key]; - if (!is.event(value) && !is.effect(value)) { - throw Error( - `model body in api key "${key}" should return event or effect`, - ); + const storeOutputs = outputs.state ?? {}; + const apiOutputs = outputs.api ?? {}; + const onMount: EventCallable | void = outputs.onMount; + function forEachKeyvalField( + cb: (kv: Keyval, field: keyof Output) => void, + ) { + for (const field of model.keyvalFields) { + if (isKeyval(storeOutputs[field])) { + cb(storeOutputs[field], field); } } } - if (!model.shapeInited) { - model.shapeInited = true; - const structShape: StructShape = { - type: 'structShape', - shape: {}, - }; - for (const key in normProps) { - const unit = normProps[key]; - structShape.shape[key] = { - type: 'structUnit', - unit: is.store(unit) ? 'store' : is.event(unit) ? 'event' : 'effect', - }; - } - for (const key in storeOutputs) { - // @ts-expect-error - model.shape[key] = define.store(); - structShape.shape[key] = isKeyval(storeOutputs[key]) - ? storeOutputs[key].__struct - : { - type: 'structUnit', - unit: 'store', - }; - } - for (const key in apiOutputs) { - const value = apiOutputs[key]; - // @ts-expect-error - model.shape[key] = is.event(value) - ? define.event() - : define.effect(); - structShape.shape[key] = { - type: 'structUnit', - unit: is.event(value) ? 'event' : 'effect', - }; - } - model.__struct = structShape; - } + const $output = withRegion(region, () => { + const resultShape = { + ...storeOutputs, + } as Output; + forEachKeyvalField(({ $items }, field) => { + // @ts-expect-error generic mismatch + resultShape[field] = $items; + }); + return combine(resultShape) as unknown as Store; + }); + const keyvalShape = {} as Instance['keyvalShape']; + + withRegion(region, () => { + forEachKeyvalField((kv, field) => { + keyvalShape[field] = kv; + }); + }); const result: Instance = { type: 'instance', + output: $output, + keyvalShape, props: storeOutputs, api: apiOutputs, - unmount() { - for (const child of childInstances) { - child.unmount(); - } - clearNode(region); - }, region, - inputs: normProps, + onMount, }; if (childInstancesTracking) { childInstancesTracking.push(result); } - launch({ - target: onMount!, - params: undefined, - defer: true, - }); return result; } +function installStateHooks( + initState: Record, + node: Node, + currentFactoryPathToStateKey: FactoryPathMap, +) { + wrapPush(node.family.links, (item, idx) => { + if (!currentFactoryPathToStateKey.has(idx)) return; + const currentPath = currentFactoryPathToStateKey.get(idx)!; + if (typeof currentPath === 'string') { + if (item.scope.state && currentPath in initState) { + item.scope.state.initial = initState[currentPath]; + item.scope.state.current = initState[currentPath]; + } + } else { + installStateHooks(initState, item, currentPath); + } + }); +} + +function wrapPush(arr: T[], cb: (item: T, realIdx: number) => void) { + const push = arr.push.bind(arr); + arr.push = (...args: T[]) => { + const idx = arr.length; + for (let i = 0; i < args.length; i++) { + const child = args[i]; + const realIdx = idx + i; + cb(child, realIdx); + } + return push(...args); + }; +} + function getStoreParams(propParams: any) { return is.store(propParams) ? propParams : createStore(propParams); } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index b049eca..fa10488 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,16 +1,30 @@ -import type { Store, Event, Effect, EventCallable, Node } from 'effector'; +import type { + Store, + Event, + Effect, + EventCallable, + Node, + UnitTargetable, +} from 'effector'; + +export type FactoryPathMap = Map; export type Model = { type: 'model'; // private - create: (props: any, config: { onMount: Event }) => any; + readonly keyField: keyof Props; + // private + readonly requiredStateFields: Array; + // private + readonly keyvalFields: Array; // private - readonly propsConfig: Props; - readonly output: Output; + readonly factoryStatePaths: FactoryPathMap; + // private + create: () => any; // private readonly __lens: Shape; // private - readonly api: Api; + // readonly api: Api; shape: Show< { [K in keyof Props]: Props[K] extends Store @@ -41,23 +55,19 @@ export type Model = { } >; // private - shapeInited: boolean; - // private - __struct?: StructShape; + __struct: StructShape; }; export type Instance = { type: 'instance'; // private - unmount(): void; + readonly output: Store; + // private + readonly keyvalShape: Record>; readonly props: Output; + onMount: UnitTargetable | void; // private region: Node; - // private - readonly inputs: Record< - string, - Store | Event | Effect - >; api: Api; }; @@ -158,6 +168,8 @@ export type ConvertToLensShape = { : never; }; +type OneOrMany = T | Array; + export type Keyval = { type: 'keyval'; api: { @@ -175,11 +187,11 @@ export type Keyval = { $keys: Store>; edit: { /** Add one or multiple entities to the collection */ - add: EventCallable; + add: EventCallable>; /** Add or replace one or multiple entities in the collection */ - set: EventCallable; + set: EventCallable>; /** Update one or multiple entities in the collection. Supports partial updates */ - update: EventCallable | Partial[]>; + update: EventCallable>>; /** Remove multiple entities from the collection, by id or by predicate */ remove: EventCallable boolean)>; /** Replace current collection with provided collection */ @@ -194,8 +206,6 @@ export type Keyval = { __lens: Shape; // private __struct: StructKeyval; - // private - __getKey: (input: Input) => string | number; }; export type StoreContext = { diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index a789dc0..14c8e0f 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -4,7 +4,7 @@ "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, + "noPropertyAccessFromIndexSignature": false, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "declaration": true, diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx index 526488f..a3caaec 100644 --- a/packages/react/src/index.tsx +++ b/packages/react/src/index.tsx @@ -5,7 +5,7 @@ import { useEffect, ReactNode, } from 'react'; -import type { Store, Event, Effect } from 'effector'; +import { type Store, type Event, type Effect, clearNode } from 'effector'; import { useList, useStoreMap, useUnit } from 'effector-react'; import type { @@ -114,7 +114,7 @@ export function ModelProvider< deps.push(value); } const instance = useMemo(() => spawn(model, value), deps); - useEffect(() => () => instance.unmount(), deps); + useEffect(() => () => clearNode(instance.region), deps); const currentStack = useContext(ModelStackContext); const nextStack = { type: 'instance' as const, diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json index 29bd979..b4c3245 100644 --- a/packages/react/tsconfig.json +++ b/packages/react/tsconfig.json @@ -5,7 +5,7 @@ "jsx": "react-jsx", "strict": true, "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, + "noPropertyAccessFromIndexSignature": false, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "declaration": true diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8a7d1cd..b84de3e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,11 +15,14 @@ importers: specifier: ^8.0.1 version: 8.0.1(eslint@9.9.0(jiti@1.21.6))(typescript@5.5.4) effector: - specifier: ^23.2.2 - version: 23.2.2 + specifier: ^23.3.0 + version: 23.3.0 effector-react: specifier: ^23.2.1 - version: 23.2.1(effector@23.2.2)(react@18.3.1) + version: 23.2.1(effector@23.3.0)(react@18.3.1) + patronum: + specifier: ^2.3.0 + version: 2.3.0(effector@23.3.0) react: specifier: ^18.3.1 version: 18.3.1 @@ -214,8 +217,8 @@ importers: packages/core: dependencies: effector: - specifier: ^23.2.2 - version: 23.2.2 + specifier: ^23.3.0 + version: 23.3.0 packages/react: dependencies: @@ -2794,6 +2797,10 @@ packages: resolution: {integrity: sha512-gzwATi9pgZQx0TNhM2LESmoUpEO+vhibLZPCvVzi7spMvKFwKnfJV2PFj4xqNFFSC35TXaznx30ne62dCQ6ZRQ==} engines: {node: '>=11.0.0'} + effector@23.3.0: + resolution: {integrity: sha512-ZnQ3POaNARlxT9+kxrK58PO/xmStBdxfPq0rceglENg8Ryxx/yx+1RsV/ziznrFPhLkZYc7NdDA1OKxnMW98/g==} + engines: {node: '>=11.0.0'} + ejs@3.1.9: resolution: {integrity: sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==} engines: {node: '>=0.10.0'} @@ -4722,6 +4729,11 @@ packages: resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} engines: {node: '>= 14.16'} + patronum@2.3.0: + resolution: {integrity: sha512-BfKIOpoymVz6XnkOn8Fi5QZ1a3r/3lXdd8BcdHmYDbIXPTIRnD1EPFBFev/DheWnOge6/ZswEqgNF2ANLGOxLw==} + peerDependencies: + effector: ^23 + picocolors@0.2.1: resolution: {integrity: sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==} @@ -9583,14 +9595,16 @@ snapshots: duplexer@0.1.2: {} - effector-react@23.2.1(effector@23.2.2)(react@18.3.1): + effector-react@23.2.1(effector@23.3.0)(react@18.3.1): dependencies: - effector: 23.2.2 + effector: 23.3.0 react: 18.3.1 use-sync-external-store: 1.2.0(react@18.3.1) effector@23.2.2: {} + effector@23.3.0: {} + ejs@3.1.9: dependencies: jake: 10.8.5 @@ -11803,6 +11817,10 @@ snapshots: pathval@2.0.0: {} + patronum@2.3.0(effector@23.3.0): + dependencies: + effector: 23.3.0 + picocolors@0.2.1: {} picocolors@1.0.0: {}