diff --git a/.changeset/shy-seals-cheer.md b/.changeset/shy-seals-cheer.md new file mode 100644 index 00000000..e8612a76 --- /dev/null +++ b/.changeset/shy-seals-cheer.md @@ -0,0 +1,5 @@ +--- +'@withease/redux': major +--- + +Initial release of Redux interop package for Effector diff --git a/apps/website/docs/.vitepress/config.js b/apps/website/docs/.vitepress/config.js index ccfd1673..1f764083 100644 --- a/apps/website/docs/.vitepress/config.js +++ b/apps/website/docs/.vitepress/config.js @@ -35,6 +35,7 @@ export default defineConfig({ text: 'Packages', items: [ { text: 'i18next', link: '/i18next/' }, + { text: 'redux', link: '/redux/' }, { text: 'web-api', link: '/web-api/' }, { text: 'factories', link: '/factories/' }, ], @@ -57,6 +58,7 @@ export default defineConfig({ { text: 'Get Started', link: '/i18next/' }, { text: 'Release policy', link: '/i18next/releases' }, ]), + ...createSidebar('redux', [{ text: 'Get Started', link: '/redux/' }]), ...createSidebar('web-api', [ { text: 'Get Started', link: '/web-api/' }, { @@ -122,6 +124,10 @@ export default defineConfig({ text: 'Events in UI-frameworks', link: '/magazine/handle_events_in_ui_frameworks', }, + { + text: 'Migrating from Redux to Effector', + link: '/magazine/migration_from_redux', + }, ], }, { diff --git a/apps/website/docs/index.md b/apps/website/docs/index.md index dd0247b6..af688b40 100644 --- a/apps/website/docs/index.md +++ b/apps/website/docs/index.md @@ -22,6 +22,11 @@ features: details: A powerful internationalization framework based on i18next link: /i18next/ linkText: Get Started + - icon: 🪝 + title: redux + details: Minimalistic package to allow simpler migration from Redux to Effector + link: /redux/ + linkText: Get Started - icon: 👩🏽‍💻 title: web-api details: Web API bindings — network status, tab visibility, and more diff --git a/apps/website/docs/magazine/migration_from_redux.md b/apps/website/docs/magazine/migration_from_redux.md new file mode 100644 index 00000000..38c2a7b3 --- /dev/null +++ b/apps/website/docs/magazine/migration_from_redux.md @@ -0,0 +1,742 @@ +# Migrating from Redux to Effector + +This guide explains how to perform a gradual, non-blocking code migration from Redux to Effector. + +## Preparation + +### Install effector + +First, you need to install the `effector` package. See [the official documentation for instructions](https://effector.dev/en/introduction/installation/). + +:::tip +It is also highly recommended setting up the official [Effector ESLint Plugin](https://eslint.effector.dev/), so it would be easier for you to follow Effector's best practices. +::: + +Also, it is recommended to read at least some of the Effector's docs, so it is easier to follow the guide. +E.g. you can read [Effector-related terminology here](https://effector.dev/en/explanation/glossary/). + +### Install @withease/redux + +This guide uses the `@withease/redux` package, which is a minimalistic set of helpers to simplify the migration, so it is recommended to install it too. + +See [the package documentation](/redux/) for detailed installation instructions. + +### Create Redux interoperability object + +In order for Redux and Effector to communicate effectively with each other, a special object must be created. + +You should do it by using `createReduxIntegration` method of the `@withease/redux` somewhere near the Redux Store configuration itself. + +:::info +Redux Toolkit `configureStore` is used here as an example, `@withease/redux` supports any kind of Redux Store. +::: + +```ts +// src/redux-store +import { createReduxIntegration } from '@withease/redux'; +import { configureStore } from '@reduxjs/tookit'; + +export const myReduxStore = configureStore({ + // ... +}); + +export const reduxInterop = createReduxIntegration({ + reduxStore: myReduxStore, + setup: appStarted, +}); +``` + +☝️ Notice, how explicit `setup` event is required to initialize the interoperability. Usually it would be an `appStarted` event or any other "app's lifecycle" event. + +You can read more about this best-practice [in the "Explicit start of the app" article](/magazine/explicit_start). + +It is recommended to pick a place in your project architecture and add a model for the app lifecycle events declaration: + +```ts +// e.g. shared/app-lifecycle/index.ts +import { createEvent } from 'effector'; + +export const appStarted = createEvent(); +``` + +And then call this event in the point, which corresponds to "start of the app" - usually this is somewhere near the render. + +```tsx +import { appStarted } from 'root/shared/app-lifecycle'; + +appStarted(); + +render(); +``` + +After that, you have everything ready to start a gradual migration. + +## Migration + +Now you have existing code with Redux" that implements the features of your product. +There is no point in stopping development altogether to migrate between technologies, this process should be integrated into the product development. + +:::tip +It is a good idea to select one of the existing functions in your code, rewrite it for the new technology and **show the resulting Pull Request to your colleagues** before starting a full-fledged migration. + +This way you can **evaluate** whether this technology helps you solve your problems and **how well it suits** your team. +::: + +This is a list of cases with examples of organizing a migration from Redux code to Effector code. + +### Migrating existing feature + +First thing you need to do in that case is to create an Effector model somewhere, where you want to put a new implementation. + +#### Effector API for the Redux code + +At first new model will only contain a "mirrored" stores and events, which are reading and sending updates to Redux Store: + +```ts +// src/features/user-info/model.ts +export const $userName = combine( + reduxInterop.$state, + (state) => state.userInfo.name ?? '' +); +export const updateName = reduxInterop.dispatch.prepend((name: string) => + userInfoSlice.updateName(name) +); +``` + +:::tip +It is recommended to use `.prepend` API of `reduxInterop.dispatch` effect to create separate Effector events, connected to their Redux action counterparts. + +The same is recommended for `reduxInterop.$state` - it is better to create separate stores via `combine` for "slices" of the Redux state, because it makes gradual migration easier. + +But since `reduxInterop.dispatch` is a normal Effect and `reduxInterop.$state` is a normal store, you can safely use both of them like so. +::: + +This model then can be used anywhere in place of classic actions and selectors. + +E.g. a UI component: + +```tsx +import { useUnit } from 'effector-react'; + +function UserInfoForm() { + const { name, nameUpdated } = useUnit({ + name: $userName, + nameUpdated: updateName, + }); + + return ( + + { + nameUpdated(e.currentTarget.value); + }} + /> + + ); +} +``` + +You can find [API reference of UI-framework integrations in the Effector's documentation](https://effector.dev/en/api/). + +#### Testing + +Now that we have the Effector API for the old code, we can write some tests for it, so that the behavior of the Redux code will be captured, and we won't break anything when porting the feature implementation to Effector. + +:::tip +Notice, that we also need to create mock version of the Redux Store, so this test is independent of any other. + +Testable version of the Redux Store should also properly mock any thunks or custom middlewares, which are used in the test. +::: + +```ts +import { configureStore } from '@reduxjs/tookit'; + +import { $userName, updateName } from 'root/features/user-info'; +import { reduxInterop } from 'root/redux-store'; +import { appStarted } from 'root/shared/app-lifecycle'; + +test('username is updated', async () => { + const mockStore = configureStore({ + // ... + }); + + const scope = fork({ + values: [ + // Providing mock version of the redux store + [reduxInterop.$reduxStore, mockStore], + ], + }); + + await allSettled(appStarted, { scope }); + + expect(scope.getState($userName)).toBe(''); + + await allSettled(updateName, { scope, params: 'John' }); + + expect(scope.getState($userName)).toBe('John'); +}); +``` + +Such tests will allow us to notice any changes in logic early on. + +:::info +You can find more details about Effector-way testing [in the "Writing tests" guide in the documentation](https://effector.dev/en/guides/testing/). +::: + +#### Gradual rewrite + +We can now extend this model with new logic or carry over existing logic from Redux, while keeping public API of Effector units. + +```ts +// src/features/user-info/model.ts +export const $userName = combine( + reduxInterop.$state, + (state) => state.userInfo.name ?? '' +); +export const updateName = createEvent(); + +sample({ + clock: updateName, + filter: (name) => name.length <= 20, + target: [ + reduxInterop.dispatch.prepend((name: string) => + userInfoSlice.updateName(name) + ), + ], +}); +``` + +☝️ Effector's model for the feature is extended with new logic (name can't be longer than 20 characters), but the public API of `$userName` store and `updateName` event is unchanged and state of the username is still lives inside Redux. + +#### Moving the state + +Eventually you should end up with a situation where: + +1. The state of the feature is still stored in Redux +2. But all related logic and side effects are now managed by the Effector +3. and all external consumers (UI-components, other features, etc.) interact with the feature through its Effector-model. + +After that you can safely move the state into the model and get rid of Redux-reducer for it: + +```ts +// src/features/user-info/model.ts +export const $userName = createStore(''); +export const updateName = createEvent(); + +sample({ + clock: updateName, + filter: (name) => name.length <= 20, + target: $userName, +}); +``` + +☝️ Feature is completely ported to Effector, `reduxInterop` is not used here anymore. + +##### Edge-case + +If there is still code that consumes this state via the Redux Store selector, and there is currently no way to move that consumer to use the Effector model, it is still possible to "sync" the state back into Redux as a read-only mirror of the Effector model state: + +```ts +// src/features/user-info/model.ts + +// ...main code + +// sync state back to Redux +sample({ + clock: $userName, + target: [ + reduxInterop.dispatch.prepend((name: string) => + userInfoSlice.syncNameFromEffector(name) + ), + ], +}); +``` + +☝️ But it's important to make sure that this is a read-only mirror that won't be changed in Redux in any other way - because then there would be two parallel versions of this state, which would probably lead to nasty bugs. + +## New feature + +Adding a new feature on Effector to a Redux project is not much different from the initial step of migrating an existing feature: + +1. Any new code is written in Effector +2. Any dependencies to Redux Store should work through `reduxInterop` API + +## Special cases + +### Middleware with side effects + +Sometimes Redux actions are not changing state, but trigger side effects via middlewares. + +Suppose Redux Store has middleware that reacts to action like `{ type: SEND_ANALYTICS_EVENT, payload }` and sends the event to our analytics. + +Sending analytics is usually involved in almost all code of the application and migration of such a feature will be much more complicated. + +In this case, the recommended upgrade path is as follows: + +#### Mirror of the action + +First, create a mirror Effector's event of the `SEND_ANALYTICS_EVENT` action by using its action-creator: + +```ts +// src/shared/analytics/model.ts +import { reduxInterop } from 'root/redux-store'; +import { sendAnalyticsEventAction } from './actions'; + +export const sendAnalytics = reduxInterop.dispatch.prepend((payload) => + sendAnalyticsEventAction(payload) +); +``` + +#### Move to event instead of an action + +As a second step, gradually change all dispatches of this action to an event call. + +E.g. instead of + +```ts +import { sendAnalyticsEventAction } from 'root/analytics'; + +dispatch(sendAnalyticsEventAction(payload)); +``` + +do + +```ts +import { sendAnalytics } from 'root/analytics'; + +sendAnalytics(payload); +``` + +It is safe to do, because the `sendAnalytics(payload)` call here is a full equivalent of the `dispatch(sendAnalyticsEventAction(payload))` and can be used instead of it - the action will still be dispatched by the `reduxInterop.dispatch` under the hood. + +In the end Redux, Effector and your UI-framework should all use this event instead of dispatching the action. + +#### Move the implementation + +Since now all analytics is sent via this event, it is now possible to fully move from the analytics middleware to Effector's model: + +```ts +// src/shared/analytics/model.ts +import { createEvent, createEffect, sample } from 'effector'; +import { sendEvent } from 'root/shared/analytics-client'; + +export const sendAnalytics = createEvent(); + +const sendEventFx = createEffect(sendEvent); + +sample({ + clock: sendAnalytics, + target: sendEventFx, +}); +``` + +### Redux Thunks + +Redux Thunks are a standard approach for writing asynchronous logic in Redux apps, and are commonly used for data fetching, so your app is probably already have a bunch of thunks, which should also be migrated at some point. + +The closest equivalent to Thunk in Effector is an [Effect](https://effector.dev/en/api/effector/effect/), which is a container for any function, which produces side effects (like fetching the data from remote source) - so Thunks should be converted to Effects. + +#### Create an Effect representation for a Thunk + +You can convert any Thunk to Effect by using Effector's [`attach` operator](https://effector.dev/en/api/effector/attach/) and wrapping a `reduxInterop.dispatch` with it. + +```ts +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { attach } from 'effector'; + +import { reduxInterop } from 'root/redux-store'; + +const someThunk = createAsyncThunk( + 'some/thunk', + async (p: number, thunkApi) => { + // thunk code + } +); + +/** + * This is a redux-thunk, converted into an effector Effect. + * + * This allows gradual migration from redux-thunks to effector Effects + */ +const someFx = attach({ + mapParams: (p: number) => someThunk(p), + effect: interop.dispatch, +}); +``` + +Now you can use it in any new code with Effector: + +```ts +sample({ + clock: doSomeButtonClicked, + target: someFx, +}); +``` + +:::info +Adding of `Fx` postfix for Effects is an Effector's naming convention, just like adding `$` to the store names. + +It is described in details in [the "Naming convention" article in the docs](https://effector.dev/en/conventions/naming/). +::: + +#### Use this Effect instead of original Thunk + +Created Effect can be safely used anywhere, where you would use the original thunk - this will allow to simply swap Effect's implementation from Thunk usage later. + +##### UI Component + +```tsx +const doSome = useUnit(someThunkFx); + +return ; +``` + +##### Other Thunk + +```ts +const makeASandwichWithSecretSauce = (clientName) = async (dispatch) => { + try { + const result = await sandwichApi.getSandwichFor(clientName) + + dispatch(sandwichSlice.ready(result)) + } catch(error) { + dispatch(sandwichSlice.failed(error)) + } +}; + +const makeASandwichFx = attach({ + mapParams(client) { + return makeASandwichWithSecretSauce(client) + }, + effect: reduxInterop.dispatch, +}) + +function makeSandwichesForEverybody() { + return function (dispatch, getState) { + if (!getState().sandwiches.isShopOpen) { + return Promise.resolve(); + } + + return dispatch(makeASandwichWithSecretSauce('My Grandma')) + .then(() => + Promise.all([ + makeASandwichFx('Me')), + // ☝️ Notice, that this Effect is intertwined with the Thunk flow + dispatch(makeANormalSandwich('My wife')), + ]) + ) + }; +} +``` + +### Swap Effect's implementation + +After this Effect is used everywhere instead of a Thunk you can safely swap implementation: + +```ts +// If Thunk was dispatching some actions internally, you can also preserve this logic in Effector's model +// and then migrate for it by following "Migrating existing feature" part of this guide +const sandwichReady = reduxInterop.dispatch.prepend((result) => + sandwichSlice.ready(result) +); +const sandiwchFailed = reduxInterop.dispatch.prepend((error) => + sandwichSlice.fail(error) +); + +const makeASandwichFx = createEffect((clientName) => + sandwichApi.getSandwichFor(clientName) +); + +sample({ + clock: makeASandwichFx.doneData, + target: sandwichReady, +}); + +sample({ + clock: makeASandwichFx.failData, + target: [ + sandwichFailed, + reportErrorToSentry, + // ... + ], +}); +``` + +That's it, Thunk is now Effect! + +### Redux Sagas + +Redux-Saga is a side effect management library for Redux. +Coincidentally, side effect management is also the main focus of Effector, so to migrate you will need to simply rewrite your sagas to Effector's concepts. + +Thanks to `@withease/redux` you can do it partially and in any order. Here are few examples of the Saga code ported to Effector. + +:::tip +These examples show the ported code, but the use of Redux actions and states is left as is, since other sagas (and any middlewares in general) may depend on them. + +See the "Migrating Existing Functions" part of this guide for how to migrate from dispatchers and selectors to events and stores completely. +::: + +#### Data fetching + +::: code-group + +```ts [saga] +function* fetchPosts() { + yield put(actions.requestPosts()); + const page = yield select((state) => state.currentPage); + const products = yield call(fetchApi, '/products', page); + yield put(actions.receivePosts(products)); +} + +function* watchFetch() { + while (yield take('FETCH_POSTS')) { + yield call(fetchPosts); // waits for the fetchPosts task to terminate + } +} +``` + +```ts [effector + @withease/redux] +const $page = combine(reduxInterop.$state, (state) => state.currentPage); +const postsRequested = reduxInterop.dispatch.prepend(actions.requestPosts); +const postsReceived = reduxInterop.dispatch.prepend(actions.receivePosts); +// This event should be used to dispatch this action in place of original dispatch +// See "Middleware with side-effects" part of this guide for explanation +const fetchPosts = reduxInterop.dispatch.prepend(() => ({ + type: 'FETCH_POSTS', +})); + +const fetchProductsByPageFx = createEffect((page) => + fetchApi('/products', page) +); + +// this sample describes the key part of the saga's logic +sample({ + clock: postsRequested, + source: $page, + target: fetchProductsByPageFx, +}); + +// Notice, that these two `sample`s here are used only to preserve actions dispatching, +// as there is might be other redux code depending on them +sample({ + clock: fetchPosts, + target: postsRequested, +}); + +sample({ + clock: fetchProductsByPageFx.doneData, + target: postsReceived, +}); +``` + +::: + +#### Throttle, delay and debounce + +:::tip +You can implement debounce, delay and throttle logic in Effector by yourself. + +But since those are common patterns, **it is recommended** to use [Patronum - the official utility library for Effector](https://patronum.effector.dev/methods/). +::: + +::: code-group + +```ts [saga] +import { throttle, debounce, delay } from 'redux-saga/effects'; + +function* handleInput(input) { + // ... +} + +function* throttleInput() { + yield throttle(500, 'INPUT_CHANGED', handleInput); +} + +function* debounceInput() { + yield debounce(1000, 'INPUT_CHANGED', handleInput); +} + +function* delayInput() { + yield take('INPUT_CHANGED'); + yield delay(5000); +} +``` + +```ts [effector + @withease/redux] +import { debounce, delay, throttle } from 'patronum'; +import { createEffect, createEvent, sample } from 'effector'; + +const inputChanged = createEvent(); +const handleInputChangeFx = createEffect((input) => { + // ... +}); + +sample({ + clock: [ + throttle({ + source: inputChanged, + timeout: 500, + }), + debounce({ + source: inputChanged, + timeout: 1000, + }), + delay({ + source: inputChanged, + timeout: 5000, + }), + ], + target: handleInputChangeFx, +}); +``` + +::: + +#### Background task + +::: code-group + +```ts [saga] +function* bgSync() { + try { + while (true) { + yield put(actions.requestStart()); + const result = yield call(someApi); + yield put(actions.requestSuccess(result)); + yield delay(5000); + } + } finally { + if (yield cancelled()) yield put(actions.requestFailure('Sync cancelled!')); + } +} + +function* main() { + while (yield take('START_BACKGROUND_SYNC')) { + // starts the task in the background + const bgSyncTask = yield fork(bgSync); + + // wait for the user stop action + yield take('STOP_BACKGROUND_SYNC'); + // user clicked stop. cancel the background task + // this will cause the forked bgSync task to jump into its finally block + yield cancel(bgSyncTask); + } +} +``` + +```ts [effector + @withease/redux] +import { createStore, sample, createEffect } from 'effector'; +import { delay } from 'patronum'; + +import { reduxInterop } from 'root/redux-store'; + +const startRequested = reduxInterop.dispatch.prepend(actions.requestStart); +const requestSuccess = reduxInterop.dispatch.prepend(actions.requestSuccess); + +export const backgroundSyncStarted = reduxInterop.dispatch.prepend( + actions.startBackgroundSync +); +export const backgroundSyncStopped = reduxInterop.dispatch.prepend( + actions.stopBackgroundSync +); + +const $needSync = createStore(false) + .on(backgroundSyncStarted, () => true) + .on(backgroundSyncStopped, () => false); +const someApiFx = createEffect(someApi); + +// This sample will run someApiFx in cycle with 5 second delays, +// until background sync is stopped +sample({ + clock: [ + backgroundSyncStarted, + delay({ + source: someApiFx.done, + timeout: 5_000, + }), + ], + filter: $needSync, + target: [ + // Dispatching original action for compatibility + // with the rest of the project + startRequested, + // Calling the API + someApiFx, + ], +}); + +// Dispatching original action for compatibility +// with the rest of the project +sample({ + clock: someApiFx.doneData, + target: requestSuccess, +}); +``` + +::: + +#### Partial Saga migration + +Previous examples shown the full rewrite of sagas, but it is not necessary. +You can move parts of the logic from any saga step-by-step, without rewriting the whole thing: + +1. To call an Effector's Event or Effect from Saga you can use a `call` operator, like `yield call(effectorEvent, argument)`. +2. To read state of the Effector's Store in the Saga you can also use `call` + `getState()` method of a store, like this: `yield call(() => $someStore.getState())`. + +:::warning +Note that it is generally **not recommended** calling the `getState` method of Effector Stores, because it is imperative and non-reactive. This method is an escape-hatch for cases where there is no other way. + +But you can sometimes use it in Sagas, because they are imperative and non-reactive themselves, and you're not always going to have the option to rewrite it to Effector right away. +::: + +Here is an earlier "Data fetching" example, but in a state of partial rewrite. + +```ts +// effector model +const $page = combine(reduxInterop.$state, (state) => state.currentPage); + +const postsRequested = reduxInterop.dispatch.prepend(actions.requestPosts); +const postsReceived = reduxInterop.dispatch.prepend(actions.receivePosts); + +export const fetchPosts = reduxInterop.dispatch.prepend(() => ({ + type: 'FETCH_POSTS', +})); + +const fetchProductsByPageFx = attach({ + source: $page, + effect(page, filter) { + return fetchApi('/products', page, filter); + }, +}); + +// saga +import { $filters } from 'root/features/filters'; + +import { postsRequested, postsReceived, fetchProductsByPageFx } from './model'; + +function* fetchPosts() { + yield call(postsRequested); + const filters = yield call(() => $filters.getState()); + const products = yield call(fetchProductsByPageFx); + yield call(postsReceived, products); +} + +function* watchFetch() { + while (yield take('FETCH_POSTS')) { + yield call(fetchPosts); // waits for the fetchPosts task to terminate + } +} +``` + +☝️ Notice how `yield call(effectorEvent, argument)` is used instead of `yield put(action)` here. It allows to both call Effector's event (to use it in Effector-based code) and dispatch an action (to use it in Redux-based code). + +## Summary + +To perform a gradual, non-blocking code migration from Redux to Effector you will need to: + +1. Install `@withease/redux` helpers package. +2. Convert a single feature to Effector, so you and your colleagues are able to evaluate if it fits you. +3. Rewrite Redux code to Effector, by converting entities of the former to their counterparts of the latter. You can do it gradually over the course of months and years, without stopping feature development of your product. +4. Remove `@withease/redux`, once there is no more Redux code left. diff --git a/apps/website/docs/redux/index.md b/apps/website/docs/redux/index.md new file mode 100644 index 00000000..f69166a2 --- /dev/null +++ b/apps/website/docs/redux/index.md @@ -0,0 +1,190 @@ +--- +outline: [2, 3] +--- + +# @withease/redux + +Minimalistic package to allow simpler migration from Redux to Effector. +Also, can handle any other use case, where one needs to communicate with Redux Store from Effector's code. + +:::info +This is an API reference article, for the Redux -> Effector migration guide [see the "Migrating from Redux to Effector" article](/magazine/migration_from_redux). +::: + +## Installation + +First, you need to install package: + +::: code-group + +```sh [pnpm] +pnpm install @withease/redux +``` + +```sh [yarn] +yarn add @withease/redux +``` + +```sh [npm] +npm install @withease/redux +``` + +::: + +## API + +### `createReduxIntegration` + +Effector <-> Redux interoperability works through special "interop" object, which provides Effector-compatible API to Redux Store. + +```ts +const myReduxStore = configureStore({ + // ... +}); + +const reduxInterop = createReduxIntegration({ + reduxStore: myReduxStore, + setup: appStarted, +}); +``` + +Explicit `setup` event is required to initialize the interoperability. Usually it would be an `appStarted` event or any other "app's lifecycle" event. + +You can read more about this practice [in the "Explicit start of the app" article](/magazine/explicit_start). + +### Interoperability object + +Redux Interoperability object provides few useful APIs. + +#### `reduxInterop.$state` + +This is an Effector's Store, which contains **the state** of the provided instance of Redux Store. + +It is useful, as it allows to represent any part of Redux state as an Effector store. + +```ts +import { combine } from 'effector'; + +const $user = combine(reduxInterop.$state, (x) => x.user); +``` + +:::tip +Notice, that `reduxInterop.$state` store will use Redux Store typings, if those are provided. So it is recommended to properly type your Redux Store. +::: + +#### `reduxInterop.dispatch` + +This is an Effector's Effect, which calls Redux Store's `dispatch` method under the hood. +Since it is a normal [Effect](https://effector.dev/en/api/effector/effect) - it supports all methods of `Effect` type. + +:::tip +It is recommended to create separate events for each specific action via `.prepend` method of `Effect`. +::: + +```ts +const updateUserName = reduxInterop.dispatch.prepend((name: string) => + userSlice.changeName(name) +); + +sample({ + clock: saveButtonClicked, + source: $nextName, + target: updateUserName, +}); +``` + +It is also possible to convert a Redux Thunk to `Effect` by using Effector's [`attach` operator](https://effector.dev/en/api/effector/attach/). + +```ts +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { attach } from 'effector'; + +const someThunk = createAsyncThunk( + 'some/thunk', + async (p: number, { dispatch }) => { + await new Promise((resolve) => setTimeout(resolve, p)); + + return dispatch(someSlice.actions.doSomething()); + } +); + +/** + * This is a redux-thunk, converted into an effector Effect. + * + * This allows gradual migration from redux-thunks to effector Effects + */ +const someThunkFx = attach({ + mapParams: (p: number) => someThunk(p), + effect: interop.dispatch, +}); + +const promise = someThunkFx(42); +// ☝️ `someThunk` will be dispatched under the hood +// `someThunkFx` will return an Promise, which will be resolved once someThunk is resolved +``` + +#### `reduxInterop.$reduxStore` + +This is an Effector's Store, which contains provided instance of Redux Store. + +It is useful, since it makes possible to use [Effector's Fork API to write tests](https://effector.dev/en/guides/testing/) for the logic, contained in the Redux Store! + +So even if the logic is mixed between the two like this: + +```ts +// app code +const myReduxStore = configureStore({ + // ... +}); + +const reduxInterop = createReduxIntegration({ + reduxStore: myReduxStore, + setup: appStarted, +}); + +// user model +const $user = combine(reduxInterop.$state, (x) => x.user); + +const updateUserName = reduxInterop.dispatch.prepend((name: string) => + userSlice.changeName(name) +); + +sample({ + clock: saveButtonClicked, + source: $nextName, + target: updateUserName, +}); +``` + +It is still possible to write a proper test like this: + +```ts +test('username updated after save button click', async () => { + const mockStore = configureStore({ + // ... + }); + + const scope = fork({ + values: [ + // Providing mock version of the redux store + [reduxInterop.$reduxStore, mockStore], + // Mocking anything else, if needed + [$nextName, 'updated'], + ], + }); + + await allSettled(appStarted, { scope }); + + expect(scope.getState($userName)).toBe('initial'); + + await allSettled(saveButtonClicked, { scope }); + + expect(scope.getState($userName)).toBe('updated'); +}); +``` + +☝️ This test will be especially useful in the future, when this part of logic will be ported to Effector. + +:::tip +Notice, that it is recommended to create a mock version of Redux Store for any tests like this, since the Store contains state, which could leak between the tests. +::: diff --git a/package.json b/package.json index e23ce575..5a982de4 100644 --- a/package.json +++ b/package.json @@ -26,15 +26,19 @@ "@nrwl/rollup": "16.5.x", "@nx/devkit": "16.5.x", "@nx/eslint-plugin": "16.5.x", - "@nx/js": "16.5.x", + "@nx/js": "16.5.5", "@nx/linter": "16.5.x", - "@nx/vite": "16.5.x", + "@nx/rollup": "16.5.5", + "@nx/vite": "16.5.5", + "@nx/web": "16.5.x", "@nx/workspace": "16.5.x", "@playwright/test": "^1.32.2", + "@reduxjs/toolkit": "^2.0.1", "@size-limit/file": "^7.0.8", "@types/node": "18.7.14", "@typescript-eslint/eslint-plugin": "5.62.0", "@typescript-eslint/parser": "5.62.0", + "@vitest/coverage-c8": "~0.32.0", "@vitest/ui": "0.34.4", "bytes-iec": "^3.1.1", "effector": "23.0.0", @@ -45,6 +49,8 @@ "nx": "16.5.x", "playwright": "^1.32.2", "prettier": "^2.6.2", + "redux": "^5.0.0", + "redux-saga": "^1.2.3", "rollup": "^3.4.0", "rollup-plugin-dts": "^5.0.0", "runtypes": "^6.7.0", @@ -55,8 +61,7 @@ "vite-tsconfig-paths": "4.2.1", "vitepress": "1.0.0-rc.31", "vitest": "0.34.4", - "vue": "3.3.4", - "@nx/web": "16.5.x" + "vue": "3.3.4" }, "dependencies": { "@algolia/client-search": "^4.14.3", diff --git a/packages/redux/.babelrc b/packages/redux/.babelrc new file mode 100644 index 00000000..678c6ba4 --- /dev/null +++ b/packages/redux/.babelrc @@ -0,0 +1,10 @@ +{ + "presets": [ + [ + "@nx/js/babel", + { + "useBuiltIns": false + } + ] + ] +} diff --git a/packages/redux/.eslintrc.json b/packages/redux/.eslintrc.json new file mode 100644 index 00000000..9d9c0db5 --- /dev/null +++ b/packages/redux/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/packages/redux/README.md b/packages/redux/README.md new file mode 100644 index 00000000..3e248986 --- /dev/null +++ b/packages/redux/README.md @@ -0,0 +1,11 @@ +# redux + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build redux` to build the library. + +## Running unit tests + +Run `nx test redux` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/packages/redux/package.json b/packages/redux/package.json new file mode 100644 index 00000000..9b176b13 --- /dev/null +++ b/packages/redux/package.json @@ -0,0 +1,9 @@ +{ + "name": "@withease/redux", + "version": "0.0.1", + "type": "commonjs", + "peerDependencies": { + "effector": "^22.8.8 || ^23.0.0", + "redux": "^4.0.0 || ^5.0.0" + } +} diff --git a/packages/redux/project.json b/packages/redux/project.json new file mode 100644 index 00000000..e1650c29 --- /dev/null +++ b/packages/redux/project.json @@ -0,0 +1,67 @@ +{ + "name": "redux", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/redux/src", + "projectType": "library", + "targets": { + "pack": { + "executor": "nx:run-commands", + "options": { + "command": "node tools/scripts/typepack.mjs --package redux" + }, + "dependsOn": [ + { + "target": "build" + } + ] + }, + "build": { + "executor": "@nrwl/rollup:rollup", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/packages/redux", + "entryFile": "packages/redux/src/index.ts", + "tsConfig": "packages/redux/tsconfig.lib.json", + "project": "packages/redux/package.json", + "format": ["esm", "cjs"], + "generateExportsField": true, + "compiler": "babel" + } + }, + "publish": { + "executor": "nx:run-commands", + "options": { + "command": "node tools/scripts/publish.mjs redux" + }, + "dependsOn": [ + { + "target": "pack" + } + ] + }, + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["packages/redux/**/*.ts"] + } + }, + "test": { + "executor": "@nx/vite:test", + "outputs": ["coverage/packages/redux"] + }, + "size": { + "executor": "./tools/executors/size-limit:size-limit", + "options": { + "limit": "1 kB", + "outputPath": "dist/packages/redux" + }, + "dependsOn": [ + { + "target": "build" + } + ] + } + }, + "tags": [] +} diff --git a/packages/redux/src/index.ts b/packages/redux/src/index.ts new file mode 100644 index 00000000..ddbd84bd --- /dev/null +++ b/packages/redux/src/index.ts @@ -0,0 +1 @@ +export * from './lib/redux'; diff --git a/packages/redux/src/lib/redux.spec.ts b/packages/redux/src/lib/redux.spec.ts new file mode 100644 index 00000000..5855454f --- /dev/null +++ b/packages/redux/src/lib/redux.spec.ts @@ -0,0 +1,536 @@ +import { createReduxIntegration } from './redux'; +import { legacy_createStore } from 'redux'; +import { + configureStore, + createSlice, + createAsyncThunk, +} from '@reduxjs/toolkit'; +import { call, take, put } from 'redux-saga/effects'; +import createSagaMiddleware from 'redux-saga'; +import { + createEvent, + fork, + allSettled, + createStore, + sample, + attach, + combine, +} from 'effector'; + +describe('@withease/redux', () => { + test('Should throw if setup is not an effector unit', () => { + const reduxStore = legacy_createStore(() => ({}), {}); + const setup = () => { + // ok + }; + + expect(() => + // @ts-expect-error - setup is not an effector unit + createReduxIntegration({ reduxStore, setup }) + ).toThrowErrorMatchingInlineSnapshot('"setup must be an effector unit"'); + }); + + test('Should throw if reduxStore is not a Redux store', () => { + const reduxStore = {}; + const setup = createEvent(); + + expect(() => + // @ts-expect-error - reduxStore is not a Redux store + createReduxIntegration({ reduxStore, setup }) + ).toThrowErrorMatchingInlineSnapshot( + '"reduxStore must be provided and should be a Redux store"' + ); + }); + + test('Any errors are logged into console', () => { + const fakeStore = { + dispatch: () => { + throw new Error('fake dispatch!'); + }, + getState: () => { + return {}; + }, + subscribe: () => { + throw new Error('fake subscribe!'); + }, + }; + + const setup = createEvent(); + + const spy = vi.spyOn(console, 'error').mockImplementation(() => { + // ok + }); + + // @ts-expect-error - fakeStore is not a Redux store + const int = createReduxIntegration({ reduxStore: fakeStore, setup }); + + expect(spy.mock.calls.map((x) => x[0])).toMatchInlineSnapshot('[]'); + + setup(); + + expect(spy.mock.calls.map((x) => x[0])).toMatchInlineSnapshot(` + [ + [Error: fake subscribe!], + ] + `); + + int.dispatch({ type: 'kek' }); + + expect(spy.mock.calls.map((x) => x[0])).toMatchInlineSnapshot(` + [ + [Error: fake subscribe!], + [Error: fake dispatch!], + ] + `); + + spy.mockRestore(); + }); + + describe('raw Redux', () => { + test('Should take redux store in', () => { + const reduxStore = legacy_createStore(() => ({}), {}); + const setup = createEvent(); + const interop = createReduxIntegration({ reduxStore, setup }); + setup(); + + expect(interop.$reduxStore.getState()).toBe(reduxStore); + }); + + test('Should allow dispatching actions', () => { + const reduxStore = legacy_createStore((_, x) => x, {}); + const setup = createEvent(); + const interop = createReduxIntegration({ reduxStore, setup }); + setup(); + + const action = { type: 'test' }; + interop.dispatch(action); + + expect(reduxStore.getState()).toEqual(action); + }); + + test('Should allow reading state', () => { + const reduxStore = legacy_createStore< + { value: string }, + { type: string; value: string } + >((_, x) => ({ + value: x.value || 'kek', + })); + const setup = createEvent(); + const interop = createReduxIntegration({ reduxStore, setup }); + setup(); + + const $state = combine(interop.$state, (x) => x.value); + + expect($state.getState()).toEqual('kek'); + + reduxStore.dispatch({ type: 'test', value: 'lol' }); + + expect($state.getState()).toEqual('lol'); + }); + + test('Should work with Fork API', async () => { + const reduxStore = legacy_createStore< + { value: string }, + { type: string; value: string } + >(() => ({ value: '' }), { value: '' }); + const setup = createEvent(); + const interop = createReduxIntegration({ reduxStore, setup }); + + const $state = combine(interop.$state, (x) => x.value); + + const mockStore = legacy_createStore< + { value: string }, + { type: string; value: string } + >((_, x) => ({ + value: x.value || 'kek', + })); + + const scope = fork({ + values: [[interop.$reduxStore, mockStore]], + }); + + await allSettled(setup, { scope }); + + expect(scope.getState($state)).toEqual('kek'); + + await allSettled(interop.dispatch, { + scope, + params: { type: 'test', value: 'lol' }, + }); + + expect(scope.getState($state)).toEqual('lol'); + }); + + test('edge case: should allow synchronous cycle update', async () => { + /** + * This is an edge case, where we have a cycle between effector and redux, + * it is useful for cases, when a feature is not entierly migrated to effector, + * so it is still needed to keep redux and effector parts in sync. + */ + + const reduxStore = legacy_createStore< + { c: number }, + { type: string; c: number } + >((s, a) => ({ c: (s || { c: 0 }).c + (a.c || 0) }), { c: 0 }); + + const setup = createEvent(); + const interop = createReduxIntegration({ + reduxStore, + setup, + }); + + const updateCount = createEvent(); + const $count = createStore(0).on(updateCount, (s, a) => s + a); + + const $reduxCount = combine(interop.$state, (x) => x.c); + + // effector updates redux + const updateReduxCount = interop.dispatch.prepend((x: number) => ({ + type: 'test', + c: x, + })); + sample({ + clock: $count, + source: $reduxCount, + filter: (r, e) => r !== e, + fn: (_r, e) => e, + target: updateReduxCount, + }); + + // redux updates effector - cycle + sample({ + clock: $reduxCount, + source: $count, + filter: (r, e) => r !== e, + fn: (_s, reduxCount) => reduxCount, + target: $count, + }); + + const scope = fork(); + + expect(scope.getState($count)).toEqual(0); + expect(scope.getState($reduxCount)).toEqual(0); + + await allSettled(setup, { scope }); + + await allSettled(updateCount, { + scope, + params: 1, + }); + + expect(scope.getState($count)).toEqual(1); + expect(scope.getState($reduxCount)).toEqual(1); + + await allSettled(updateReduxCount, { + scope, + params: 1, + }); + + expect(scope.getState($count)).toEqual(2); + expect(scope.getState($reduxCount)).toEqual(2); + }); + }); + + describe('Redux Toolkit', () => { + test('Should work with basic Redux Toolkit', () => { + const testSlice = createSlice({ + name: 'test', + initialState: 'kek', + reducers: { + test: () => 'lol', + }, + }); + const reduxStore = configureStore({ + reducer: { + test: testSlice.reducer, + }, + }); + const setup = createEvent(); + const interop = createReduxIntegration({ reduxStore, setup }); + const $test = combine(interop.$state, (x) => x.test); + setup(); + + expect(interop.$reduxStore.getState()).toBe(reduxStore); + + expect($test.getState()).toEqual('kek'); + + interop.dispatch(testSlice.actions.test()); + + expect($test.getState()).toEqual('lol'); + }); + + test('Should work with Fork API', async () => { + const reduxStore = legacy_createStore<{ test: string }, { type: string }>( + () => ({ test: '' }), + { test: '' } + ); + + const testSlice = createSlice({ + name: 'test', + initialState: 'kek', + reducers: { + test: () => 'lol', + }, + }); + const mockStore = configureStore({ + reducer: { + test: testSlice.reducer, + }, + }); + const setup = createEvent(); + const interop = createReduxIntegration({ reduxStore, setup }); + const $test = combine(interop.$state, (x) => x.test); + + const scope = fork({ + values: [[interop.$reduxStore, mockStore]], + }); + + await allSettled(setup, { scope }); + + expect(scope.getState(interop.$reduxStore)).toBe(mockStore); + + expect(scope.getState($test)).toEqual('kek'); + expect($test.getState()).toEqual(''); + + await allSettled(interop.dispatch, { + scope, + params: testSlice.actions.test(), + }); + + expect(scope.getState($test)).toEqual('lol'); + expect($test.getState()).toEqual(''); + }); + + test('Should support redux-thunks', async () => { + const testSlice = createSlice({ + name: 'test', + initialState: 'kek', + reducers: { + test: () => 'lol', + }, + }); + const reduxStore = configureStore({ + reducer: { + test: testSlice.reducer, + }, + }); + const setup = createEvent(); + const interop = createReduxIntegration({ reduxStore, setup }); + const $test = combine(interop.$state, (x) => x.test); + + const lolThunk = (p: number) => async (dispatch: any) => { + await new Promise((resolve) => setTimeout(resolve, p)); + + return dispatch(testSlice.actions.test()); + }; + + /** + * This is a redux-thunk, converted into an effector Effect. + * + * This allows gradual migration from redux-thunks to effector Effects + */ + const lolThunkFx = attach({ + mapParams: (p: number) => lolThunk(p), + effect: interop.dispatch, + }); + + const scope = fork({ + values: [ + [ + interop.$reduxStore, + // Independent copy of original store + configureStore({ + reducer: { + test: testSlice.reducer, + }, + }), + ], + ], + }); + + expect(scope.getState($test)).toEqual('kek'); + expect($test.getState()).toEqual('kek'); // non-scope state + + await allSettled(setup, { scope }); + + await allSettled(lolThunkFx, { + scope, + params: 100, + }); + + expect(scope.getState($test)).toEqual('lol'); + expect($test.getState()).toEqual('kek'); // non-scope state should not have changed + }); + + test('Should support RTK Async Thunks', async () => { + const testSlice = createSlice({ + name: 'test', + initialState: 'kek', + reducers: { + test: () => 'lol', + }, + }); + const reduxStore = configureStore({ + reducer: { + test: testSlice.reducer, + }, + }); + const setup = createEvent(); + const interop = createReduxIntegration({ reduxStore, setup }); + const $test = combine(interop.$state, (x) => x.test); + + const lolThunk = createAsyncThunk( + 'test/lol', + async (p: number, { dispatch }) => { + await new Promise((resolve) => setTimeout(resolve, p)); + + return dispatch(testSlice.actions.test()); + } + ); + + /** + * This is a redux-thunk, converted into an effector Effect. + * + * This allows gradual migration from redux-thunks to effector Effects + */ + const lolThunkFx = attach({ + // thunk is not explicitly a redux action + mapParams: (p: number) => lolThunk(p), + effect: interop.dispatch, + }); + + const scope = fork({ + values: [ + [ + interop.$reduxStore, + // Independent copy of original store + configureStore({ + reducer: { + test: testSlice.reducer, + }, + }), + ], + ], + }); + + expect(scope.getState($test)).toEqual('kek'); + expect($test.getState()).toEqual('kek'); // non-scope state + + await allSettled(setup, { scope }); + + await allSettled(lolThunkFx, { + scope, + params: 100, + }); + + expect(scope.getState($test)).toEqual('lol'); + expect($test.getState()).toEqual('kek'); // non-scope state should not have changed + }); + + describe('Redux Sagas', () => { + test('Should allow calling events from sagas', async () => { + const testEvent = createEvent(); + const $test = createStore('kek').on(testEvent, (_, p) => p); + + const testSlice = createSlice({ + name: 'test', + initialState: 'test', + reducers: { + test: () => 'test', + }, + }); + + function* lolSaga() { + yield take(testSlice.actions.test.type); + yield call(testEvent, 'lol'); + } + + const sagaMiddleware = createSagaMiddleware(); + + const store = configureStore({ + reducer: { + test: testSlice.reducer, + }, + // @ts-expect-error - sagaMiddleware type is not compatible here for some reason :shrug: + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().concat(sagaMiddleware), + }); + + sagaMiddleware.run(lolSaga); + + const setup = createEvent(); + const interop = createReduxIntegration({ + reduxStore: store, + setup, + }); + + const doTest = interop.dispatch.prepend(testSlice.actions.test); + + const scope = fork(); + + await allSettled(setup, { scope }); + + expect(scope.getState($test)).toEqual('kek'); + + await allSettled(doTest, { scope }); + + expect(scope.getState($test)).toEqual('lol'); + }); + + test('Should allow reading stores from sagas', async () => { + const $someStore = createStore('kek'); + + const testSlice = createSlice({ + name: 'test', + initialState: 'kek', + reducers: { + test: () => 'test', + change: (_, a) => a.payload, + }, + }); + + const sagaMiddleware = createSagaMiddleware(); + + const store = configureStore({ + reducer: { + test: testSlice.reducer, + }, + // @ts-expect-error - sagaMiddleware type is not compatible here for some reason :shrug: + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().concat(sagaMiddleware), + }); + + const setup = createEvent(); + const interop = createReduxIntegration({ + reduxStore: store, + setup, + }); + + const $test = combine(interop.$state, (x) => x.test); + + function* lolSaga() { + yield take(testSlice.actions.test.type); + // @ts-expect-error - typescript having a hard time with generators + const result = yield call(() => $someStore.getState()); + yield put(testSlice.actions.change(result)); + } + + sagaMiddleware.run(lolSaga); + + const doTest = interop.dispatch.prepend(testSlice.actions.test); + + const scope = fork({ + values: [[$someStore, 'lol']], + }); + + await allSettled(setup, { scope }); + + expect(scope.getState($test)).toEqual('kek'); + + await allSettled(doTest, { scope }); + + expect(scope.getState($test)).toEqual('lol'); + }); + }); + }); +}); diff --git a/packages/redux/src/lib/redux.ts b/packages/redux/src/lib/redux.ts new file mode 100644 index 00000000..6c337822 --- /dev/null +++ b/packages/redux/src/lib/redux.ts @@ -0,0 +1,137 @@ +import type { Unit, Store, Effect } from 'effector'; +import type { Store as ReduxStore, Action } from 'redux'; +import { + createStore, + createEvent, + is, + sample, + attach, + scopeBind, +} from 'effector'; + +/** + * Type for any thunk-like thing, which can be dispatched to Redux store + * + * Since generally Thunk is a any function, we can't type it properly + */ +type AnyThunkLikeThing = (...args: any[]) => any; + +/** + * + * Utility function to create an Effector API to interact with Redux store, + * useful for cases like soft migration from Redux to Effector. + * + * @param config - interop config + * @param config.reduxStore - a redux store + * @param config.setup - effector unit which will setup subscription to the store + * @returns Interop API object + */ +export function createReduxIntegration< + State = unknown, + Act extends Action = { type: string; [k: string]: unknown }, + // eslint-disable-next-line @typescript-eslint/ban-types + Ext extends {} = {} +>(config: { + reduxStore: ReduxStore; + // We don't care about the type of the setup unit here + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setup: Unit; +}): { + /** + * Effector store containing the Redux store + * + * You can use it to substitute Redux store instance, while writing tests via Effector's Fork API + * @example + * ``` + * const scope = fork({ + * values: [ + * [reduxInterop.$reduxStore, reduxStoreMock] + * ] + * }) + * ``` + */ + $reduxStore: Store; + /** + * Effector's event, which will trigger Redux store dispatch + * + * @example + * ``` + * const updateName = reduxInterop.dispatch.prepend((name: string) => updateNameAction(name)); + * ``` + */ + dispatch: Effect; + /** + * Effector store containing the state of the Redux store + * + * You can use it to subscribe to the Redux store state changes in Effector + * @example + * ``` + * const $userName = combine(reduxInterop.$state, state => state.user.name) + * ``` + */ + $state: Store; +} { + const { reduxStore, setup } = config; + if (!is.unit(setup)) { + throw new Error('setup must be an effector unit'); + } + if ( + !reduxStore || + !reduxStore.dispatch || + !reduxStore.getState || + !reduxStore.subscribe + ) { + throw new Error('reduxStore must be provided and should be a Redux store'); + } + + const $reduxStore = createStore(reduxStore, { + serialize: 'ignore', + name: 'redux/$reduxStore', + }); + + const stateUpdated = createEvent(); + + const $state = createStore(reduxStore.getState() ?? null, { + serialize: 'ignore', + name: 'redux/$state', + skipVoid: false, + }).on(stateUpdated, (_, state) => state); + + const dispatchFx = attach({ + source: $reduxStore, + effect(store, action: Act | AnyThunkLikeThing) { + return store.dispatch(action as Act) as unknown; + }, + }); + + const reduxInteropSetupFx = attach({ + source: $reduxStore, + effect(store) { + const sendUpdate = scopeBind(stateUpdated, { safe: true }); + + sendUpdate(store.getState()); + + store.subscribe(() => { + sendUpdate(store.getState()); + }); + }, + }); + + sample({ + clock: setup, + target: reduxInteropSetupFx, + }); + + /** + * Logging any errors from the interop to the console for simplicity + */ + sample({ + clock: [dispatchFx.failData, reduxInteropSetupFx.failData], + }).watch(console.error); + + return { + $reduxStore, + dispatch: dispatchFx, + $state, + }; +} diff --git a/packages/redux/tsconfig.json b/packages/redux/tsconfig.json new file mode 100644 index 00000000..bdf594cd --- /dev/null +++ b/packages/redux/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "types": ["vitest"] + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/redux/tsconfig.lib.json b/packages/redux/tsconfig.lib.json new file mode 100644 index 00000000..33eca2c2 --- /dev/null +++ b/packages/redux/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/packages/redux/tsconfig.spec.json b/packages/redux/tsconfig.spec.json new file mode 100644 index 00000000..6d3be742 --- /dev/null +++ b/packages/redux/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node"] + }, + "include": [ + "vite.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/packages/redux/vite.config.ts b/packages/redux/vite.config.ts new file mode 100644 index 00000000..b2bacb5f --- /dev/null +++ b/packages/redux/vite.config.ts @@ -0,0 +1,32 @@ +/// +import { defineConfig } from 'vite'; + +import viteTsConfigPaths from 'vite-tsconfig-paths'; + +export default defineConfig({ + cacheDir: '../../node_modules/.vite/redux', + + plugins: [ + viteTsConfigPaths({ + root: '../../', + }), + ], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ + // viteTsConfigPaths({ + // root: '../../', + // }), + // ], + // }, + + test: { + globals: true, + cache: { + dir: '../../node_modules/.vitest', + }, + environment: 'node', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a2edc38..cb434675 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,16 +13,19 @@ importers: '@nrwl/rollup': 16.5.x '@nx/devkit': 16.5.x '@nx/eslint-plugin': 16.5.x - '@nx/js': 16.5.x + '@nx/js': 16.5.5 '@nx/linter': 16.5.x - '@nx/vite': 16.5.x + '@nx/rollup': 16.5.5 + '@nx/vite': 16.5.5 '@nx/web': 16.5.x '@nx/workspace': 16.5.x '@playwright/test': ^1.32.2 + '@reduxjs/toolkit': ^2.0.1 '@size-limit/file': ^7.0.8 '@types/node': 18.7.14 '@typescript-eslint/eslint-plugin': 5.62.0 '@typescript-eslint/parser': 5.62.0 + '@vitest/coverage-c8': ~0.32.0 '@vitest/ui': 0.34.4 bytes-iec: ^3.1.1 effector: 23.0.0 @@ -33,6 +36,8 @@ importers: nx: 16.5.x playwright: ^1.32.2 prettier: ^2.6.2 + redux: ^5.0.0 + redux-saga: ^1.2.3 rollup: ^3.4.0 rollup-plugin-dts: ^5.0.0 runtypes: ^6.7.0 @@ -59,14 +64,17 @@ importers: '@nx/eslint-plugin': 16.5.5_pskxzdqgz7ixj3binky6n75ayu '@nx/js': 16.5.5_nx@16.5.5+typescript@5.1.6 '@nx/linter': 16.5.5_kwllfdb2v446ti4q3q2swa5rd4 + '@nx/rollup': 16.5.5_oktbkxpsmwvyf527clbg2rcz6y '@nx/vite': 16.5.5_s2gbh5qz6ik5vo2vre6z3lum54 '@nx/web': 16.5.5_nx@16.5.5+typescript@5.1.6 '@nx/workspace': 16.5.5 '@playwright/test': 1.32.2 + '@reduxjs/toolkit': 2.0.1 '@size-limit/file': 7.0.8_size-limit@7.0.8 '@types/node': 18.7.14 '@typescript-eslint/eslint-plugin': 5.62.0_c42x62htuvinjyo6sqia6oy3e4 '@typescript-eslint/parser': 5.62.0_7haavtekmro7ptbnqmctjaodju + '@vitest/coverage-c8': 0.32.4_vitest@0.34.4 '@vitest/ui': 0.34.4_vitest@0.34.4 bytes-iec: 3.1.1 effector: 23.0.0 @@ -77,6 +85,8 @@ importers: nx: 16.5.5 playwright: 1.32.2 prettier: 2.7.1 + redux: 5.0.0 + redux-saga: 1.2.3 rollup: 3.4.0 rollup-plugin-dts: 5.0.0_t5s4utmkuub4tfq3v447yfkfbm runtypes: 6.7.0 @@ -95,6 +105,9 @@ importers: packages/i18next: specifiers: {} + packages/redux: + specifiers: {} + packages/web-api: specifiers: {} @@ -275,6 +288,14 @@ packages: '@jridgewell/trace-mapping': 0.3.14 dev: true + /@ampproject/remapping/2.2.1: + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.2 + '@jridgewell/trace-mapping': 0.3.19 + dev: true + /@babel/code-frame/7.18.6: resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} engines: {node: '>=6.9.0'} @@ -333,7 +354,7 @@ packages: '@babel/helper-compilation-targets': 7.22.15 '@babel/helper-module-transforms': 7.22.17_@babel+core@7.22.17 '@babel/helpers': 7.22.15 - '@babel/parser': 7.22.16 + '@babel/parser': 7.23.4 '@babel/template': 7.22.15 '@babel/traverse': 7.22.17 '@babel/types': 7.22.17 @@ -1878,7 +1899,7 @@ packages: '@babel/helper-function-name': 7.22.5 '@babel/helper-hoist-variables': 7.22.5 '@babel/helper-split-export-declaration': 7.22.6 - '@babel/parser': 7.22.16 + '@babel/parser': 7.23.4 '@babel/types': 7.22.17 debug: 4.3.4 globals: 11.12.0 @@ -1902,6 +1923,10 @@ packages: '@babel/helper-validator-identifier': 7.22.15 to-fast-properties: 2.0.0 + /@bcoe/v8-coverage/0.2.3: + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + dev: true + /@changesets/apply-release-plan/6.0.3: resolution: {integrity: sha512-/3JKqtDefs2YSEQI6JQo43/MKTLfhPdrW/BFmqnRpW8UmPB+YXjjQgfjR/2KOaObLOkoixcL3WCK4wNkn/Krmw==} dependencies: @@ -2690,6 +2715,11 @@ packages: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} dev: true + /@istanbuljs/schema/0.1.3: + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + dev: true + /@jest/schemas/29.6.3: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2889,6 +2919,12 @@ packages: - verdaccio dev: true + /@nrwl/rollup/16.5.5: + resolution: {integrity: sha512-Eli96d4IDId84VIxQ2D8jezYTF/Bkdl1y0ds1fF60WJ8Gv/g27o4ZeTF8q+kN0VXlazwdcBVO4I2nhaEovRXkQ==} + dependencies: + '@nx/rollup': 16.5.5_oktbkxpsmwvyf527clbg2rcz6y + dev: true + /@nrwl/rollup/16.5.5_oktbkxpsmwvyf527clbg2rcz6y: resolution: {integrity: sha512-Eli96d4IDId84VIxQ2D8jezYTF/Bkdl1y0ds1fF60WJ8Gv/g27o4ZeTF8q+kN0VXlazwdcBVO4I2nhaEovRXkQ==} dependencies: @@ -3160,7 +3196,7 @@ packages: /@nx/rollup/16.5.5_oktbkxpsmwvyf527clbg2rcz6y: resolution: {integrity: sha512-C16BWkNBDWX8581EGXBIwEl0DmJ9BSSX79vhJGrVlq0uwQMPWwRm6MrEpAzqiiO70fztKvYFAGGAyJGEKwUR2Q==} dependencies: - '@nrwl/rollup': 16.5.5_oktbkxpsmwvyf527clbg2rcz6y + '@nrwl/rollup': 16.5.5 '@nx/devkit': 16.5.5_nx@16.5.5 '@nx/js': 16.5.5_nx@16.5.5+typescript@5.1.6 '@rollup/plugin-babel': 5.3.1_p5rdeczgubbtv4lfg2mcoxxnwm @@ -3168,15 +3204,15 @@ packages: '@rollup/plugin-image': 2.1.1_rollup@2.79.1 '@rollup/plugin-json': 4.1.0_rollup@2.79.1 '@rollup/plugin-node-resolve': 13.3.0_rollup@2.79.1 - autoprefixer: 10.4.15_postcss@8.4.29 + autoprefixer: 10.4.15_postcss@8.4.31 babel-plugin-transform-async-to-promises: 0.8.18 chalk: 4.1.2 dotenv: 10.0.0 - postcss: 8.4.29 + postcss: 8.4.31 rollup: 2.79.1 rollup-plugin-copy: 3.5.0 rollup-plugin-peer-deps-external: 2.2.4_rollup@2.79.1 - rollup-plugin-postcss: 4.0.2_postcss@8.4.29 + rollup-plugin-postcss: 4.0.2_postcss@8.4.31 rollup-plugin-typescript2: 0.34.1_u4azwogfy4ehekjp2akth6llga rxjs: 7.8.1 tslib: 2.4.0 @@ -3306,6 +3342,61 @@ packages: resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} dev: true + /@redux-saga/core/1.2.3: + resolution: {integrity: sha512-U1JO6ncFBAklFTwoQ3mjAeQZ6QGutsJzwNBjgVLSWDpZTRhobUzuVDS1qH3SKGJD8fvqoaYOjp6XJ3gCmeZWgA==} + dependencies: + '@babel/runtime': 7.22.15 + '@redux-saga/deferred': 1.2.1 + '@redux-saga/delay-p': 1.2.1 + '@redux-saga/is': 1.1.3 + '@redux-saga/symbols': 1.1.3 + '@redux-saga/types': 1.2.1 + redux: 4.2.1 + typescript-tuple: 2.2.1 + dev: true + + /@redux-saga/deferred/1.2.1: + resolution: {integrity: sha512-cmin3IuuzMdfQjA0lG4B+jX+9HdTgHZZ+6u3jRAOwGUxy77GSlTi4Qp2d6PM1PUoTmQUR5aijlA39scWWPF31g==} + dev: true + + /@redux-saga/delay-p/1.2.1: + resolution: {integrity: sha512-MdiDxZdvb1m+Y0s4/hgdcAXntpUytr9g0hpcOO1XFVyyzkrDu3SKPgBFOtHn7lhu7n24ZKIAT1qtKyQjHqRd+w==} + dependencies: + '@redux-saga/symbols': 1.1.3 + dev: true + + /@redux-saga/is/1.1.3: + resolution: {integrity: sha512-naXrkETG1jLRfVfhOx/ZdLj0EyAzHYbgJWkXbB3qFliPcHKiWbv/ULQryOAEKyjrhiclmr6AMdgsXFyx7/yE6Q==} + dependencies: + '@redux-saga/symbols': 1.1.3 + '@redux-saga/types': 1.2.1 + dev: true + + /@redux-saga/symbols/1.1.3: + resolution: {integrity: sha512-hCx6ZvU4QAEUojETnX8EVg4ubNLBFl1Lps4j2tX7o45x/2qg37m3c6v+kSp8xjDJY+2tJw4QB3j8o8dsl1FDXg==} + dev: true + + /@redux-saga/types/1.2.1: + resolution: {integrity: sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==} + dev: true + + /@reduxjs/toolkit/2.0.1: + resolution: {integrity: sha512-fxIjrR9934cmS8YXIGd9e7s1XRsEU++aFc9DVNMFMRTM5Vtsg2DCRMj21eslGtDt43IUf9bJL3h5bwUlZleibA==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + dependencies: + immer: 10.0.3 + redux: 5.0.0 + redux-thunk: 3.1.0_redux@5.0.0 + reselect: 5.0.1 + dev: true + /@rollup/plugin-babel/5.3.1_p5rdeczgubbtv4lfg2mcoxxnwm: resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} engines: {node: '>= 10.0.0'} @@ -3562,6 +3653,10 @@ packages: ci-info: 3.3.2 dev: true + /@types/istanbul-lib-coverage/2.0.6: + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + dev: true + /@types/json-schema/7.0.11: resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} dev: true @@ -3778,6 +3873,20 @@ packages: vue: 3.3.9_typescript@5.1.6 dev: true + /@vitest/coverage-c8/0.32.4_vitest@0.34.4: + resolution: {integrity: sha512-ahJowPxgSBnaI2J+L/xmzM2oWFkk/+HtjnRfkQZrZd7H80JyG1/D6Xp6UPSZk8MXnoa90NThoyXRDBt12tNBRg==} + deprecated: v8 coverage is moved to @vitest/coverage-v8 package + peerDependencies: + vitest: '>=0.30.0 <1' + dependencies: + '@ampproject/remapping': 2.2.1 + c8: 7.14.0 + magic-string: 0.30.5 + picocolors: 1.0.0 + std-env: 3.4.3 + vitest: 0.34.4_z3so6msm7ayeqtjjbl45xcygaq + dev: true + /@vitest/expect/0.34.4: resolution: {integrity: sha512-XlMKX8HyYUqB8dsY8Xxrc64J2Qs9pKMt2Z8vFTL4mBWXJsg4yoALHzJfDWi8h5nkO4Zua4zjqtapQ/IluVkSnA==} dependencies: @@ -4240,7 +4349,7 @@ packages: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: true - /autoprefixer/10.4.15_postcss@8.4.29: + /autoprefixer/10.4.15_postcss@8.4.31: resolution: {integrity: sha512-KCuPB8ZCIqFdA4HwKXsvz7j6gvSDNhDP7WnUjBleRkKjPdvCmHFuQ77ocavI8FT6NdvlBnE2UFr2H4Mycn8Vew==} engines: {node: ^10 || ^12 || >=14} hasBin: true @@ -4252,7 +4361,7 @@ packages: fraction.js: 4.3.6 normalize-range: 0.1.2 picocolors: 1.0.0 - postcss: 8.4.29 + postcss: 8.4.31 postcss-value-parser: 4.2.0 dev: true @@ -4447,6 +4556,25 @@ packages: engines: {node: '>= 0.8'} dev: true + /c8/7.14.0: + resolution: {integrity: sha512-i04rtkkcNcCf7zsQcSv/T9EbUn4RXQ6mropeMcjFOsQXQ0iGLAr/xT6TImQg4+U9hmNpN9XdvPkjUL1IzbgxJw==} + engines: {node: '>=10.12.0'} + hasBin: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@istanbuljs/schema': 0.1.3 + find-up: 5.0.0 + foreground-child: 2.0.0 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.1.6 + rimraf: 3.0.2 + test-exclude: 6.0.0 + v8-to-istanbul: 9.2.0 + yargs: 16.2.0 + yargs-parser: 20.2.9 + dev: true + /cac/6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -4557,7 +4685,7 @@ packages: normalize-path: 3.0.0 readdirp: 3.6.0 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /ci-info/3.3.2: @@ -4683,6 +4811,10 @@ packages: safe-buffer: 5.1.2 dev: true + /convert-source-map/2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true + /core-js-compat/3.32.2: resolution: {integrity: sha512-+GjlguTDINOijtVRUxrQOv3kfu9rl+qPNdX2LTbJ/ZyVTuxK+ksVSAGX1nHstu4hrv1En/uPTtWgq2gI5wt4AQ==} dependencies: @@ -4726,13 +4858,13 @@ packages: which: 2.0.2 dev: true - /css-declaration-sorter/6.4.1_postcss@8.4.29: + /css-declaration-sorter/6.4.1_postcss@8.4.31: resolution: {integrity: sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==} engines: {node: ^10 || ^12 || >=14} peerDependencies: postcss: ^8.0.9 dependencies: - postcss: 8.4.29 + postcss: 8.4.31 dev: true /css-select/4.3.0: @@ -4764,62 +4896,62 @@ packages: hasBin: true dev: true - /cssnano-preset-default/5.2.14_postcss@8.4.29: + /cssnano-preset-default/5.2.14_postcss@8.4.31: resolution: {integrity: sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - css-declaration-sorter: 6.4.1_postcss@8.4.29 - cssnano-utils: 3.1.0_postcss@8.4.29 - postcss: 8.4.29 - postcss-calc: 8.2.4_postcss@8.4.29 - postcss-colormin: 5.3.1_postcss@8.4.29 - postcss-convert-values: 5.1.3_postcss@8.4.29 - postcss-discard-comments: 5.1.2_postcss@8.4.29 - postcss-discard-duplicates: 5.1.0_postcss@8.4.29 - postcss-discard-empty: 5.1.1_postcss@8.4.29 - postcss-discard-overridden: 5.1.0_postcss@8.4.29 - postcss-merge-longhand: 5.1.7_postcss@8.4.29 - postcss-merge-rules: 5.1.4_postcss@8.4.29 - postcss-minify-font-values: 5.1.0_postcss@8.4.29 - postcss-minify-gradients: 5.1.1_postcss@8.4.29 - postcss-minify-params: 5.1.4_postcss@8.4.29 - postcss-minify-selectors: 5.2.1_postcss@8.4.29 - postcss-normalize-charset: 5.1.0_postcss@8.4.29 - postcss-normalize-display-values: 5.1.0_postcss@8.4.29 - postcss-normalize-positions: 5.1.1_postcss@8.4.29 - postcss-normalize-repeat-style: 5.1.1_postcss@8.4.29 - postcss-normalize-string: 5.1.0_postcss@8.4.29 - postcss-normalize-timing-functions: 5.1.0_postcss@8.4.29 - postcss-normalize-unicode: 5.1.1_postcss@8.4.29 - postcss-normalize-url: 5.1.0_postcss@8.4.29 - postcss-normalize-whitespace: 5.1.1_postcss@8.4.29 - postcss-ordered-values: 5.1.3_postcss@8.4.29 - postcss-reduce-initial: 5.1.2_postcss@8.4.29 - postcss-reduce-transforms: 5.1.0_postcss@8.4.29 - postcss-svgo: 5.1.0_postcss@8.4.29 - postcss-unique-selectors: 5.1.1_postcss@8.4.29 - dev: true - - /cssnano-utils/3.1.0_postcss@8.4.29: + css-declaration-sorter: 6.4.1_postcss@8.4.31 + cssnano-utils: 3.1.0_postcss@8.4.31 + postcss: 8.4.31 + postcss-calc: 8.2.4_postcss@8.4.31 + postcss-colormin: 5.3.1_postcss@8.4.31 + postcss-convert-values: 5.1.3_postcss@8.4.31 + postcss-discard-comments: 5.1.2_postcss@8.4.31 + postcss-discard-duplicates: 5.1.0_postcss@8.4.31 + postcss-discard-empty: 5.1.1_postcss@8.4.31 + postcss-discard-overridden: 5.1.0_postcss@8.4.31 + postcss-merge-longhand: 5.1.7_postcss@8.4.31 + postcss-merge-rules: 5.1.4_postcss@8.4.31 + postcss-minify-font-values: 5.1.0_postcss@8.4.31 + postcss-minify-gradients: 5.1.1_postcss@8.4.31 + postcss-minify-params: 5.1.4_postcss@8.4.31 + postcss-minify-selectors: 5.2.1_postcss@8.4.31 + postcss-normalize-charset: 5.1.0_postcss@8.4.31 + postcss-normalize-display-values: 5.1.0_postcss@8.4.31 + postcss-normalize-positions: 5.1.1_postcss@8.4.31 + postcss-normalize-repeat-style: 5.1.1_postcss@8.4.31 + postcss-normalize-string: 5.1.0_postcss@8.4.31 + postcss-normalize-timing-functions: 5.1.0_postcss@8.4.31 + postcss-normalize-unicode: 5.1.1_postcss@8.4.31 + postcss-normalize-url: 5.1.0_postcss@8.4.31 + postcss-normalize-whitespace: 5.1.1_postcss@8.4.31 + postcss-ordered-values: 5.1.3_postcss@8.4.31 + postcss-reduce-initial: 5.1.2_postcss@8.4.31 + postcss-reduce-transforms: 5.1.0_postcss@8.4.31 + postcss-svgo: 5.1.0_postcss@8.4.31 + postcss-unique-selectors: 5.1.1_postcss@8.4.31 + dev: true + + /cssnano-utils/3.1.0_postcss@8.4.31: resolution: {integrity: sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.29 + postcss: 8.4.31 dev: true - /cssnano/5.1.15_postcss@8.4.29: + /cssnano/5.1.15_postcss@8.4.31: resolution: {integrity: sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - cssnano-preset-default: 5.2.14_postcss@8.4.29 + cssnano-preset-default: 5.2.14_postcss@8.4.31 lilconfig: 2.0.5 - postcss: 8.4.29 + postcss: 8.4.31 yaml: 1.10.2 dev: true @@ -5491,6 +5623,14 @@ packages: optional: true dev: true + /foreground-child/2.0.0: + resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} + engines: {node: '>=8.0.0'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 3.0.7 + dev: true + /form-data/4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -5861,6 +6001,10 @@ packages: whatwg-encoding: 2.0.0 dev: true + /html-escaper/2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + dev: true + /html-void-elements/3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} dev: true @@ -5927,13 +6071,13 @@ packages: resolution: {integrity: sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==} dev: true - /icss-utils/5.1.0_postcss@8.4.29: + /icss-utils/5.1.0_postcss@8.4.31: resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - postcss: 8.4.29 + postcss: 8.4.31 dev: true /ieee754/1.2.1: @@ -5944,6 +6088,10 @@ packages: engines: {node: '>= 4'} dev: true + /immer/10.0.3: + resolution: {integrity: sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==} + dev: true + /import-cwd/3.0.0: resolution: {integrity: sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==} engines: {node: '>=8'} @@ -6175,6 +6323,28 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true + /istanbul-lib-coverage/3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + dev: true + + /istanbul-lib-report/3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + dev: true + + /istanbul-reports/3.1.6: + resolution: {integrity: sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==} + engines: {node: '>=8'} + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + dev: true + /jake/10.8.5: resolution: {integrity: sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==} engines: {node: '>=10'} @@ -6426,6 +6596,13 @@ packages: semver: 6.3.1 dev: true + /make-dir/4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + dependencies: + semver: 7.5.3 + dev: true + /map-obj/1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} engines: {node: '>=0.10.0'} @@ -7014,17 +7191,17 @@ packages: - supports-color dev: true - /postcss-calc/8.2.4_postcss@8.4.29: + /postcss-calc/8.2.4_postcss@8.4.31: resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==} peerDependencies: postcss: ^8.2.2 dependencies: - postcss: 8.4.29 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 postcss-value-parser: 4.2.0 dev: true - /postcss-colormin/5.3.1_postcss@8.4.29: + /postcss-colormin/5.3.1_postcss@8.4.31: resolution: {integrity: sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: @@ -7033,58 +7210,58 @@ packages: browserslist: 4.21.10 caniuse-api: 3.0.0 colord: 2.9.3 - postcss: 8.4.29 + postcss: 8.4.31 postcss-value-parser: 4.2.0 dev: true - /postcss-convert-values/5.1.3_postcss@8.4.29: + /postcss-convert-values/5.1.3_postcss@8.4.31: resolution: {integrity: sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: browserslist: 4.21.10 - postcss: 8.4.29 + postcss: 8.4.31 postcss-value-parser: 4.2.0 dev: true - /postcss-discard-comments/5.1.2_postcss@8.4.29: + /postcss-discard-comments/5.1.2_postcss@8.4.31: resolution: {integrity: sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.29 + postcss: 8.4.31 dev: true - /postcss-discard-duplicates/5.1.0_postcss@8.4.29: + /postcss-discard-duplicates/5.1.0_postcss@8.4.31: resolution: {integrity: sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.29 + postcss: 8.4.31 dev: true - /postcss-discard-empty/5.1.1_postcss@8.4.29: + /postcss-discard-empty/5.1.1_postcss@8.4.31: resolution: {integrity: sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.29 + postcss: 8.4.31 dev: true - /postcss-discard-overridden/5.1.0_postcss@8.4.29: + /postcss-discard-overridden/5.1.0_postcss@8.4.31: resolution: {integrity: sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.29 + postcss: 8.4.31 dev: true - /postcss-load-config/3.1.4_postcss@8.4.29: + /postcss-load-config/3.1.4_postcss@8.4.31: resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} engines: {node: '>= 10'} peerDependencies: @@ -7097,22 +7274,22 @@ packages: optional: true dependencies: lilconfig: 2.0.5 - postcss: 8.4.29 + postcss: 8.4.31 yaml: 1.10.2 dev: true - /postcss-merge-longhand/5.1.7_postcss@8.4.29: + /postcss-merge-longhand/5.1.7_postcss@8.4.31: resolution: {integrity: sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.29 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - stylehacks: 5.1.1_postcss@8.4.29 + stylehacks: 5.1.1_postcss@8.4.31 dev: true - /postcss-merge-rules/5.1.4_postcss@8.4.29: + /postcss-merge-rules/5.1.4_postcss@8.4.31: resolution: {integrity: sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: @@ -7120,97 +7297,97 @@ packages: dependencies: browserslist: 4.21.10 caniuse-api: 3.0.0 - cssnano-utils: 3.1.0_postcss@8.4.29 - postcss: 8.4.29 + cssnano-utils: 3.1.0_postcss@8.4.31 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 dev: true - /postcss-minify-font-values/5.1.0_postcss@8.4.29: + /postcss-minify-font-values/5.1.0_postcss@8.4.31: resolution: {integrity: sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.29 + postcss: 8.4.31 postcss-value-parser: 4.2.0 dev: true - /postcss-minify-gradients/5.1.1_postcss@8.4.29: + /postcss-minify-gradients/5.1.1_postcss@8.4.31: resolution: {integrity: sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: colord: 2.9.3 - cssnano-utils: 3.1.0_postcss@8.4.29 - postcss: 8.4.29 + cssnano-utils: 3.1.0_postcss@8.4.31 + postcss: 8.4.31 postcss-value-parser: 4.2.0 dev: true - /postcss-minify-params/5.1.4_postcss@8.4.29: + /postcss-minify-params/5.1.4_postcss@8.4.31: resolution: {integrity: sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: browserslist: 4.21.10 - cssnano-utils: 3.1.0_postcss@8.4.29 - postcss: 8.4.29 + cssnano-utils: 3.1.0_postcss@8.4.31 + postcss: 8.4.31 postcss-value-parser: 4.2.0 dev: true - /postcss-minify-selectors/5.2.1_postcss@8.4.29: + /postcss-minify-selectors/5.2.1_postcss@8.4.31: resolution: {integrity: sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.29 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 dev: true - /postcss-modules-extract-imports/3.0.0_postcss@8.4.29: + /postcss-modules-extract-imports/3.0.0_postcss@8.4.31: resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - postcss: 8.4.29 + postcss: 8.4.31 dev: true - /postcss-modules-local-by-default/4.0.3_postcss@8.4.29: + /postcss-modules-local-by-default/4.0.3_postcss@8.4.31: resolution: {integrity: sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - icss-utils: 5.1.0_postcss@8.4.29 - postcss: 8.4.29 + icss-utils: 5.1.0_postcss@8.4.31 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 postcss-value-parser: 4.2.0 dev: true - /postcss-modules-scope/3.0.0_postcss@8.4.29: + /postcss-modules-scope/3.0.0_postcss@8.4.31: resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - postcss: 8.4.29 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 dev: true - /postcss-modules-values/4.0.0_postcss@8.4.29: + /postcss-modules-values/4.0.0_postcss@8.4.31: resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - icss-utils: 5.1.0_postcss@8.4.29 - postcss: 8.4.29 + icss-utils: 5.1.0_postcss@8.4.31 + postcss: 8.4.31 dev: true - /postcss-modules/4.3.1_postcss@8.4.29: + /postcss-modules/4.3.1_postcss@8.4.31: resolution: {integrity: sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q==} peerDependencies: postcss: ^8.0.0 @@ -7218,117 +7395,117 @@ packages: generic-names: 4.0.0 icss-replace-symbols: 1.1.0 lodash.camelcase: 4.3.0 - postcss: 8.4.29 - postcss-modules-extract-imports: 3.0.0_postcss@8.4.29 - postcss-modules-local-by-default: 4.0.3_postcss@8.4.29 - postcss-modules-scope: 3.0.0_postcss@8.4.29 - postcss-modules-values: 4.0.0_postcss@8.4.29 + postcss: 8.4.31 + postcss-modules-extract-imports: 3.0.0_postcss@8.4.31 + postcss-modules-local-by-default: 4.0.3_postcss@8.4.31 + postcss-modules-scope: 3.0.0_postcss@8.4.31 + postcss-modules-values: 4.0.0_postcss@8.4.31 string-hash: 1.1.3 dev: true - /postcss-normalize-charset/5.1.0_postcss@8.4.29: + /postcss-normalize-charset/5.1.0_postcss@8.4.31: resolution: {integrity: sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.29 + postcss: 8.4.31 dev: true - /postcss-normalize-display-values/5.1.0_postcss@8.4.29: + /postcss-normalize-display-values/5.1.0_postcss@8.4.31: resolution: {integrity: sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.29 + postcss: 8.4.31 postcss-value-parser: 4.2.0 dev: true - /postcss-normalize-positions/5.1.1_postcss@8.4.29: + /postcss-normalize-positions/5.1.1_postcss@8.4.31: resolution: {integrity: sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.29 + postcss: 8.4.31 postcss-value-parser: 4.2.0 dev: true - /postcss-normalize-repeat-style/5.1.1_postcss@8.4.29: + /postcss-normalize-repeat-style/5.1.1_postcss@8.4.31: resolution: {integrity: sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.29 + postcss: 8.4.31 postcss-value-parser: 4.2.0 dev: true - /postcss-normalize-string/5.1.0_postcss@8.4.29: + /postcss-normalize-string/5.1.0_postcss@8.4.31: resolution: {integrity: sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.29 + postcss: 8.4.31 postcss-value-parser: 4.2.0 dev: true - /postcss-normalize-timing-functions/5.1.0_postcss@8.4.29: + /postcss-normalize-timing-functions/5.1.0_postcss@8.4.31: resolution: {integrity: sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.29 + postcss: 8.4.31 postcss-value-parser: 4.2.0 dev: true - /postcss-normalize-unicode/5.1.1_postcss@8.4.29: + /postcss-normalize-unicode/5.1.1_postcss@8.4.31: resolution: {integrity: sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: browserslist: 4.21.10 - postcss: 8.4.29 + postcss: 8.4.31 postcss-value-parser: 4.2.0 dev: true - /postcss-normalize-url/5.1.0_postcss@8.4.29: + /postcss-normalize-url/5.1.0_postcss@8.4.31: resolution: {integrity: sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: normalize-url: 6.1.0 - postcss: 8.4.29 + postcss: 8.4.31 postcss-value-parser: 4.2.0 dev: true - /postcss-normalize-whitespace/5.1.1_postcss@8.4.29: + /postcss-normalize-whitespace/5.1.1_postcss@8.4.31: resolution: {integrity: sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.29 + postcss: 8.4.31 postcss-value-parser: 4.2.0 dev: true - /postcss-ordered-values/5.1.3_postcss@8.4.29: + /postcss-ordered-values/5.1.3_postcss@8.4.31: resolution: {integrity: sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - cssnano-utils: 3.1.0_postcss@8.4.29 - postcss: 8.4.29 + cssnano-utils: 3.1.0_postcss@8.4.31 + postcss: 8.4.31 postcss-value-parser: 4.2.0 dev: true - /postcss-reduce-initial/5.1.2_postcss@8.4.29: + /postcss-reduce-initial/5.1.2_postcss@8.4.31: resolution: {integrity: sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: @@ -7336,16 +7513,16 @@ packages: dependencies: browserslist: 4.21.10 caniuse-api: 3.0.0 - postcss: 8.4.29 + postcss: 8.4.31 dev: true - /postcss-reduce-transforms/5.1.0_postcss@8.4.29: + /postcss-reduce-transforms/5.1.0_postcss@8.4.31: resolution: {integrity: sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.29 + postcss: 8.4.31 postcss-value-parser: 4.2.0 dev: true @@ -7357,24 +7534,24 @@ packages: util-deprecate: 1.0.2 dev: true - /postcss-svgo/5.1.0_postcss@8.4.29: + /postcss-svgo/5.1.0_postcss@8.4.31: resolution: {integrity: sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.29 + postcss: 8.4.31 postcss-value-parser: 4.2.0 svgo: 2.8.0 dev: true - /postcss-unique-selectors/5.1.1_postcss@8.4.29: + /postcss-unique-selectors/5.1.1_postcss@8.4.31: resolution: {integrity: sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.29 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 dev: true @@ -7534,6 +7711,30 @@ packages: strip-indent: 3.0.0 dev: true + /redux-saga/1.2.3: + resolution: {integrity: sha512-HDe0wTR5nhd8Xr5xjGzoyTbdAw6rjy1GDplFt3JKtKN8/MnkQSRqK/n6aQQhpw5NI4ekDVOaW+w4sdxPBaCoTQ==} + dependencies: + '@redux-saga/core': 1.2.3 + dev: true + + /redux-thunk/3.1.0_redux@5.0.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + dependencies: + redux: 5.0.0 + dev: true + + /redux/4.2.1: + resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} + dependencies: + '@babel/runtime': 7.22.15 + dev: true + + /redux/5.0.0: + resolution: {integrity: sha512-blLIYmYetpZMET6Q6uCY7Jtl/Im5OBldy+vNPauA8vvsdqyt66oep4EUpAMWNHauTC6xa9JuRPhRB72rY82QGA==} + dev: true + /regenerate-unicode-properties/10.1.0: resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==} engines: {node: '>=4'} @@ -7600,6 +7801,10 @@ packages: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} dev: true + /reselect/5.0.1: + resolution: {integrity: sha512-D72j2ubjgHpvuCiORWkOUxndHJrxDaSolheiz5CO+roz8ka97/4msh2E8F5qay4GawR5vzBt5MkbDHT+Rdy/Wg==} + dev: true + /resolve-from/4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -7672,7 +7877,7 @@ packages: rollup: 2.79.1 dev: true - /rollup-plugin-postcss/4.0.2_postcss@8.4.29: + /rollup-plugin-postcss/4.0.2_postcss@8.4.31: resolution: {integrity: sha512-05EaY6zvZdmvPUDi3uCcAQoESDcYnv8ogJJQRp6V5kZ6J6P7uAVJlrTZcaaA20wTH527YTnKfkAoPxWI/jPp4w==} engines: {node: '>=10'} peerDependencies: @@ -7680,13 +7885,13 @@ packages: dependencies: chalk: 4.1.2 concat-with-sourcemaps: 1.1.0 - cssnano: 5.1.15_postcss@8.4.29 + cssnano: 5.1.15_postcss@8.4.31 import-cwd: 3.0.0 p-queue: 6.6.2 pify: 5.0.0 - postcss: 8.4.29 - postcss-load-config: 3.1.4_postcss@8.4.29 - postcss-modules: 4.3.1_postcss@8.4.29 + postcss: 8.4.31 + postcss-load-config: 3.1.4_postcss@8.4.31 + postcss-modules: 4.3.1_postcss@8.4.31 promise.series: 0.2.0 resolve: 1.22.1 rollup-pluginutils: 2.8.2 @@ -7722,7 +7927,7 @@ packages: engines: {node: '>=10.0.0'} hasBin: true optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /rollup/3.29.0: @@ -7730,7 +7935,7 @@ packages: engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /rollup/3.4.0: @@ -7738,7 +7943,7 @@ packages: engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /rollup/4.6.0: @@ -8144,14 +8349,14 @@ packages: resolution: {integrity: sha512-Ca5ib8HrFn+f+0n4N4ScTIA9iTOQ7MaGS1ylHcoVqW9J7w2w8PzN6g9gKmTYgGEBH8e120+RCmhpje6jC5uGWA==} dev: false - /stylehacks/5.1.1_postcss@8.4.29: + /stylehacks/5.1.1_postcss@8.4.31: resolution: {integrity: sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: browserslist: 4.21.10 - postcss: 8.4.29 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 dev: true @@ -8208,6 +8413,15 @@ packages: engines: {node: '>=8'} dev: true + /test-exclude/6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + dev: true + /text-table/0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true @@ -8355,6 +8569,22 @@ packages: engines: {node: '>=8'} dev: true + /typescript-compare/0.0.2: + resolution: {integrity: sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==} + dependencies: + typescript-logic: 0.0.0 + dev: true + + /typescript-logic/0.0.0: + resolution: {integrity: sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==} + dev: true + + /typescript-tuple/2.2.1: + resolution: {integrity: sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==} + dependencies: + typescript-compare: 0.0.2 + dev: true + /typescript/5.1.6: resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} engines: {node: '>=14.17'} @@ -8487,6 +8717,15 @@ packages: resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==} dev: true + /v8-to-istanbul/9.2.0: + resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==} + engines: {node: '>=10.12.0'} + dependencies: + '@jridgewell/trace-mapping': 0.3.19 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + dev: true + /validate-npm-package-license/3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: @@ -8588,7 +8827,7 @@ packages: postcss: 8.4.29 rollup: 3.29.0 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /vite/5.0.2_@types+node@18.7.14: @@ -8912,6 +9151,11 @@ packages: decamelize: 1.2.0 dev: true + /yargs-parser/20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + dev: true + /yargs-parser/21.0.1: resolution: {integrity: sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==} engines: {node: '>=12'} @@ -8939,6 +9183,19 @@ packages: yargs-parser: 18.1.3 dev: true + /yargs/16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + dependencies: + cliui: 7.0.4 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + dev: true + /yargs/17.5.1: resolution: {integrity: sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==} engines: {node: '>=12'} diff --git a/tsconfig.base.json b/tsconfig.base.json index 7636808d..3de190fd 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,6 +15,7 @@ "paths": { "@withease/factories": ["packages/factories/index.ts"], "@withease/i18next": ["packages/i18next/index.ts"], + "@withease/redux": ["packages/redux/src/index.ts"], "@withease/web-api": ["packages/web-api/index.ts"] } },