├── .nvmrc ├── .github ├── FUNDING.yml └── workflows │ ├── changes.yml │ ├── release.yml │ ├── version.yml │ └── ci.yml ├── .prettierrc ├── packages ├── redux │ ├── src │ │ ├── index.ts │ │ └── integration.ts │ ├── README.md │ ├── tsconfig.json │ ├── vite.config.js │ ├── CHANGELOG.md │ └── package.json ├── i18next │ ├── src │ │ ├── index.ts │ │ ├── language.test.ts │ │ ├── async_init.test.ts │ │ ├── reporting.test.ts │ │ ├── is_ready.test.ts │ │ └── translated.test.ts │ ├── README.md │ ├── tsconfig.json │ ├── vite.config.js │ ├── package.json │ └── CHANGELOG.md ├── factories │ ├── src │ │ ├── index.ts │ │ ├── errors.ts │ │ ├── create_factory.ts │ │ ├── factories.test-d.ts │ │ ├── issue-33.test-d.ts │ │ ├── invoke.ts │ │ └── factories.test.ts │ ├── README.md │ ├── tsconfig.json │ ├── vite.config.js │ ├── CHANGELOG.md │ └── package.json ├── web-api │ ├── README.md │ ├── tsconfig.json │ ├── src │ │ ├── trigger_protocol.ts │ │ ├── index.ts │ │ ├── preferred_languages.test.ts │ │ ├── network_status.ts │ │ ├── page_visibility.ts │ │ ├── screen_orientation.ts │ │ ├── preferred_languages.ts │ │ ├── shared.ts │ │ └── media_query.ts │ ├── vite.config.js │ ├── package.json │ └── CHANGELOG.md └── contracts │ ├── README.md │ ├── tsconfig.json │ ├── CHANGELOG.md │ ├── vite.config.js │ ├── src │ ├── contract.test-d.ts │ └── interop.test.ts │ └── package.json ├── apps ├── website │ ├── .gitignore │ ├── docs │ │ ├── public │ │ │ ├── favicon.ico │ │ │ ├── icon-192.png │ │ │ ├── icon-512.png │ │ │ ├── apple-touch-icon.png │ │ │ └── manifest.webmanifest │ │ ├── i18next │ │ │ ├── instance.md │ │ │ ├── language.md │ │ │ ├── is_ready.md │ │ │ ├── change_language.md │ │ │ ├── reporting.md │ │ │ ├── releases.md │ │ │ ├── t.md │ │ │ ├── translated.md │ │ │ └── index.md │ │ ├── statements │ │ │ ├── compile_target.md │ │ │ ├── statements.data.ts │ │ │ ├── index.md │ │ │ ├── tests.md │ │ │ ├── typescript.md │ │ │ ├── releases.md │ │ │ └── ecosystem.md │ │ ├── .vitepress │ │ │ ├── theme │ │ │ │ ├── index.js │ │ │ │ └── LiveDemo.vue │ │ │ └── sidebar_creator.mjs │ │ ├── web-api │ │ │ ├── apis.data.ts │ │ │ ├── geolocation.live.vue │ │ │ ├── preferred_languages.live.vue │ │ │ ├── media_query.live.vue │ │ │ ├── index.md │ │ │ ├── network_status.live.vue │ │ │ ├── page_visibility.live.vue │ │ │ ├── screen_orientation.live.vue │ │ │ ├── page_visibility.md │ │ │ ├── network_status.md │ │ │ ├── screen_orientation.md │ │ │ ├── media_query.md │ │ │ └── preferred_languages.md │ │ ├── magazine │ │ │ ├── articles.data.ts │ │ │ ├── index.md │ │ │ ├── scopefull.md │ │ │ ├── dependency_injection.md │ │ │ ├── no_methods.md │ │ │ ├── no_domains.md │ │ │ └── explicit_start.md │ │ ├── protocols │ │ │ ├── protocols.data.ts │ │ │ ├── index.md │ │ │ ├── contract.md │ │ │ └── trigger.md │ │ ├── ecosystem.ts │ │ ├── contracts │ │ │ ├── cookbook │ │ │ │ ├── merge_objects.md │ │ │ │ ├── custom_matchers.md │ │ │ │ └── optional_fields.md │ │ │ ├── array_numbers.live.vue │ │ │ ├── size_chart.vue │ │ │ ├── sizes.data.ts │ │ │ └── index.md │ │ ├── factories │ │ │ ├── important_caveats.md │ │ │ ├── motivation.md │ │ │ └── index.md │ │ ├── index.md │ │ └── redux │ │ │ └── index.md │ ├── package.json │ └── scripts │ │ ├── changelog.mjs │ │ └── jsdoc.mjs └── web-api-demo │ ├── src │ ├── favicon.ico │ ├── preferred-languages.ts │ ├── page-visibility.ts │ ├── network.ts │ ├── media-query.ts │ ├── geolocation.ts │ └── screen-orientation.ts │ ├── vite.config.js │ ├── README.md │ ├── package.json │ ├── tsconfig.json │ ├── playwright.config.ts │ ├── test │ ├── preferred_languages.spec.ts │ ├── media_query.spec.ts │ └── geolocation.spec.ts │ ├── preferred-languages.html │ ├── network.html │ ├── media-query.html │ ├── page-visibility.html │ ├── .browserslistrc │ ├── screen-orientation.html │ ├── geolocation.html │ └── index.html ├── pnpm-workspace.yaml ├── branding ├── With Ease.png └── With Ease@2x.png ├── .prettierignore ├── tools ├── other-majors │ ├── manifest.json │ └── prepare.mjs ├── utils │ └── commit_message.js ├── publint.mjs └── vite │ └── types.js ├── .gitignore ├── .editorconfig ├── .changeset ├── config.json └── README.md ├── tsconfig.base.json ├── LICENSE ├── README.md └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.12 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [igorkamyshev] 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /packages/redux/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './integration'; 2 | -------------------------------------------------------------------------------- /apps/website/.gitignore: -------------------------------------------------------------------------------- 1 | docs/.vitepress/cache 2 | CHANGELOG.md 3 | api.md -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/**' 3 | - 'apps/**' 4 | -------------------------------------------------------------------------------- /packages/i18next/src/index.ts: -------------------------------------------------------------------------------- 1 | export { createI18nextIntegration } from './integration'; 2 | -------------------------------------------------------------------------------- /branding/With Ease.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/effector/withease/HEAD/branding/With Ease.png -------------------------------------------------------------------------------- /branding/With Ease@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/effector/withease/HEAD/branding/With Ease@2x.png -------------------------------------------------------------------------------- /apps/web-api-demo/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/effector/withease/HEAD/apps/web-api-demo/src/favicon.ico -------------------------------------------------------------------------------- /packages/redux/README.md: -------------------------------------------------------------------------------- 1 | # @withease/redux 2 | 3 | Read documentation [here](https://withease.effector.dev/redux/). 4 | -------------------------------------------------------------------------------- /apps/website/docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/effector/withease/HEAD/apps/website/docs/public/favicon.ico -------------------------------------------------------------------------------- /apps/website/docs/public/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/effector/withease/HEAD/apps/website/docs/public/icon-192.png -------------------------------------------------------------------------------- /apps/website/docs/public/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/effector/withease/HEAD/apps/website/docs/public/icon-512.png -------------------------------------------------------------------------------- /packages/factories/src/index.ts: -------------------------------------------------------------------------------- 1 | export { createFactory } from './create_factory'; 2 | export { invoke } from './invoke'; 3 | -------------------------------------------------------------------------------- /packages/i18next/README.md: -------------------------------------------------------------------------------- 1 | # @withease/i18next 2 | 3 | Read documentation [here](https://withease.effector.dev/i18next/). 4 | -------------------------------------------------------------------------------- /packages/web-api/README.md: -------------------------------------------------------------------------------- 1 | # @withease/web-api 2 | 3 | Read documentation [here](https://withease.effector.dev/web-api/). 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | pnpm-lock.yaml 6 | api.md -------------------------------------------------------------------------------- /packages/contracts/README.md: -------------------------------------------------------------------------------- 1 | # @withease/contracts 2 | 3 | Read documentation [here](https://withease.effector.dev/contracts/). 4 | -------------------------------------------------------------------------------- /packages/factories/README.md: -------------------------------------------------------------------------------- 1 | # @withease/factories 2 | 3 | Read documentation [here](https://withease.effector.dev/factories/). 4 | -------------------------------------------------------------------------------- /tools/other-majors/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "22": { 3 | "effector": "^22.8.6", 4 | "effector-vue": "^22.2.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /apps/website/docs/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/effector/withease/HEAD/apps/website/docs/public/apple-touch-icon.png -------------------------------------------------------------------------------- /apps/web-api-demo/vite.config.js: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from 'vite-tsconfig-paths'; 2 | 3 | export default { 4 | plugins: [tsconfigPaths()], 5 | }; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .DS_Store 4 | *.log 5 | 6 | # Playwright 7 | **/test-results 8 | **/playwright-report 9 | **/playwright/.cache 10 | -------------------------------------------------------------------------------- /apps/website/docs/public/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "icons": [ 3 | { "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" }, 4 | { "src": "/icon-512.png", "type": "image/png", "sizes": "512x512" } 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /apps/web-api-demo/README.md: -------------------------------------------------------------------------------- 1 | # web-api demo 2 | 3 | ## Local set up 4 | 5 | - clone the repo 6 | - ensure that `pnpm` is installed 7 | - install dependencies via `pnpm install` 8 | - start showcase via `pnpm --filter web-api-demo dev` 9 | -------------------------------------------------------------------------------- /apps/website/docs/i18next/instance.md: -------------------------------------------------------------------------------- 1 | # `$instance` 2 | 3 | The instance of i18next that is used by the integration can be accessed via the `$instance` [_Store_](https://effector.dev/docs/api/effector/store). 4 | -------------------------------------------------------------------------------- /apps/website/docs/statements/compile_target.md: -------------------------------------------------------------------------------- 1 | # Compile target 2 | 3 | All `@withease/*` packages are compiled to `es2018`. If you want to use them in an older environment, you need to compile them to a lower target in your building setup. 4 | -------------------------------------------------------------------------------- /apps/website/docs/.vitepress/theme/index.js: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme'; 2 | import LiveDemo from './LiveDemo.vue'; 3 | 4 | export default { 5 | extends: DefaultTheme, 6 | enhanceApp(ctx) { 7 | ctx.app.component('LiveDemo', LiveDemo); 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /apps/website/docs/i18next/language.md: -------------------------------------------------------------------------------- 1 | # `$language` 2 | 3 | A [_Store_](https://effector.dev/docs/api/effector/store) containing the current language. 4 | 5 | ```ts 6 | const { $language } = createI18nextIntegration({ 7 | /* ... */ 8 | }); 9 | ``` 10 | -------------------------------------------------------------------------------- /apps/web-api-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-api-demo", 3 | "private": true, 4 | "scripts": { 5 | "dev": "vite dev", 6 | "build": "vite build", 7 | "e2e": "playwright test" 8 | }, 9 | "dependencies": { 10 | "@withease/web-api": "workspace:*" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/web-api-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "types": ["node"], 6 | "outDir": "dist", 7 | "rootDir": "src", 8 | "baseUrl": "src" 9 | }, 10 | "include": ["src/**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/contracts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "types": ["node"], 6 | "outDir": "dist", 7 | "rootDir": "src", 8 | "baseUrl": "src" 9 | }, 10 | "include": ["src/**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/factories/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "types": ["node"], 6 | "outDir": "dist", 7 | "rootDir": "src", 8 | "baseUrl": "src" 9 | }, 10 | "include": ["src/**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/i18next/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "types": ["node"], 6 | "outDir": "dist", 7 | "rootDir": "src", 8 | "baseUrl": "src" 9 | }, 10 | "include": ["src/**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/redux/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "types": ["node"], 6 | "outDir": "dist", 7 | "rootDir": "src", 8 | "baseUrl": "src" 9 | }, 10 | "include": ["src/**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/web-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "types": ["node"], 6 | "outDir": "dist", 7 | "rootDir": "src", 8 | "baseUrl": "src" 9 | }, 10 | "include": ["src/**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /apps/website/docs/i18next/is_ready.md: -------------------------------------------------------------------------------- 1 | # `$isReady` 2 | 3 | A [_Store_](https://effector.dev/docs/api/effector/store) containing a boolean value that indicates whether the integration is ready to use. 4 | 5 | ```ts 6 | const { $isReady } = createI18nextIntegration({ 7 | /* ... */ 8 | }); 9 | ``` 10 | -------------------------------------------------------------------------------- /packages/web-api/src/trigger_protocol.ts: -------------------------------------------------------------------------------- 1 | import type { Event, EventCallable } from 'effector'; 2 | 3 | export type TriggerProtocol = { 4 | '@@trigger': () => { 5 | setup: EventCallable; 6 | teardown: EventCallable; 7 | fired: Event | Event; 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /apps/website/docs/statements/statements.data.ts: -------------------------------------------------------------------------------- 1 | import type { SiteConfig } from 'vitepress'; 2 | 3 | const config: SiteConfig = (globalThis as any).VITEPRESS_CONFIG; 4 | 5 | export default { 6 | load() { 7 | return config.userConfig.themeConfig.sidebar['/statements'].at(0).items; 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /apps/website/docs/web-api/apis.data.ts: -------------------------------------------------------------------------------- 1 | import type { SiteConfig } from 'vitepress'; 2 | 3 | const config: SiteConfig = (globalThis as any).VITEPRESS_CONFIG; 4 | 5 | export default { 6 | load() { 7 | return config.userConfig.themeConfig.sidebar['/web-api/'].at(0).items.at(1) 8 | .items; 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /packages/contracts/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @withease/contracts 2 | 3 | ## 1.1.0 4 | 5 | ### Minor Changes 6 | 7 | - 091ba06: Add `nothing` _Contract_ to simplify optional fields handling 8 | - 091ba06: Add `anything` _Contract_ to bypass validation 9 | 10 | ## 1.0.0 11 | 12 | ### Major Changes 13 | 14 | - 16968b8: Initial release 15 | -------------------------------------------------------------------------------- /apps/website/docs/magazine/articles.data.ts: -------------------------------------------------------------------------------- 1 | import type { SiteConfig } from 'vitepress'; 2 | 3 | const config: SiteConfig = (globalThis as any).VITEPRESS_CONFIG; 4 | 5 | export default { 6 | load() { 7 | const categories = config.userConfig.themeConfig.sidebar['/magazine/']; 8 | 9 | return categories; 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /apps/website/docs/protocols/protocols.data.ts: -------------------------------------------------------------------------------- 1 | import type { SiteConfig } from 'vitepress'; 2 | 3 | const config: SiteConfig = (globalThis as any).VITEPRESS_CONFIG; 4 | 5 | export default { 6 | load() { 7 | const protocols = 8 | config.userConfig.themeConfig.sidebar['/protocols/'].at(0).items; 9 | 10 | return protocols; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /apps/website/docs/.vitepress/sidebar_creator.mjs: -------------------------------------------------------------------------------- 1 | export function createSidebar(packageName, sidebar) { 2 | return { 3 | [`/${packageName}/`]: [ 4 | { 5 | text: packageName, 6 | items: [ 7 | ...sidebar, 8 | { text: 'Changelog', link: `/${packageName}/CHANGELOG` }, 9 | ], 10 | }, 11 | ], 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /apps/website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "private": true, 4 | "scripts": { 5 | "prepare": "node scripts/changelog.mjs && node scripts/jsdoc.mjs", 6 | "dev": "pnpm prepare && vitepress dev docs", 7 | "build": "pnpm prepare && vitepress build docs" 8 | }, 9 | "dependencies": { 10 | "@withease/web-api": "workspace:*" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/web-api/src/index.ts: -------------------------------------------------------------------------------- 1 | export { trackScreenOrientation } from './screen_orientation'; 2 | export { trackPageVisibility } from './page_visibility'; 3 | export { trackNetworkStatus } from './network_status'; 4 | export { trackMediaQuery } from './media_query'; 5 | export { trackPreferredLanguages } from './preferred_languages'; 6 | export { trackGeolocation } from './geolocation'; 7 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.1.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": "../tools/utils/commit_message.js", 5 | "fixed": [], 6 | "linked": [], 7 | "ignore": ["website", "*-demo"], 8 | "access": "public", 9 | "baseBranch": "origin/master", 10 | "updateInternalDependencies": "patch" 11 | } 12 | -------------------------------------------------------------------------------- /tools/utils/commit_message.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | getAddMessage(changeset) { 3 | return changeset.summary; 4 | }, 5 | getVersionMessage(releasePlan) { 6 | // We uses fixed version numbers for all packages 7 | const version = releasePlan.releases 8 | .filter((release) => release.type !== 'none') 9 | .at(0).newVersion; 10 | 11 | return `Release ${version}`; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /apps/website/docs/statements/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar: false 3 | --- 4 | 5 | # Statements 6 | 7 | We follow several principles in developing libraries. There are our statements about it: 8 | 9 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /apps/website/docs/i18next/change_language.md: -------------------------------------------------------------------------------- 1 | # `changeLanguageFx` 2 | 3 | An [_Effect_](https://effector.dev/en/api/effector/effect/) that can be called with a language code to change the current language. 4 | 5 | ```ts 6 | const { changeLanguageFx } = createI18nextIntegration({ 7 | /* ... */ 8 | }); 9 | 10 | sample({ 11 | clock: someButtonClicked, 12 | fn: () => 'en', 13 | target: changeLanguageFx, 14 | }); 15 | ``` 16 | -------------------------------------------------------------------------------- /apps/web-api-demo/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from '@playwright/test'; 2 | 3 | const config: PlaywrightTestConfig = { 4 | retries: 3, 5 | maxFailures: 2, 6 | timeout: 120000, 7 | use: { baseURL: 'http://localhost:5173' }, 8 | webServer: { 9 | command: 'pnpm --filter web-api-demo dev', 10 | url: 'http://localhost:5173', 11 | reuseExistingServer: !process.env.CI, 12 | }, 13 | }; 14 | 15 | export default config; 16 | -------------------------------------------------------------------------------- /apps/website/docs/statements/tests.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | We believe that the only way to write good code is to test it. It leads to the following consequences: 4 | 5 | 1. Any feature in any library listed at With Ease are created with testing in mind, so users do not have to invent some complex ways to test their code. 6 | 2. Libraries are tested itself. It means that you can be sure that it works as expected. We are not accepting any pull requests that do not have tests. 7 | -------------------------------------------------------------------------------- /tools/publint.mjs: -------------------------------------------------------------------------------- 1 | import { publint } from 'publint'; 2 | import { formatMessage } from 'publint/utils'; 3 | import fs from 'node:fs/promises'; 4 | 5 | const { messages } = await publint({ 6 | pkgDir: '.', 7 | strict: true, 8 | }); 9 | 10 | if (messages.length) { 11 | const pkg = await fs.readFile('package.json', 'utf8').then(JSON.parse); 12 | 13 | for (const message of messages) { 14 | console.log(formatMessage(message, pkg)); 15 | } 16 | process.exit(1); 17 | } 18 | -------------------------------------------------------------------------------- /packages/contracts/vite.config.js: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from 'vite-tsconfig-paths'; 2 | import dts from '../../tools/vite/types'; 3 | 4 | export default { 5 | test: { 6 | typecheck: { 7 | ignoreSourceErrors: true, 8 | }, 9 | }, 10 | plugins: [tsconfigPaths(), dts()], 11 | build: { 12 | lib: { 13 | entry: 'src/index.ts', 14 | name: '@withease/contracts', 15 | fileName: 'contracts', 16 | formats: ['es', 'cjs'], 17 | }, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /packages/factories/vite.config.js: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from 'vite-tsconfig-paths'; 2 | import dts from '../../tools/vite/types'; 3 | 4 | export default { 5 | test: { 6 | typecheck: { 7 | ignoreSourceErrors: true, 8 | }, 9 | }, 10 | plugins: [tsconfigPaths(), dts()], 11 | build: { 12 | lib: { 13 | entry: 'src/index.ts', 14 | name: '@withease/factories', 15 | fileName: 'factories', 16 | formats: ['es', 'cjs'], 17 | }, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /apps/web-api-demo/src/preferred-languages.ts: -------------------------------------------------------------------------------- 1 | import { trackPreferredLanguages } from '@withease/web-api'; 2 | import { createEvent } from 'effector'; 3 | 4 | const appStarted = createEvent(); 5 | 6 | const languageElement = document.querySelector('#language')!; 7 | 8 | const { $language } = trackPreferredLanguages({ setup: appStarted }); 9 | 10 | $language.watch((language) => { 11 | console.log('language', language); 12 | languageElement.textContent = language; 13 | }); 14 | 15 | appStarted(); 16 | -------------------------------------------------------------------------------- /packages/factories/src/errors.ts: -------------------------------------------------------------------------------- 1 | export function factoryCalledDirectly() { 2 | return new Error( 3 | 'Do not call factory directly, pass it to invoke function instead' 4 | ); 5 | } 6 | 7 | export function invokeAcceptsOnlyFactories() { 8 | return new Error('Function passed to invoke is not created by createFactory'); 9 | } 10 | 11 | export function factoryHasMoreThanOneArgument() { 12 | return new Error( 13 | 'createFactory does not support functions with more than 1 argument' 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /apps/web-api-demo/test/preferred_languages.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | const PREFERRED_LANGUAGES_PAGE = '/preferred-languages.html'; 4 | 5 | test.use({ locale: 'it-IT' }); 6 | 7 | test('should detect initial language', async ({ page }) => { 8 | await page.goto(PREFERRED_LANGUAGES_PAGE); 9 | 10 | const languageContainer = await page.$('#language'); 11 | const languageTextContent = await languageContainer.textContent(); 12 | 13 | expect(languageTextContent).toBe('it-IT'); 14 | }); 15 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /apps/website/docs/i18next/reporting.md: -------------------------------------------------------------------------------- 1 | # `reporting` 2 | 3 | An object with the following fields: 4 | 5 | - `missingKey`, [_Event_](https://effector.dev/en/api/effector/event/) will be triggered when a key is missing in the translation resources, requires [adding `saveMissing` option to the i18next instance](https://www.i18next.com/overview/api#onmissingkey). 6 | 7 | ```ts 8 | const { reporting } = createI18nextIntegration({ 9 | /* ... */ 10 | }); 11 | 12 | sample({ clock: reporting.missingKey, target: sendWarningToSentry }); 13 | ``` 14 | -------------------------------------------------------------------------------- /.github/workflows/changes.yml: -------------------------------------------------------------------------------- 1 | name: Changes 2 | 3 | on: 4 | pull_request: 5 | branches: [master, next-*] 6 | 7 | jobs: 8 | changeset: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | with: 14 | fetch-depth: 0 15 | - uses: pnpm/action-setup@v2 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version-file: '.nvmrc' 19 | cache: 'pnpm' 20 | - run: pnpm install --frozen-lockfile 21 | - run: pnpm pnpm changeset status 22 | -------------------------------------------------------------------------------- /packages/web-api/vite.config.js: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from 'vite-tsconfig-paths'; 2 | import dts from '../../tools/vite/types'; 3 | 4 | export default { 5 | test: { 6 | typecheck: { 7 | ignoreSourceErrors: true, 8 | }, 9 | }, 10 | plugins: [tsconfigPaths(), dts()], 11 | build: { 12 | lib: { 13 | entry: 'src/index.ts', 14 | name: '@withease/web-api', 15 | fileName: 'web-api', 16 | formats: ['es', 'cjs'], 17 | }, 18 | rollupOptions: { 19 | external: ['effector'], 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /packages/redux/vite.config.js: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from 'vite-tsconfig-paths'; 2 | import dts from '../../tools/vite/types'; 3 | 4 | export default { 5 | test: { 6 | typecheck: { 7 | ignoreSourceErrors: true, 8 | }, 9 | }, 10 | plugins: [tsconfigPaths(), dts()], 11 | build: { 12 | lib: { 13 | entry: 'src/index.ts', 14 | name: '@withease/redux', 15 | fileName: 'redux', 16 | formats: ['es', 'cjs'], 17 | }, 18 | rollupOptions: { 19 | external: ['effector', 'redux'], 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /apps/website/docs/statements/typescript.md: -------------------------------------------------------------------------------- 1 | # TypeScript 2 | 3 | All libraries listen at With Ease are going to provide first-class support of TypeScript types. They are written in TypeScript, and includes type-tests. However, TypeScript itself does not aim for [apply a sound or "provably correct" type system](https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals#non-goals), instead it strikes a balance between correctness and productivity. So, we cannot guarantee that all the types are correct, but we are going to do our best to provide the best possible types. 4 | -------------------------------------------------------------------------------- /packages/i18next/vite.config.js: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from 'vite-tsconfig-paths'; 2 | import dts from '../../tools/vite/types'; 3 | 4 | export default { 5 | test: { 6 | typecheck: { 7 | ignoreSourceErrors: true, 8 | }, 9 | }, 10 | plugins: [tsconfigPaths(), dts()], 11 | build: { 12 | lib: { 13 | entry: 'src/index.ts', 14 | name: '@withease/i18next', 15 | fileName: 'i18next', 16 | formats: ['es', 'cjs'], 17 | }, 18 | rollupOptions: { 19 | external: ['effector', 'i18next'], 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /apps/website/docs/web-api/geolocation.live.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 20 | -------------------------------------------------------------------------------- /apps/website/docs/i18next/releases.md: -------------------------------------------------------------------------------- 1 | # Releases policy of `@withease/i18next` 2 | 3 | ::: tip 4 | This library does not follow the same [release cycle as other libraries in the With Ease organization](/statements/releases) because it is bound to the release cycle of `i18next` itself. 5 | ::: 6 | 7 | The major version of this library is bound to the major version of `i18next` itself. For example, `@withease/i18next@22.x.x` is compatible with `i18next@22.x.x` and is tested against it. 8 | 9 | Major version of this library can include breaking changes, but it will be soundness. 10 | -------------------------------------------------------------------------------- /apps/web-api-demo/preferred-languages.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | web-api demo 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Preferred languages

14 |

Language:

15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/web-api-demo/network.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | web-api demo 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Network

14 |

Online:

15 |

Offline:

16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /apps/website/docs/web-api/preferred_languages.live.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /apps/web-api-demo/media-query.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | web-api demo 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Media

14 |

Mobile:

15 |

Desktop:

16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /apps/web-api-demo/page-visibility.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | web-api demo 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Page visibility

14 |

Visible:

15 |

Hidden:

16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /apps/website/docs/web-api/media_query.live.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 22 | -------------------------------------------------------------------------------- /apps/web-api-demo/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by: 2 | # 1. autoprefixer to adjust CSS to support the below specified browsers 3 | # 2. babel preset-env to adjust included polyfills 4 | # 5 | # For additional information regarding the format and rule options, please see: 6 | # https://github.com/browserslist/browserslist#queries 7 | # 8 | # If you need to support different browsers in production, you may tweak the list below. 9 | 10 | last 1 Chrome version 11 | last 1 Firefox version 12 | last 2 Edge major versions 13 | last 2 Safari major version 14 | last 2 iOS major versions 15 | Firefox ESR 16 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: pnpm/action-setup@v2 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version-file: '.nvmrc' 17 | cache: 'pnpm' 18 | - run: pnpm install --frozen-lockfile 19 | - run: | 20 | echo "//registry.npmjs.org/:_authToken="${{secrets.NPM_TOKEN}}"" > ~/.npmrc 21 | shell: sh 22 | - run: pnpm run -r build 23 | - run: pnpm -r publish --no-git-checks 24 | -------------------------------------------------------------------------------- /apps/website/docs/web-api/index.md: -------------------------------------------------------------------------------- 1 | # web-api 2 | 3 | Web API bindings — network status, tab visibility, and more 4 | 5 | ## Installation 6 | 7 | First, you need to install integration: 8 | 9 | ::: code-group 10 | 11 | ```sh [pnpm] 12 | pnpm install @withease/web-api 13 | ``` 14 | 15 | ```sh [yarn] 16 | yarn add @withease/web-api 17 | ``` 18 | 19 | ```sh [npm] 20 | npm install @withease/web-api 21 | ``` 22 | 23 | ::: 24 | 25 | ## Available integrations 26 | 27 | 30 | 31 | 34 | -------------------------------------------------------------------------------- /.github/workflows/version.yml: -------------------------------------------------------------------------------- 1 | name: Version 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | concurrency: ${{ github.workflow }}-${{ github.ref }} 8 | 9 | jobs: 10 | version: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: pnpm/action-setup@v2 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version-file: '.nvmrc' 18 | cache: 'pnpm' 19 | - run: pnpm install --frozen-lockfile 20 | 21 | - name: Create Version Pull Request 22 | uses: changesets/action@v1 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | -------------------------------------------------------------------------------- /apps/website/docs/web-api/network_status.live.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 24 | -------------------------------------------------------------------------------- /apps/web-api-demo/src/page-visibility.ts: -------------------------------------------------------------------------------- 1 | import { trackPageVisibility } from '@withease/web-api'; 2 | import { createEvent } from 'effector'; 3 | 4 | const appStarted = createEvent(); 5 | 6 | const visibleElement = document.querySelector('#visible')!; 7 | const hiddenElement = document.querySelector('#hidden')!; 8 | 9 | const page = trackPageVisibility({ setup: appStarted }); 10 | 11 | page.$visible.watch((visible) => { 12 | console.log('visible', visible); 13 | visibleElement.textContent = JSON.stringify(visible); 14 | }); 15 | page.$hidden.watch((hidden) => { 16 | console.log('hidden', hidden); 17 | hiddenElement.textContent = JSON.stringify(hidden); 18 | }); 19 | 20 | appStarted(); 21 | -------------------------------------------------------------------------------- /apps/website/docs/magazine/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar: false 3 | rss: false 4 | --- 5 | 6 | # Magazine 7 | 8 | The collection of articles about Effector and related topics. It is not a replacement for the [official documentation](https://effector.dev), but it can help you to understand some concepts better. 9 | 10 | ::: tip 11 | You can read With Ease Magazine in the [RSS format](/feed.rss). 12 | ::: 13 | 14 | 17 | 18 |
19 |

{{ category.text }}

20 | 23 |
24 | -------------------------------------------------------------------------------- /apps/web-api-demo/src/network.ts: -------------------------------------------------------------------------------- 1 | import { trackNetworkStatus } from '@withease/web-api'; 2 | import { createEvent } from 'effector'; 3 | 4 | const appStarted = createEvent(); 5 | 6 | // Network status 7 | 8 | const network = trackNetworkStatus({ setup: appStarted }); 9 | 10 | const onlineElement = document.querySelector('#online')!; 11 | const offlineElement = document.querySelector('#offline')!; 12 | 13 | network.$online.watch((online) => { 14 | console.log('online', online); 15 | onlineElement.textContent = JSON.stringify(online); 16 | }); 17 | network.$offline.watch((offline) => { 18 | console.log('offline', offline); 19 | offlineElement.textContent = JSON.stringify(offline); 20 | }); 21 | 22 | appStarted(); 23 | -------------------------------------------------------------------------------- /apps/web-api-demo/screen-orientation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | web-api demo 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Screen orientation

14 |

Type:

15 |

Angle:

16 |

Portrait:

17 |

Landscape:

18 |
19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /apps/website/docs/protocols/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar: false 3 | --- 4 | 5 | # Protocols 6 | 7 | Protocols are agreements between two or more parties on how to connect and communicate with each other. They are the foundation of the Effector's ecosystem. They allow you to connect different parts of the ecosystem with each other without direct dependencies. 8 | 9 | Some protocols are created by the Effector's committee, and some are provided by the community. The following list contains all known protocols: 10 | 11 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /packages/contracts/src/contract.test-d.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expectTypeOf } from 'vitest'; 2 | 3 | import { and, Contract, num, obj, str } from './index'; 4 | 5 | describe('and', () => { 6 | test('inline contract', () => { 7 | const contract = and(num, { 8 | isData: (data): data is number => data > 0, 9 | getErrorMessages: () => ['data must be greater than 0'], 10 | }); 11 | 12 | expectTypeOf(contract).toEqualTypeOf>(); 13 | }); 14 | 15 | test('as extends', () => { 16 | const contract = and(obj({ name: str }), obj({ age: num })); 17 | 18 | expectTypeOf(contract).toEqualTypeOf< 19 | Contract 20 | >(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tools/vite/types.js: -------------------------------------------------------------------------------- 1 | import dts from 'vite-plugin-dts'; 2 | import { readdir, copyFile } from 'node:fs/promises'; 3 | import * as path from 'node:path'; 4 | 5 | export default function typesPlugin() { 6 | return dts({ 7 | entryRoot: 'src', 8 | tsConfigFilePath: 'tsconfig.json', 9 | skipDiagnostics: true, 10 | rollupTypes: true, 11 | async afterBuild() { 12 | const files = await readdir('dist'); 13 | const dtsFiles = files.filter((file) => file.endsWith('.d.ts')); 14 | await Promise.all( 15 | dtsFiles.map((file) => 16 | copyFile( 17 | path.join('dist', file), 18 | path.join('dist', file.replace('.d.ts', '.d.cts')) 19 | ) 20 | ) 21 | ); 22 | }, 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /apps/website/docs/ecosystem.ts: -------------------------------------------------------------------------------- 1 | export const ecosystem = [ 2 | { 3 | icon: '🫙', 4 | title: 'effector-storage', 5 | details: 'Sync Stores with any external storage (like localStorage)', 6 | link: 'https://github.com/yumauri/effector-storage', 7 | linkText: 'Learn More', 8 | }, 9 | { 10 | icon: '🪄', 11 | title: 'Farfetched', 12 | details: 'The advanced data fetching tool for web applications', 13 | link: 'https://ff.effector.dev', 14 | linkText: 'Learn More', 15 | }, 16 | { 17 | icon: '🪞', 18 | title: 'reflect', 19 | details: 20 | 'API for bind Effector to React components in an efficient and composable way', 21 | link: 'https://github.com/effector/reflect', 22 | linkText: 'Learn More', 23 | }, 24 | ]; 25 | -------------------------------------------------------------------------------- /apps/web-api-demo/src/media-query.ts: -------------------------------------------------------------------------------- 1 | import { trackMediaQuery } from '@withease/web-api'; 2 | import { createEvent } from 'effector'; 3 | 4 | const appStarted = createEvent(); 5 | 6 | const mobileElement = document.querySelector('#mobile')!; 7 | const desktopElement = document.querySelector('#desktop')!; 8 | 9 | const { mobile, desktop } = trackMediaQuery( 10 | { desktop: '(min-width: 601px)', mobile: '(max-width: 600px)' }, 11 | { setup: appStarted } 12 | ); 13 | 14 | mobile.$matches.watch((active) => { 15 | console.log('mobile', active); 16 | mobileElement.textContent = JSON.stringify(active); 17 | }); 18 | desktop.$matches.watch((inactive) => { 19 | console.log('desktop', inactive); 20 | desktopElement.textContent = JSON.stringify(inactive); 21 | }); 22 | 23 | appStarted(); 24 | -------------------------------------------------------------------------------- /apps/website/docs/contracts/cookbook/merge_objects.md: -------------------------------------------------------------------------------- 1 | # Merge Objects 2 | 3 | Merge two [_Contracts_](/protocols/contract) representing objects into a single [_Contract_](/protocols/contract) representing an object with fields from both input objects is a common operation in many applications. 4 | 5 | With `@withease/contracts` in can be done with simple `and` call: 6 | 7 | ```ts 8 | import { num, str, obj, and, type UnContract } from '@withease/contracts'; 9 | 10 | const Price = obj({ 11 | currency: str, 12 | value: num, 13 | }); 14 | 15 | const PriceWithDiscount = and( 16 | Price, 17 | obj({ 18 | discount: num, 19 | }) 20 | ); 21 | 22 | type TPriceWithDiscount = UnContract; 23 | // 👆 { currency: string, value: number, discount: number } 24 | ``` 25 | -------------------------------------------------------------------------------- /apps/web-api-demo/geolocation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | web-api demo 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Geolocation

14 | 15 |

latitude:

16 |

longitude:

17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "composite": true, 5 | "sourceMap": true, 6 | "declaration": true, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": false, 11 | "target": "es2018", 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "skipDefaultLibCheck": true, 15 | "paths": { 16 | "@withease/factories": ["packages/factories/src/index.ts"], 17 | "@withease/i18next": ["packages/i18next/src/index.ts"], 18 | "@withease/redux": ["packages/redux/src/index.ts"], 19 | "@withease/web-api": ["packages/web-api/src/index.ts"], 20 | "@withease/contracts": ["packages/contracts/src/index.ts"] 21 | } 22 | }, 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /apps/web-api-demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | web-api demo 6 | 7 | 8 | 9 | 10 | 11 | 12 |

@withease/web-api

13 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /apps/website/docs/contracts/cookbook/custom_matchers.md: -------------------------------------------------------------------------------- 1 | # Custom Matchers 2 | 3 | Since `@withease/contracts` is built on top of [_Contract_](/protocols/contract), you can embed your own matcher into the schema naturally. 4 | 5 | Let us write a custom matcher that checks if an age of a user is within a certain range: 6 | 7 | ```ts 8 | import { type Contract, and, num } from '@withease/contracts'; 9 | 10 | function age({ min, max }: { min: number; max: number }) { 11 | return and(num, { 12 | isData: (data) => data >= min && data <= max, 13 | getErrorMessages: (data) => [ 14 | `Expected a number between ${min} and ${max}, but got ${data}`, 15 | ], 16 | }); 17 | } 18 | ``` 19 | 20 | Now you can use this matcher in your schema: 21 | 22 | ```ts 23 | import { obj, str } from '@withease/contracts'; 24 | 25 | const User = obj({ 26 | name: str, 27 | age: age(18, 100), 28 | }); 29 | ``` 30 | -------------------------------------------------------------------------------- /packages/factories/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @withease/factories 2 | 3 | ## 1.0.5 4 | 5 | ### Patch Changes 6 | 7 | - c3a3bb4: Update toolchain 8 | 9 | ## 1.0.4 10 | 11 | ### Patch Changes 12 | 13 | - a6ac670: Add missed license field to package.json 14 | 15 | ## 1.0.3 16 | 17 | ### Patch Changes 18 | 19 | - 2308868: Fix false-positive exception in case of many nexted factories 20 | 21 | ## 1.0.2 22 | 23 | ### Patch Changes 24 | 25 | - 1071cf6: Throw error for nested un-invoked factories anyway 26 | 27 | ## 1.0.1 28 | 29 | ### Patch Changes 30 | 31 | - e2dad7f: Fix ESM/CJS builds 32 | 33 | ## 1.0.0 34 | 35 | - Nothing new, just a version bump to mark library as stable 36 | 37 | ## 0.2.0 38 | 39 | ### Minor Changes 40 | 41 | - a5f0c3e: Add escape hatch for `Type instantiation is excessively deep and possibly infinite` 42 | 43 | ## 0.1.0 44 | 45 | ### Minor Changes 46 | 47 | - b7b4730: Initial release 48 | -------------------------------------------------------------------------------- /apps/website/docs/statements/releases.md: -------------------------------------------------------------------------------- 1 | # Releases policy 2 | 3 | The main goal of With Ease libraries is to **make developer experience better**, as a part of this strategy we are committing to some rules of releases. 4 | 5 | ## Stable releases 6 | 7 | After the first stable release, we will maintain **backward compatibility for 2 years** in any library. Of course, we can introduce some breaking changes, but we commit that any breaking change will be prepended by deprecation warning at least for one year. 8 | 9 | ::: tip 10 | Some libraries can have different rules of releases, but it will be described in the documentation of the library. For example, `@withease/i18next` release cycle is bound to the release cycle of `i18next` itself. 11 | ::: 12 | 13 | ## 0.x.x releases 14 | 15 | Until API would be stabilized, we are going to release versions 0.x.x as pre-releases. Each version can include breaking changes, but it will be soundness. 16 | -------------------------------------------------------------------------------- /packages/factories/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@withease/factories", 3 | "version": "1.0.5", 4 | "license": "MIT", 5 | "scripts": { 6 | "test:run": "vitest run --typecheck", 7 | "build": "vite build", 8 | "size": "size-limit", 9 | "publint": "node ../../tools/publint.mjs", 10 | "typelint": "attw --pack" 11 | }, 12 | "type": "module", 13 | "files": [ 14 | "dist" 15 | ], 16 | "main": "./dist/factories.cjs", 17 | "module": "./dist/factories.js", 18 | "types": "./dist/factories.d.ts", 19 | "exports": { 20 | ".": { 21 | "import": { 22 | "types": "./dist/factories.d.ts", 23 | "default": "./dist/factories.js" 24 | }, 25 | "require": { 26 | "types": "./dist/factories.d.cts", 27 | "default": "./dist/factories.cjs" 28 | } 29 | } 30 | }, 31 | "size-limit": [ 32 | { 33 | "path": "./dist/factories.js", 34 | "limit": "356 B" 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /apps/website/docs/web-api/page_visibility.live.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 31 | -------------------------------------------------------------------------------- /apps/website/docs/contracts/array_numbers.live.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 33 | -------------------------------------------------------------------------------- /apps/website/docs/web-api/screen_orientation.live.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 37 | -------------------------------------------------------------------------------- /apps/website/scripts/changelog.mjs: -------------------------------------------------------------------------------- 1 | import glob from 'glob'; 2 | import { readFile, writeFile } from 'node:fs/promises'; 3 | import { promisify } from 'node:util'; 4 | import { markdown } from 'markdown'; 5 | import { resolve } from 'node:path'; 6 | 7 | const files = await promisify(glob)( 8 | '../../{packages,deleted_packages}/*/CHANGELOG.md', 9 | { 10 | absolute: true, 11 | } 12 | ); 13 | 14 | const changelogs = await Promise.all( 15 | files.map((file) => 16 | readFile(file, 'UTF-8') 17 | .then((content) => content.toString()) 18 | .then(parseChangelog) 19 | ) 20 | ); 21 | 22 | for (const { name, content } of changelogs) { 23 | const filePath = resolve('docs', name, 'CHANGELOG.md'); 24 | 25 | await writeFile(filePath, content); 26 | } 27 | 28 | // --- // --- 29 | 30 | async function parseChangelog(content) { 31 | const [_1, header] = markdown.parse(content); 32 | 33 | const name = header.at(2).replace('@withease/', ''); 34 | 35 | return { name, content }; 36 | } 37 | -------------------------------------------------------------------------------- /apps/website/docs/i18next/t.md: -------------------------------------------------------------------------------- 1 | # `$t` 2 | 3 | A [_Store_](https://effector.dev/docs/api/effector/store) containing a [translation function](https://www.i18next.com/overview/api#t), can be used anywhere in your app. 4 | 5 | ```ts 6 | const { $t } = createI18nextIntegration({ 7 | /* ... */ 8 | }); 9 | 10 | const $someTranslatedString = $t.map((t) => t('cityPois.buttonText')); 11 | ``` 12 | 13 | The second argument is an optional object with options for the translation function. 14 | 15 | ```ts 16 | const $city = createStore({ name: 'Moscow' }); 17 | 18 | const { $t } = createI18nextIntegration({ 19 | /* ... */ 20 | }); 21 | 22 | const $someTranslatedString = combine({ city: $city, t: $t }, ({ city, t }) => 23 | t('cityPois.buttonText', { 24 | cityName: city.name, 25 | }) 26 | ); 27 | ``` 28 | 29 | In both cases, result will be a [_Store_](https://effector.dev/docs/api/effector/store) containing a translated string. It will be updated automatically when the language or available translations will be changed. 30 | -------------------------------------------------------------------------------- /packages/redux/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @withease/redux 2 | 3 | ## 1.1.2 4 | 5 | ### Patch Changes 6 | 7 | - c3a3bb4: Update toolchain 8 | 9 | ## 1.1.1 10 | 11 | ### Patch Changes 12 | 13 | - a6ac670: Add missed license field to package.json 14 | 15 | ## 1.1.0 16 | 17 | ### Minor Changes 18 | 19 | - e08a5d6: Add a new overload for the `createReduxIntegration` - without explicit `reduxStore`, which allows you to pass the Store via `setup` _Event_ later. 20 | 21 | This helps to avoid dependency cycles, but at a cost: 22 | The type support for `reduxInterop.$state` will be slightly worse and `reduxInterop.dispatch` will be no-op (and will show warnings in console) until interop object is provided with Redux Store. 23 | 24 | ## 1.0.2 25 | 26 | ### Patch Changes 27 | 28 | - 72b097b: Fix \$reduxStore public type 29 | 30 | ## 1.0.1 31 | 32 | ### Patch Changes 33 | 34 | - 6fa3703: Fixed public types generation 35 | 36 | ## 1.0.0 37 | 38 | ### Major Changes 39 | 40 | - 0e84e06: Initial release of Redux interop package for Effector 41 | -------------------------------------------------------------------------------- /packages/redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@withease/redux", 3 | "version": "1.1.2", 4 | "license": "MIT", 5 | "peerDependencies": { 6 | "effector": "^22.8.8 || ^23.0.0", 7 | "redux": "^4.0.0 || ^5.0.0" 8 | }, 9 | "scripts": { 10 | "test:run": "vitest run --typecheck", 11 | "build": "vite build", 12 | "size": "size-limit", 13 | "publint": "node ../../tools/publint.mjs", 14 | "typelint": "attw --pack" 15 | }, 16 | "type": "module", 17 | "files": [ 18 | "dist" 19 | ], 20 | "main": "./dist/redux.cjs", 21 | "module": "./dist/redux.js", 22 | "types": "./dist/redux.d.ts", 23 | "exports": { 24 | ".": { 25 | "import": { 26 | "types": "./dist/redux.d.ts", 27 | "default": "./dist/redux.js" 28 | }, 29 | "require": { 30 | "types": "./dist/redux.d.cts", 31 | "default": "./dist/redux.cjs" 32 | } 33 | } 34 | }, 35 | "size-limit": [ 36 | { 37 | "path": "./dist/redux.js", 38 | "limit": "569 B" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /apps/web-api-demo/src/geolocation.ts: -------------------------------------------------------------------------------- 1 | import { trackGeolocation } from '@withease/web-api'; 2 | 3 | const latitudeElement = document.querySelector('#latitude')!; 4 | const longitudeElement = document.querySelector('#longitude')!; 5 | const getLocationButton = document.querySelector('#get-location')!; 6 | const startWatchingButton = document.querySelector('#start-watching')!; 7 | const stopWatchingButton = document.querySelector('#stop-watching')!; 8 | 9 | const { $latitude, $longitude, request, watching } = trackGeolocation({}); 10 | 11 | $latitude.watch((latitude) => { 12 | console.log('latitude', latitude); 13 | latitudeElement.textContent = JSON.stringify(latitude); 14 | }); 15 | $longitude.watch((longitude) => { 16 | console.log('longitude', longitude); 17 | longitudeElement.textContent = JSON.stringify(longitude); 18 | }); 19 | 20 | getLocationButton.addEventListener('click', () => request()); 21 | startWatchingButton.addEventListener('click', () => watching.start()); 22 | stopWatchingButton.addEventListener('click', () => watching.stop()); 23 | -------------------------------------------------------------------------------- /apps/website/docs/factories/important_caveats.md: -------------------------------------------------------------------------------- 1 | # Important caveats 2 | 3 | This library adds some limitations to factories created using `createFactory` function. 4 | 5 | ## Single argument 6 | 7 | Factories created by `createFactory` function accept only one argument. If you need to pass multiple arguments to a factory, you have to wrap them in an object. 8 | 9 | ```js 10 | import { createFactory } from '@withease/factories'; 11 | 12 | const someFactory = createFactory(({ arg1, arg2 }) => { 13 | // ... 14 | }); 15 | ``` 16 | 17 | ## Type instantiation is excessively deep and possibly infinite 18 | 19 | In some cases, TypeScript may throw an error like this: 20 | 21 | ``` 22 | Type instantiation is excessively deep and possibly infinite 23 | ``` 24 | 25 | It happens when you try to create a factory with a complex type. For this case, you can use inline function declaration in `invoke` 26 | 27 | ```ts 28 | const myFactory = createFactory(/* complex types there */); 29 | 30 | const value = invoke(() => myFactory(/* complex argument there */)); 31 | ``` 32 | -------------------------------------------------------------------------------- /packages/web-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@withease/web-api", 3 | "version": "1.3.0", 4 | "license": "MIT", 5 | "peerDependencies": { 6 | "effector": "^22.5.0 || ^23.0.0" 7 | }, 8 | "scripts": { 9 | "test:run": "vitest run --typecheck", 10 | "test:watch": "vitest --typecheck", 11 | "build": "vite build", 12 | "size": "size-limit", 13 | "publint": "node ../../tools/publint.mjs", 14 | "typelint": "attw --pack" 15 | }, 16 | "type": "module", 17 | "files": [ 18 | "dist" 19 | ], 20 | "main": "./dist/web-api.cjs", 21 | "module": "./dist/web-api.js", 22 | "types": "./dist/web-api.d.ts", 23 | "exports": { 24 | ".": { 25 | "import": { 26 | "types": "./dist/web-api.d.ts", 27 | "default": "./dist/web-api.js" 28 | }, 29 | "require": { 30 | "types": "./dist/web-api.d.cts", 31 | "default": "./dist/web-api.cjs" 32 | } 33 | } 34 | }, 35 | "size-limit": [ 36 | { 37 | "path": "./dist/web-api.js", 38 | "limit": "2.49 kB" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /packages/contracts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@withease/contracts", 3 | "version": "1.1.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "test:run": "vitest run --typecheck", 7 | "test:watch": "vitest --typecheck", 8 | "build": "vite build", 9 | "size": "size-limit", 10 | "publint": "node ../../tools/publint.mjs", 11 | "typelint": "attw --pack" 12 | }, 13 | "type": "module", 14 | "publishConfig": { 15 | "access": "public" 16 | }, 17 | "files": [ 18 | "dist" 19 | ], 20 | "main": "./dist/contracts.cjs", 21 | "module": "./dist/contracts.js", 22 | "types": "./dist/contracts.d.ts", 23 | "exports": { 24 | ".": { 25 | "import": { 26 | "types": "./dist/contracts.d.ts", 27 | "default": "./dist/contracts.js" 28 | }, 29 | "require": { 30 | "types": "./dist/contracts.d.cts", 31 | "default": "./dist/contracts.cjs" 32 | } 33 | } 34 | }, 35 | "size-limit": [ 36 | { 37 | "path": "./dist/contracts.js", 38 | "limit": "831 B" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /packages/i18next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@withease/i18next", 3 | "version": "25.0.0", 4 | "license": "MIT", 5 | "peerDependencies": { 6 | "effector": "^22.5.0 || ^23.0.0", 7 | "i18next": "^23.0.0 || ^24.0.0 || ^25.0.0" 8 | }, 9 | "scripts": { 10 | "test:run": "vitest run --typecheck", 11 | "build": "vite build", 12 | "size": "size-limit", 13 | "publint": "node ../../tools/publint.mjs", 14 | "typelint": "attw --pack" 15 | }, 16 | "type": "module", 17 | "files": [ 18 | "dist" 19 | ], 20 | "main": "./dist/i18next.cjs", 21 | "module": "./dist/i18next.js", 22 | "types": "./dist/i18next.d.ts", 23 | "exports": { 24 | ".": { 25 | "import": { 26 | "types": "./dist/i18next.d.ts", 27 | "default": "./dist/i18next.js" 28 | }, 29 | "require": { 30 | "types": "./dist/i18next.d.cts", 31 | "default": "./dist/i18next.cjs" 32 | } 33 | } 34 | }, 35 | "size-limit": [ 36 | { 37 | "path": "./dist/i18next.js", 38 | "limit": "1.33 kB" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /packages/factories/src/create_factory.ts: -------------------------------------------------------------------------------- 1 | import { factoryCalledDirectly, factoryHasMoreThanOneArgument } from './errors'; 2 | import { markFactoryAsCalled, invokeLevel } from './invoke'; 3 | 4 | export function createFactory any>(creator: C): C { 5 | /* 6 | * DX improvement for JS-users who do not get TS error 7 | * when pass function with more than 1 argument 8 | */ 9 | if (creator.length > 1) { 10 | throw factoryHasMoreThanOneArgument(); 11 | } 12 | 13 | const create = (params: any) => { 14 | /* 15 | * DX improvement for users who try to call factory directly without invoke 16 | */ 17 | if (invokeLevel === 0) { 18 | throw factoryCalledDirectly(); 19 | } 20 | 21 | const value = creator(params); 22 | 23 | /* 24 | * It is important to call markFactoryAsCalled after creator call 25 | * because invoke function checks this flag to throw an error 26 | * if called function is not created by createFactory 27 | */ 28 | markFactoryAsCalled(); 29 | 30 | return value; 31 | }; 32 | 33 | return create as C; 34 | } 35 | -------------------------------------------------------------------------------- /tools/other-majors/prepare.mjs: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from 'node:fs/promises'; 2 | 3 | import manifest from './manifest.json' assert { type: 'json' }; 4 | 5 | const [, , versoin] = process.argv; 6 | 7 | const oldPackageJson = await readFile('package.json').then((file) => 8 | JSON.parse(file.toString()) 9 | ); 10 | 11 | const newVersions = manifest[versoin]; 12 | 13 | if (!newVersions) { 14 | throw new Error(`No versions found for ${versoin}`); 15 | } 16 | 17 | await writeFile( 18 | 'package.json', 19 | JSON.stringify( 20 | { 21 | ...oldPackageJson, 22 | devDependencies: applyNewVersions( 23 | oldPackageJson.devDependencies, 24 | newVersions 25 | ), 26 | dependencies: applyNewVersions(oldPackageJson.dependencies, newVersions), 27 | }, 28 | null, 29 | 2 30 | ) 31 | ); 32 | 33 | function applyNewVersions(deps, newVersions) { 34 | return Object.entries(deps).reduce((acc, [key, value]) => { 35 | if (newVersions[key]) { 36 | return { ...acc, [key]: newVersions[key] }; 37 | } 38 | 39 | return { ...acc, [key]: value }; 40 | }, {}); 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Igor Kamyşev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/web-api-demo/src/screen-orientation.ts: -------------------------------------------------------------------------------- 1 | import { trackScreenOrientation } from '@withease/web-api'; 2 | import { createEvent } from 'effector'; 3 | 4 | const appStarted = createEvent(); 5 | 6 | const typeElement = document.querySelector('#type')!; 7 | const angleElement = document.querySelector('#angle')!; 8 | const portraitElement = document.querySelector('#portrait')!; 9 | const landscapeElement = document.querySelector('#landscape')!; 10 | 11 | const { $type, $angle, $landscape, $portrait } = trackScreenOrientation({ 12 | setup: appStarted, 13 | }); 14 | 15 | $type.watch((type) => { 16 | console.log('type', type); 17 | typeElement.textContent = JSON.stringify(type); 18 | }); 19 | $angle.watch((angle) => { 20 | console.log('angle', angle); 21 | angleElement.textContent = JSON.stringify(angle); 22 | }); 23 | $landscape.watch((landscape) => { 24 | console.log('landscape', landscape); 25 | landscapeElement.textContent = JSON.stringify(landscape); 26 | }); 27 | $portrait.watch((portrait) => { 28 | console.log('portrait', portrait); 29 | portraitElement.textContent = JSON.stringify(portrait); 30 | }); 31 | 32 | appStarted(); 33 | -------------------------------------------------------------------------------- /apps/web-api-demo/test/media_query.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | const MEDIA_QUERY_PAGE = '/media-query.html'; 4 | 5 | test('should detect mobile viewport', async ({ page }) => { 6 | await page.goto(MEDIA_QUERY_PAGE); 7 | await page.setViewportSize({ width: 500, height: 1000 }); 8 | 9 | const mobileContainer = await page.$('#mobile'); 10 | const mobileTextContent = await mobileContainer.textContent(); 11 | 12 | const desktopContainer = await page.$('#desktop'); 13 | const desktopTextContent = await desktopContainer.textContent(); 14 | 15 | expect(mobileTextContent).toBe('true'); 16 | expect(desktopTextContent).toBe('false'); 17 | }); 18 | 19 | test('should detect desktop viewport', async ({ page }) => { 20 | await page.goto(MEDIA_QUERY_PAGE); 21 | await page.setViewportSize({ width: 1000, height: 1000 }); 22 | 23 | const mobileContainer = await page.$('#mobile'); 24 | const mobileTextContent = await mobileContainer.textContent(); 25 | 26 | const desktopContainer = await page.$('#desktop'); 27 | const desktopTextContent = await desktopContainer.textContent(); 28 | 29 | expect(mobileTextContent).toBe('false'); 30 | expect(desktopTextContent).toBe('true'); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/web-api/src/preferred_languages.test.ts: -------------------------------------------------------------------------------- 1 | import { allSettled, createEvent, fork } from 'effector'; 2 | import { describe, test, expect } from 'vitest'; 3 | 4 | import { trackPreferredLanguages } from './preferred_languages'; 5 | 6 | describe('trackPreferredLanguages on server', () => { 7 | const setup = createEvent(); 8 | 9 | const { $languages } = trackPreferredLanguages({ setup }); 10 | 11 | test('do nothing for empty header', async () => { 12 | const scope = fork(); 13 | 14 | await allSettled(setup, { scope }); 15 | 16 | expect(scope.getState($languages)).toEqual([]); 17 | }); 18 | 19 | test.each([ 20 | { header: 'en-US,en;q=0.9', result: ['en-US', 'en'] }, 21 | { 22 | header: 'fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5', 23 | result: ['fr-CH', 'fr', 'en', 'de'], 24 | }, 25 | { 26 | header: 'de-CH', 27 | result: ['de-CH'], 28 | }, 29 | ])('use value from valid header $header', async ({ header, result }) => { 30 | const scope = fork({ 31 | values: [[trackPreferredLanguages.$acceptLanguageHeader, header]], 32 | }); 33 | 34 | await allSettled(setup, { scope }); 35 | 36 | expect(scope.getState($languages)).toEqual(result); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/i18next/src/language.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from 'vitest'; 2 | import { allSettled, createEvent, fork } from 'effector'; 3 | import { createInstance } from 'i18next'; 4 | 5 | import { createI18nextIntegration } from './integration'; 6 | 7 | describe('integration.$language/changeLanguageFx', () => { 8 | test('change language', async () => { 9 | const setup = createEvent(); 10 | 11 | const instance = createInstance({ 12 | resources: { 13 | th: { common: { key: 'th value' } }, 14 | en: { common: { key: 'en value' } }, 15 | }, 16 | lng: 'th', 17 | }); 18 | 19 | const { $language, changeLanguageFx, translated } = 20 | createI18nextIntegration({ 21 | instance, 22 | setup, 23 | }); 24 | 25 | const $val = translated('common:key'); 26 | 27 | const scope = fork(); 28 | 29 | // Before initialization 30 | expect(scope.getState($language)).toBeNull(); 31 | 32 | await allSettled(setup, { scope }); 33 | 34 | // Initial value 35 | expect(scope.getState($language)).toBe('th'); 36 | expect(scope.getState($val)).toBe('th value'); 37 | 38 | await allSettled(changeLanguageFx, { scope, params: 'en' }); 39 | 40 | // After change 41 | expect(scope.getState($language)).toBe('en'); 42 | expect(scope.getState($val)).toBe('en value'); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /apps/website/docs/i18next/translated.md: -------------------------------------------------------------------------------- 1 | # `translated` 2 | 3 | A factory that returns [_Store_](https://effector.dev/docs/api/effector/store) containing a translated string. 4 | 5 | ```ts 6 | const { translated } = createI18nextIntegration({ 7 | /* ... */ 8 | }); 9 | 10 | const $someTranslatedString = translated('premiumLabel.BrandOne'); 11 | ``` 12 | 13 | The second argument is an optional object with options for the translation function. Options can be a [_Store_](https://effector.dev/docs/api/effector/store) or a plain value. 14 | 15 | ```ts 16 | const $city = createStore({ name: 'Moscow' }); 17 | 18 | const { translated } = createI18nextIntegration({ 19 | /* ... */ 20 | }); 21 | 22 | const $someTranslatedString = translated('cityPois.buttonText', { 23 | cityName: $city.map((city) => city.name), 24 | }); 25 | ``` 26 | 27 | Also, you can pass a template string with [_Store_](https://effector.dev/docs/api/effector/store) parts of a key: 28 | 29 | ```ts 30 | const $pathOfAKey = createStore('BrandOne'); 31 | 32 | const { translated } = createI18nextIntegration({ 33 | /* ... */ 34 | }); 35 | 36 | const $someTranslatedString = translated`premiumLabel.${$pathOfAKey}`; 37 | ``` 38 | 39 | Result of the factory will be a [_Store_](https://effector.dev/docs/api/effector/store) containing a translated string. It will be updated automatically when the language or available translations will be changed. 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # With Ease 2 | 3 | A set of libraries and recipes to make frontend development easier thanks to Effector. 4 | 5 | ## Maintains 6 | 7 | ### Getting started 8 | 9 | - clone repo 10 | - install deps via `pnpm install` 11 | - make changes 12 | - make sure that your changes is passing checks: 13 | - run tests via `pnpm run -r test:run` 14 | - try to build it via `pnpm run -r build` 15 | - format code via `pnpm run format:check` 16 | - fill in changes via `pnpm changeset` 17 | - open a PR 18 | - enjoy 🎉 19 | 20 | ### Release workflow 21 | 22 | Releases of With Ease are automated by [changesets](https://github.com/changesets/changesets) and GitHub Actions. Your only duty is creating changeset for every PR, it is controlled by [Changes-action](./.github/workflows/changes.yml). 23 | 24 | After merging PR to master-branch, [Version-action](./.github/workflows/version.yml) will update special PR with the next release. To publish this release, just merge special PR and wait, [Release-action](./.github/workflows/release.yml) will publish packages. 25 | 26 | ### Repository management 27 | 28 | #### New package creation 29 | 30 | Copy-paste `packages/web-api` directory, rename it to the package name. Then, update `package.json`, `README.md` and `vite.config.js` files. Then, delete `CHANGELOG.md` file and any other files that are not needed in the new package. 31 | 32 | Fancy generator will be added in the future. 33 | -------------------------------------------------------------------------------- /packages/contracts/src/interop.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | import { Array, String } from 'runtypes'; 4 | import { object, string } from 'superstruct'; 5 | import { runtypeContract } from '@farfetched/runtypes'; 6 | import { superstructContract } from '@farfetched/superstruct'; 7 | 8 | import { obj, arr } from './index'; 9 | 10 | describe('runtypes', () => { 11 | it('supports Runtype inside', () => { 12 | const cntrct = obj({ name: runtypeContract(Array(String)) }); 13 | 14 | expect(cntrct.isData({ name: ['foo'] })).toBe(true); 15 | expect(cntrct.getErrorMessages({ name: ['foo'] })).toEqual([]); 16 | 17 | expect(cntrct.isData({ name: [1] })).toBe(false); 18 | expect(cntrct.getErrorMessages({ name: [1] })).toMatchInlineSnapshot(` 19 | [ 20 | "name: 0: Expected string, but was number", 21 | ] 22 | `); 23 | }); 24 | }); 25 | 26 | describe('superstruct', () => { 27 | it('supports Superstruct inside', () => { 28 | const cntrct = arr(superstructContract(object({ name: string() }))); 29 | 30 | expect(cntrct.isData([{ name: 'foo' }])).toBe(true); 31 | expect(cntrct.getErrorMessages([{ name: 'foo' }])).toEqual([]); 32 | 33 | expect(cntrct.isData([{ name: 1 }])).toBe(false); 34 | expect(cntrct.getErrorMessages([{ name: 1 }])).toMatchInlineSnapshot(` 35 | [ 36 | "0: name: Expected a string, but received: 1", 37 | ] 38 | `); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /apps/website/docs/contracts/cookbook/optional_fields.md: -------------------------------------------------------------------------------- 1 | # Optional Fields 2 | 3 | By default, all fields mentioned in the schema of `obj` are required. However, you can make a field optional explicitly. 4 | 5 | In case you do not care how exactly the field is optional, you can use the `or` in combination with `noting`: 6 | 7 | ```ts 8 | import { obj, str, num, or, nothing } from '@withease/contracts'; 9 | 10 | const UserWithOptionalAge = obj({ 11 | name: str, 12 | age: or(num, nothing), 13 | }); 14 | ``` 15 | 16 | In the example above, the `age` field can be either a number or missing or `null` or `undefined`. 17 | 18 | ## Only `null` 19 | 20 | In case you expect a field to have `null` as a value, you can add it to the field definition as follows: 21 | 22 | ```ts 23 | import { obj, str, num, or, val } from '@withease/contracts'; 24 | 25 | const UserWithOptionalAge = obj({ 26 | name: str, 27 | age: or(num, val(null)), 28 | }); 29 | ``` 30 | 31 | ## Only `undefined` 32 | 33 | If you expect a field to be missing, you can pass `undefined` as a value: 34 | 35 | ::: warning 36 | In `@withease/contracts`, `undefined` as a field value is the same as a missing field. If you need to differentiate between the two, you can fallback to more powerful tools like Zod or Runtypes. 37 | ::: 38 | 39 | ```ts 40 | import { obj, str, num, or, val } from '@withease/contracts'; 41 | 42 | const UserWithPossibleNoAge = obj({ 43 | name: str, 44 | age: or(num, val(undefined)), 45 | }); 46 | ``` 47 | -------------------------------------------------------------------------------- /apps/website/docs/contracts/size_chart.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 62 | -------------------------------------------------------------------------------- /apps/website/docs/.vitepress/theme/LiveDemo.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 53 | -------------------------------------------------------------------------------- /apps/website/docs/protocols/contract.md: -------------------------------------------------------------------------------- 1 | # _Contract_ 2 | 3 | A rule to statically validate received data. Any object following the strict API could be used as a _Contract_. 4 | 5 | ::: tip Packages that use _Contract_ 6 | 7 | - [`@farfetched/core`](https://farfetched.pages.dev) 8 | - [`effector-storage`](https://github.com/yumauri/effector-storage) 9 | 10 | ::: 11 | 12 | ::: tip Packages that provide integration for creating _Contract_ 13 | 14 | - [`@withease/contracts`](/contracts/) 15 | - [`@farfetched/runtypes`](https://farfetched.pages.dev/api/contracts/runtypes.html) 16 | - [`@farfetched/zod`](https://farfetched.pages.dev/api/contracts/zod.html) 17 | - [`@farfetched/io-ts`](https://farfetched.pages.dev/api/contracts/io-ts.html) 18 | - [`@farfetched/superstruct`](https://farfetched.pages.dev/api/contracts/superstruct.html) 19 | - [`@farfetched/typed-contracts`](https://farfetched.pages.dev/api/contracts/typed-contracts.html) 20 | - [`@farfetched/valibot`](https://farfetched.pages.dev/api/contracts/valibot.html) 21 | 22 | ::: 23 | 24 | ## API reference 25 | 26 | To create a _Contract_ you need to provide an object with the following fields: 27 | 28 | ```ts 29 | interface Contract { 30 | /** 31 | * Checks if Raw is Data 32 | */ 33 | isData: (prepared: Raw) => prepared is Data; 34 | /** 35 | * - empty array is dedicated for valid response 36 | * - array of string with validation erorrs for invalidDataError 37 | */ 38 | getErrorMessages: (prepared: Raw) => string[]; 39 | } 40 | ``` 41 | -------------------------------------------------------------------------------- /apps/website/docs/contracts/sizes.data.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | async load() { 3 | try { 4 | const [ 5 | zodSize, 6 | runtypesSize, 7 | ioTsSize, 8 | fpTsSize, 9 | superstructSize, 10 | typedContractsSize, 11 | valibotSize, 12 | ] = await Promise.all([ 13 | definePackageSize('zod', 'lib/index.js'), 14 | definePackageSize('runtypes', 'lib/index.js'), 15 | definePackageSize('io-ts', 'lib/index.js'), 16 | definePackageSize('fp-ts', 'lib/index.js'), 17 | definePackageSize('superstruct', 'dist/index.mjs'), 18 | definePackageSize('typed-contracts', 'lib/bundle.js'), 19 | definePackageSize('valibot', './dist/index.js'), 20 | ]); 21 | 22 | return [ 23 | { name: 'Zod', size: zodSize }, 24 | { name: 'runtypes', size: runtypesSize }, 25 | { name: 'io-ts + fp-ts', size: ioTsSize + fpTsSize }, 26 | { name: 'superstruct', size: superstructSize }, 27 | { name: 'typed-contracts', size: typedContractsSize }, 28 | { name: 'valibot', size: valibotSize }, 29 | ]; 30 | } catch (error) { 31 | return null; 32 | } 33 | }, 34 | }; 35 | 36 | async function definePackageSize(packageName, moduleName) { 37 | const response = await fetch( 38 | `https://esm.run/${packageName}@latest/${moduleName}`, 39 | { method: 'HEAD' } 40 | ); 41 | 42 | const encodedSize = Number(response.headers.get('content-length') ?? 0); 43 | 44 | return encodedSize; 45 | } 46 | -------------------------------------------------------------------------------- /packages/i18next/src/async_init.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { allSettled, createEffect, createEvent, fork } from 'effector'; 3 | 4 | import { createI18nextIntegration } from './integration'; 5 | import { createInstance } from 'i18next'; 6 | 7 | describe('integration, async init', () => { 8 | test('instance as Effect', async () => { 9 | const setup = createEvent(); 10 | 11 | const instance = createInstance(); 12 | 13 | const integration = createI18nextIntegration({ 14 | instance: createEffect(() => instance), 15 | setup, 16 | }); 17 | 18 | const scope = fork(); 19 | 20 | expect(scope.getState(integration.$isReady)).toBeFalsy(); 21 | expect(scope.getState(integration.$instance)).toBeNull(); 22 | 23 | await allSettled(setup, { scope }); 24 | 25 | expect(scope.getState(integration.$isReady)).toBeTruthy(); 26 | expect(scope.getState(integration.$instance)).toBe(instance); 27 | }); 28 | 29 | test('instance as function', async () => { 30 | const setup = createEvent(); 31 | 32 | const instance = createInstance(); 33 | 34 | const integration = createI18nextIntegration({ 35 | instance: async () => instance, 36 | setup, 37 | }); 38 | 39 | const scope = fork(); 40 | 41 | expect(scope.getState(integration.$isReady)).toBeFalsy(); 42 | expect(scope.getState(integration.$instance)).toBeNull(); 43 | 44 | await allSettled(setup, { scope }); 45 | 46 | expect(scope.getState(integration.$isReady)).toBeTruthy(); 47 | expect(scope.getState(integration.$instance)).toBe(instance); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/web-api/src/network_status.ts: -------------------------------------------------------------------------------- 1 | import { type Event, type Store, createEvent, createStore } from 'effector'; 2 | 3 | import { readValue, setupListener, type Setupable } from './shared'; 4 | import { type TriggerProtocol } from './trigger_protocol'; 5 | 6 | type NetworkStatus = ({ setup, teardown }: Setupable) => { 7 | online: Event; 8 | offline: Event; 9 | $online: Store; 10 | $offline: Store; 11 | }; 12 | 13 | const trackNetworkStatus: NetworkStatus & TriggerProtocol = (config) => { 14 | const online = setupListener( 15 | { 16 | add: (listener) => window.addEventListener('online', listener), 17 | remove: (listener) => window.removeEventListener('online', listener), 18 | }, 19 | config 20 | ); 21 | const offline = setupListener( 22 | { 23 | add: (listener) => window.addEventListener('offline', listener), 24 | remove: (listener) => window.removeEventListener('offline', listener), 25 | }, 26 | config 27 | ); 28 | 29 | const $online = createStore( 30 | readValue(() => navigator.onLine, true), 31 | { serialize: 'ignore' } 32 | ) 33 | .on(online, () => true) 34 | .on(offline, () => false); 35 | 36 | const $offline = $online.map((online) => !online); 37 | 38 | return { online, offline, $offline, $online }; 39 | }; 40 | 41 | trackNetworkStatus['@@trigger'] = () => { 42 | const setup = createEvent(); 43 | const teardown = createEvent(); 44 | 45 | const { online } = trackNetworkStatus({ setup, teardown }); 46 | 47 | return { setup, teardown, fired: online }; 48 | }; 49 | 50 | export { trackNetworkStatus }; 51 | -------------------------------------------------------------------------------- /packages/i18next/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @withease/i18next 2 | 3 | ## 25.0.0 4 | 5 | ### Major Changes 6 | 7 | - 0c5ac4f: Support i18next@25 8 | 9 | ## 24.0.0 10 | 11 | ### Major Changes 12 | 13 | - 442be74: Test against i18next@24. For migration guide please refer to the i18next docs: https://www.i18next.com/misc/migration-guide 14 | 15 | ## 23.2.2 16 | 17 | ### Patch Changes 18 | 19 | - c3a3bb4: Update toolchain 20 | 21 | ## 23.2.1 22 | 23 | ### Patch Changes 24 | 25 | - a6ac670: Add missed license field to package.json 26 | 27 | ## 23.2.0 28 | 29 | ### Minor Changes 30 | 31 | - 6989158: Allow to pass async `instance` to integration 32 | - 60af5db: Add _Store_ `$language` and _Effect_ `changeLanguageFx` to integration 33 | 34 | ## 23.1.0 35 | 36 | ### Minor Changes 37 | 38 | - 41a00ef: Allow to use with Effector 23 39 | 40 | ### Patch Changes 41 | 42 | - 41a00ef: Add forgotten allowance for usage with i18next 23 in peerDependencies 43 | 44 | ## 23.0.0 45 | 46 | ### Major Changes 47 | 48 | - 08b93dd: Test against i18next@23. For migration guide please refer to the i18next docs: https://www.i18next.com/misc/migration-guide 49 | 50 | ## 22.0.1 51 | 52 | ### Patch Changes 53 | 54 | - e2dad7f: Fix ESM/CJS builds 55 | 56 | ## 22.0.0 57 | 58 | - Nothing new, just a version bump to mark library as stable 59 | 60 | ## 0.1.2 61 | 62 | ### Patch Changes 63 | 64 | - 34e5105: Do not use `.at` on Array 65 | - 45affeb: Simplify internal implementation of `$t` _Store_ 66 | 67 | ## 0.1.1 68 | 69 | ### Patch Changes 70 | 71 | - 65cc82e: Fix `$t` value before `setup` _Event_ 72 | 73 | ## 0.1.0 74 | 75 | ### Minor Changes 76 | 77 | - 7c47551: Initial release 78 | -------------------------------------------------------------------------------- /packages/factories/src/factories.test-d.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expectTypeOf } from 'vitest'; 2 | 3 | import { createFactory } from './create_factory'; 4 | import { invoke } from './invoke'; 5 | 6 | describe('factories', () => { 7 | test('supports overloads', () => { 8 | function createFlag(status: number): number; 9 | function createFlag(status: string): string; 10 | 11 | function createFlag(status: number | string) { 12 | return status; 13 | } 14 | 15 | const createFlagFactory = createFactory(createFlag); 16 | 17 | const stringFlag = invoke(createFlagFactory, '1'); 18 | expectTypeOf(stringFlag).toEqualTypeOf(); 19 | 20 | const numberFlag = invoke(createFlagFactory, 1); 21 | expectTypeOf(numberFlag).toEqualTypeOf(); 22 | }); 23 | 24 | test('supports factories with no arguments', () => { 25 | function createFlag(): number { 26 | return 1; 27 | } 28 | 29 | const createFlagFactory = createFactory(createFlag); 30 | 31 | const flag = invoke(createFlagFactory); 32 | expectTypeOf(flag).toEqualTypeOf(); 33 | }); 34 | 35 | test('supports factories with no arguments as part of overload', () => { 36 | function createFlag(): number; 37 | function createFlag(status: string): string; 38 | 39 | function createFlag(status?: string): any { 40 | return status ?? 0; 41 | } 42 | 43 | const createFlagFactory = createFactory(createFlag); 44 | 45 | const numberFlag = invoke(createFlagFactory); 46 | expectTypeOf(numberFlag).toEqualTypeOf(); 47 | 48 | const stringFlag = invoke(createFlagFactory, 'stat'); 49 | expectTypeOf(stringFlag).toEqualTypeOf(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "withease", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "packageManager": "pnpm@7.28.0", 6 | "scripts": { 7 | "build": "pnpm run -r build", 8 | "format:check": "pnpm prettier {packages,apps}/**/*.{ts,tsx,md,vue} --check --no-error-on-unmatched-pattern", 9 | "format:apply": "pnpm prettier {packages,apps}/**/*.{ts,tsx,md,vue} --write --no-error-on-unmatched-pattern" 10 | }, 11 | "devDependencies": { 12 | "@arethetypeswrong/cli": "^0.15.3", 13 | "@babel/parser": "^7.25.0", 14 | "@changesets/cli": "^2.24.1", 15 | "@farfetched/core": "^0.8.13", 16 | "@farfetched/runtypes": "^0.12.4", 17 | "@farfetched/superstruct": "^0.12.4", 18 | "@playwright/test": "^1.32.2", 19 | "@reduxjs/toolkit": "^2.0.1", 20 | "@size-limit/file": "^7.0.8", 21 | "@types/node": "^20.12.7", 22 | "comment-parser": "^1.4.1", 23 | "effector": "23.0.0", 24 | "effector-vue": "23.0.0", 25 | "estree-walker": "^3.0.3", 26 | "glob": "^8.0.3", 27 | "i18next": "25.0.0", 28 | "markdown": "^0.5.0", 29 | "playwright": "^1.32.2", 30 | "prettier": "^2.6.2", 31 | "publint": "^0.2.7", 32 | "redux": "^5.0.0", 33 | "redux-saga": "^1.2.3", 34 | "runtypes": "^6.7.0", 35 | "size-limit": "^7.0.8", 36 | "superstruct": "^2.0.2", 37 | "typescript": "5.1.6", 38 | "vite": "5", 39 | "vite-plugin-dts": "^3.8.3", 40 | "vite-tsconfig-paths": "4.2.1", 41 | "vitepress": "1.3.1", 42 | "vitepress-plugin-rss": "^0.2.9", 43 | "vitest": "2.0.4" 44 | }, 45 | "dependencies": { 46 | "chart.js": "^4.4.3", 47 | "vue": "3.4.35", 48 | "vue-chartjs": "^5.3.1", 49 | "@algolia/client-search": "^4.14.3", 50 | "bytes": "^3.1.2", 51 | "lodash-es": "^4.17.21", 52 | "pretty-bytes": "^6.1.1", 53 | "sandpack-vue3": "^3.1.7" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/web-api/src/page_visibility.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Event, 3 | type Store, 4 | createEvent, 5 | createStore, 6 | sample, 7 | } from 'effector'; 8 | 9 | import { readValue, setupListener, type Setupable } from './shared'; 10 | import { type TriggerProtocol } from './trigger_protocol'; 11 | 12 | type PageVisibility = ({ setup, teardown }: Setupable) => { 13 | visible: Event; 14 | hidden: Event; 15 | $visible: Store; 16 | $hidden: Store; 17 | }; 18 | 19 | const trackPageVisibility: PageVisibility & TriggerProtocol = (config) => { 20 | const visibilityChanged = setupListener( 21 | { 22 | add: (listener) => 23 | document.addEventListener('visibilitychange', listener), 24 | remove: (listener) => 25 | document.removeEventListener('visibilitychange', listener), 26 | readPayload: () => document.visibilityState, 27 | }, 28 | config 29 | ); 30 | 31 | const $visibilityState = createStore( 32 | readValue(() => document.visibilityState, 'visible'), 33 | { serialize: 'ignore' } 34 | ).on(visibilityChanged, (_, state) => state); 35 | 36 | // -- Public API 37 | const $visible = $visibilityState.map((state) => state === 'visible'); 38 | const $hidden = $visibilityState.map((state) => state === 'hidden'); 39 | 40 | const visible = sample({ 41 | clock: $visible.updates, 42 | filter: Boolean, 43 | fn: (): void => { 44 | // 45 | }, 46 | }); 47 | const hidden = sample({ 48 | clock: $hidden.updates, 49 | filter: Boolean, 50 | fn: (): void => { 51 | // 52 | }, 53 | }); 54 | 55 | // -- Result 56 | return { visible, hidden, $visible, $hidden }; 57 | }; 58 | 59 | trackPageVisibility['@@trigger'] = () => { 60 | const setup = createEvent(); 61 | const teardown = createEvent(); 62 | 63 | const { visible } = trackPageVisibility({ setup, teardown }); 64 | 65 | return { setup, teardown, fired: visible }; 66 | }; 67 | 68 | export { trackPageVisibility }; 69 | -------------------------------------------------------------------------------- /apps/website/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | markdownStyles: false 5 | 6 | hero: 7 | name: Effector 8 | text: with ease 9 | tagline: A set of libraries and recipes to make frontend development easier thanks to Effector 10 | image: 11 | src: /logo.svg 12 | alt: With Ease 13 | actions: 14 | - theme: brand 15 | text: Magazine 16 | link: /magazine/ 17 | - theme: alt 18 | text: View on GitHub 19 | link: https://github.com/igorkamyshev/withease 20 | 21 | features: 22 | - icon: 🌐 23 | title: i18next 24 | details: A powerful internationalization framework based on i18next 25 | link: /i18next/ 26 | linkText: Get Started 27 | - icon: 🪝 28 | title: redux 29 | details: Minimalistic package to allow simpler migration from Redux to Effector 30 | link: /redux/ 31 | linkText: Get Started 32 | - icon: 👩🏽‍💻 33 | title: web-api 34 | details: Web API bindings — network status, tab visibility, and more 35 | link: /web-api/ 36 | linkText: Get Started 37 | - icon: 📄 38 | title: contracts 39 | details: Extremely small library to validate data from external sources 40 | link: /contracts/ 41 | linkText: Get Started 42 | - icon: 👩‍🏭 43 | title: factories 44 | details: Set of helpers to create factories in your application 45 | link: /factories/ 46 | linkText: Get Started 47 | --- 48 | 49 | 63 | 64 | 70 | 71 | -------------------------------------------------------------------------------- /apps/website/docs/magazine/scopefull.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Catch Scope-less Calls 3 | date: 2024-08-20 4 | --- 5 | 6 | # Catch Scope-less Calls 7 | 8 | [Fork API](https://effector.dev/en/api/effector/fork/) is an Effector's killer feature. It allows you to execute any number of application instances in parallel in a single thread which is great for testing and SSR. Fork API has [some rules](/magazine/fork_api_rules) to follow and this article is about automated validation of them. 9 | 10 | ## The Problem 11 | 12 | Some violations of the Fork API rules can be detected by static analysis tools like ESLint with the [effector/scope](https://eslint.effector.dev/presets/scope.html) preset. But some rules require runtime validation. For example, it is illegal to make an imperative call of an [_Event_](https://effector.dev/en/api/effector/event/) with no explicit [_Scope_](https://effector.dev/docs/api/effector/scope). However, for ESLint it is almost impossible to detect such calls. 13 | 14 | In this case we need to listen to all messages that pass through Effector's kernel and analyze them. If we find a message with no [_Scope_](https://effector.dev/docs/api/effector/scope) we can log it. 15 | 16 | ## The Solution 17 | 18 | Effector has a special API to listen messages that pass through the library. It is called [Inspect API](https://effector.dev/en/api/effector/inspect/). You can use it to catch all messages and analyze them. This API is great for debugging and testing which is what we need. 19 | 20 | The usage of the Inspect API is quite simple. You need to call the `inspect` function with a callback that will be called for each message. The callback will receive a message object that contains all the information about the message. You can analyze this object and do whatever you want. 21 | 22 | ```ts 23 | import { inspect, Message } from 'effector/inspect'; 24 | 25 | inspect({ 26 | /** 27 | * Explicitly define that we will 28 | * catch only messages where Scope is undefined 29 | */ 30 | scope: undefined, 31 | fn: (m: Message) => { 32 | const name = `${m.kind} ${m.name}`; 33 | const error = new Error(`${name} is not bound to scope`); 34 | 35 | console.error(error); 36 | }, 37 | }); 38 | ``` 39 | -------------------------------------------------------------------------------- /apps/website/docs/factories/motivation.md: -------------------------------------------------------------------------------- 1 | # Motivation 2 | 3 | This library is created to solve two major problems: 4 | 5 | 1. Controlling that factories are invoked correctly. 6 | 2. Marking nested factories as factories. 7 | 8 | Let us elaborate on these problems. 9 | 10 | ## Controlling that factories are invoked correctly 11 | 12 | In Effector's ecosystem, all factories [have to be added to the code-transformation plugin's config](https://effector.dev/en/explanation/sids/). But it is really easy to forget to add a factory to the config after creating it. Effector's plugin will not throw an error in this case, but the factory will not work correctly in case of SSR. 13 | 14 | In case of using this library, you have to remember only one thing: all factories have to be created using `createFactory` function from `@withease/factories` library. The result of this function is not callable, so you will get an error if you try to invoke it directly. 15 | 16 | ```js 17 | import { createFactory } from '@withease/factories'; 18 | 19 | const someFactory = createFactory((arg) => { 20 | // ... 21 | }); 22 | 23 | // This will throw an error 24 | const value = someFactory(config); 25 | ``` 26 | 27 | This type of factories can be invoked only by using `invoke` function from `@withease/factories` library. This function accepts a factory and its arguments and returns a result of factory invocation. 28 | 29 | ```js 30 | import { createFactory, invoke } from '@withease/factories'; 31 | 32 | const someFactory = createFactory((arg) => { 33 | // ... 34 | }); 35 | 36 | const value = invoke(someFactory, config); 37 | ``` 38 | 39 | So, if you add `@withease/factories` to the config of Effector's plugin, you can be sure that all factories are invoked correctly, and you do not have to add them to the config manually. 40 | 41 | ## Marking nested factories as factories 42 | 43 | As you can see in [tests in this PR](https://github.com/effector/effector/pull/938) `effector/babel-plugin` can not mark factories as factories if they are nested in some object exported from a module. It is silently ignored by the plugin because it is nearly impossible to detect such cases automatically based on the code's AST. 44 | 45 | However, with this library, you do not have to worry about this problem. All factories created using `createFactory` function and invoked by using `invoke` function will be marked as factories no matter where they are located in the code. 46 | -------------------------------------------------------------------------------- /packages/web-api/src/screen_orientation.ts: -------------------------------------------------------------------------------- 1 | import { type Store, createEvent, createStore, sample } from 'effector'; 2 | 3 | import { type Setupable, readValue, setupListener } from './shared'; 4 | import { type TriggerProtocol } from './trigger_protocol'; 5 | 6 | type ScreenOrientation = ({ setup, teardown }: Setupable) => { 7 | $type: Store; 8 | $angle: Store; 9 | $portrait: Store; 10 | $landscape: Store; 11 | }; 12 | 13 | const trackScreenOrientation: ScreenOrientation & TriggerProtocol = ( 14 | config 15 | ) => { 16 | const $type = createStore( 17 | readValue(() => screen.orientation.type, null), 18 | { 19 | serialize: 'ignore', 20 | } 21 | ); 22 | const $angle = createStore( 23 | readValue(() => screen.orientation.angle, null), 24 | { serialize: 'ignore' } 25 | ); 26 | 27 | /** 28 | * States if device is in landscape orientation 29 | */ 30 | const $landscape = $type.map((type) => { 31 | return type === 'landscape-primary' || type === 'landscape-secondary'; 32 | }); 33 | 34 | /** 35 | * States if device is in portrait orientation 36 | */ 37 | const $portrait = $type.map((type) => { 38 | return type === 'portrait-primary' || type === 'portrait-secondary'; 39 | }); 40 | 41 | const orientationChanged = setupListener( 42 | { 43 | add: (listener) => 44 | screen.orientation.addEventListener('change', listener), 45 | remove: (listener) => 46 | screen.orientation.removeEventListener('change', listener), 47 | readPayload: () => screen.orientation, 48 | }, 49 | config 50 | ); 51 | 52 | sample({ 53 | clock: orientationChanged, 54 | fn: () => screen.orientation.type, 55 | target: $type, 56 | }); 57 | 58 | sample({ 59 | clock: orientationChanged, 60 | fn: () => screen.orientation.angle, 61 | target: $angle, 62 | }); 63 | 64 | return { $type, $angle, $portrait, $landscape }; 65 | }; 66 | 67 | trackScreenOrientation['@@trigger'] = () => { 68 | const setup = createEvent(); 69 | const teardown = createEvent(); 70 | 71 | const { $type } = trackScreenOrientation({ setup, teardown }); 72 | 73 | const fired = sample({ 74 | clock: $type.updates, 75 | fn: (): void => { 76 | // noop 77 | }, 78 | }); 79 | 80 | return { setup, teardown, fired }; 81 | }; 82 | 83 | export { trackScreenOrientation }; 84 | -------------------------------------------------------------------------------- /packages/web-api/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @withease/web-api 2 | 3 | ## 1.3.0 4 | 5 | ### Minor Changes 6 | 7 | - 55266ec: Add `trackGeolocation` integration 8 | 9 | ## 1.2.3 10 | 11 | ### Patch Changes 12 | 13 | - c3a3bb4: Update toolchain 14 | 15 | ## 1.2.2 16 | 17 | ### Patch Changes 18 | 19 | - a6ac670: Add missed license field to package.json 20 | 21 | ## 1.2.1 22 | 23 | ### Patch Changes 24 | 25 | - 3989ba4: Fix typo in `ScreenOrientation` type 26 | 27 | ## 1.2.0 28 | 29 | ### Minor Changes 30 | 31 | - 1a83c99: Add `$landscape` and `$portrait` stores to `trackScreenOrientation` integration 32 | 33 | ## 1.1.1 34 | 35 | ### Patch Changes 36 | 37 | - 3e21bda: Fix incorrect types of `@@trigger` protocol in case of Effector 23 38 | 39 | ## 1.1.0 40 | 41 | ### Minor Changes 42 | 43 | - 41a00ef: Allow to use with Effector 23 44 | 45 | ## 1.0.1 46 | 47 | ### Patch Changes 48 | 49 | - e2dad7f: Fix ESM/CJS builds 50 | 51 | ## 1.0.0 52 | 53 | - Nothing new, just a version bump to mark library as stable 54 | 55 | ## 0.4.5 56 | 57 | ### Patch Changes 58 | 59 | - d846aec: Fire matched _Event_ after setup if current query is \$matches 60 | 61 | ## 0.4.4 62 | 63 | ### Patch Changes 64 | 65 | - 8661467: Fire matched _Event_ after setup if current query is \$matches 66 | 67 | ## 0.4.3 68 | 69 | ### Patch Changes 70 | 71 | - 7b3880d: Make all integration bulletproof against weird env 72 | 73 | ## 0.4.2 74 | 75 | ### Patch Changes 76 | 77 | - 098baea: Fix undefined value of `$visiblityState` in non-usual env 78 | 79 | ## 0.4.1 80 | 81 | ### Patch Changes 82 | 83 | - 6a988b4: Fix undefined `$visibilityState` in non-usual env 84 | 85 | ## 0.4.0 86 | 87 | ### Minor Changes 88 | 89 | - b8eb3f4: Add new integration `trackPreferredLanguages` 90 | 91 | ## 0.3.1 92 | 93 | ### Patch Changes 94 | 95 | - f466d54: Fix extra firing of `visible` and `hidden` in `trackPageVisibility` 96 | 97 | ## 0.3.0 98 | 99 | ### Minor Changes 100 | 101 | - 4de0174: Add `trackScreenOrientation` 102 | - b86b9b9: Delete deprecated (because of typo) field `$visibile` from return object of `trackPageVisibility` 103 | 104 | ## 0.2.0 105 | 106 | ### Minor Changes 107 | 108 | - 290f48d: Add `trackMediaQuery` 109 | 110 | ## 0.1.1 111 | 112 | ### Patch Changes 113 | 114 | - 0d0c929: Fix a typo in a field of object returned from `trackPageVisibility` — `$visibile` => `$visible` 115 | 116 | ## 0.1.0 117 | 118 | ### Minor Changes 119 | 120 | - a9452f8: Initial release 121 | -------------------------------------------------------------------------------- /apps/website/docs/web-api/page_visibility.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Page visibility 3 | --- 4 | 5 | # Page visibility 6 | 7 | Allows tracking window focus and blur with [_Events_](https://effector.dev/en/api/effector/event/) and [_Stores_](https://effector.dev/docs/api/effector/store). 8 | 9 | ::: info 10 | 11 | Uses [Page Visibility API](https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API) under the hood 12 | 13 | ::: 14 | 15 | ## Usage 16 | 17 | All you need to do is to create an integration by calling `trackPageVisibility` with an integration options: 18 | 19 | - `setup`: after this [_Event_](https://effector.dev/en/api/effector/event/) all listeners will be installed, and the integration will be ready to use; it is required because it is better to use [explicit initialization _Event_ in the application](/magazine/explicit_start). 20 | - `teardown?`: after this [_Event_](https://effector.dev/en/api/effector/event/) all listeners will be removed, and the integration will be ready to be destroyed. 21 | 22 | ```ts 23 | import { trackPageVisibility } from '@withease/web-api'; 24 | 25 | const { visible, hidden, $visible, $hidden } = trackPageVisibility({ 26 | setup: appStarted, 27 | }); 28 | ``` 29 | 30 | Returns an object with: 31 | 32 | - `visible`: [_Event_](https://effector.dev/en/api/effector/event/) fired when the content of a tab has become visible 33 | - `hidden`: [_Event_](https://effector.dev/en/api/effector/event/) fired when the content of a tab has been hidden 34 | - `$visible`: [_Store_](https://effector.dev/docs/api/effector/store) with `true` if document is visible and `false` if it is hidden 35 | - `$hidden`: [_Store_](https://effector.dev/docs/api/effector/store) with `false` if document is visible and `true` if it is hidden 36 | 37 | ::: tip 38 | It supports [`@@trigger` protocol](/protocols/trigger). Since it allows firing only one [_Event_](https://effector.dev/en/api/effector/event/) `trackPageVisibility` triggers `visible` as a `fired` in case of [`@@trigger` protocol](/protocols/trigger). 39 | 40 | ```ts 41 | import { trackPageVisibility } from '@withease/web-api'; 42 | 43 | somethingExpectsTrigger(trackPageVisibility); 44 | ``` 45 | 46 | ::: 47 | 48 | ## Live demo 49 | 50 | Let us show you a live demo of how it works. The following demo displays history of `hidden` and `visible` [_Events_](https://effector.dev/en/api/effector/event/). _Switch to other tab and return there see how it works._ 51 | 52 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | code: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: pnpm/action-setup@v2 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version-file: '.nvmrc' 15 | cache: 'pnpm' 16 | - run: pnpm install --frozen-lockfile 17 | - run: pnpm run format:check 18 | - run: pnpm run -r build 19 | - run: pnpm run -r test:run 20 | pkg: 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - uses: pnpm/action-setup@v2 26 | - uses: actions/setup-node@v3 27 | with: 28 | node-version-file: '.nvmrc' 29 | cache: 'pnpm' 30 | - run: pnpm install --frozen-lockfile 31 | - run: pnpm run -r build 32 | - run: pnpm run -r size 33 | - run: pnpm run -r publint 34 | - run: pnpm run -r typelint 35 | e2e: 36 | runs-on: ubuntu-latest 37 | 38 | steps: 39 | - uses: actions/checkout@v3 40 | - uses: pnpm/action-setup@v2 41 | - uses: actions/setup-node@v3 42 | with: 43 | node-version-file: '.nvmrc' 44 | cache: 'pnpm' 45 | - run: pnpm install --frozen-lockfile 46 | - run: pnpm playwright install 47 | - run: pnpm run -r build 48 | - run: pnpm run -r e2e 49 | 50 | code_old_versions: 51 | runs-on: ubuntu-latest 52 | 53 | strategy: 54 | matrix: 55 | version: [22] 56 | 57 | steps: 58 | - uses: actions/checkout@v3 59 | - uses: pnpm/action-setup@v2 60 | - uses: actions/setup-node@v3 61 | with: 62 | node-version-file: '.nvmrc' 63 | cache: 'pnpm' 64 | 65 | - run: node ./tools/other-majors/prepare.mjs ${{ matrix.version }} 66 | - run: pnpm install --no-frozen-lockfile 67 | 68 | - run: pnpm run -r build 69 | - run: pnpm run -r test:run 70 | 71 | e2e_old_versions: 72 | runs-on: ubuntu-latest 73 | 74 | strategy: 75 | matrix: 76 | version: [22] 77 | 78 | steps: 79 | - uses: actions/checkout@v3 80 | - uses: pnpm/action-setup@v2 81 | - uses: actions/setup-node@v3 82 | with: 83 | node-version-file: '.nvmrc' 84 | cache: 'pnpm' 85 | - run: pnpm install --frozen-lockfile 86 | 87 | - run: node ./tools/other-majors/prepare.mjs ${{ matrix.version }} 88 | - run: pnpm install --no-frozen-lockfile 89 | 90 | - run: pnpm playwright install 91 | - run: pnpm run -r build 92 | - run: pnpm run -r e2e 93 | -------------------------------------------------------------------------------- /packages/web-api/src/preferred_languages.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Event, 3 | type Store, 4 | combine, 5 | createEvent, 6 | createStore, 7 | sample, 8 | } from 'effector'; 9 | 10 | import { type Setupable, readValue, setupListener } from './shared'; 11 | import { type TriggerProtocol } from './trigger_protocol'; 12 | 13 | type PrefereredLanguages = ({ setup, teardown }: Setupable) => { 14 | languageChanged: Event; 15 | $language: Store; 16 | $languages: Store; 17 | }; 18 | 19 | type ScopeOverridesSupport = { 20 | $acceptLanguageHeader: Store; 21 | }; 22 | 23 | const $acceptLanguageHeader = createStore(null, { 24 | serialize: 'ignore', 25 | }); 26 | 27 | const $headerLanguages = $acceptLanguageHeader.map((header) => { 28 | if (!header) { 29 | return []; 30 | } 31 | 32 | return header 33 | .split(',') 34 | .map((lang) => lang.split(';')[0]?.trim()) 35 | .filter((lang) => lang && lang !== '*'); 36 | }); 37 | 38 | const trackPreferredLanguages: PrefereredLanguages & 39 | TriggerProtocol & 40 | ScopeOverridesSupport = (config) => { 41 | const $navigatorLanguages = createStore( 42 | readValue(() => navigator.languages, []), 43 | { serialize: 'ignore' } 44 | ); 45 | 46 | const languagesChanged = setupListener( 47 | { 48 | add: (listener) => window.addEventListener('languagechange', listener), 49 | remove: (listener) => 50 | window.removeEventListener('languagechange', listener), 51 | readPayload: () => navigator.languages, 52 | }, 53 | config 54 | ); 55 | 56 | sample({ clock: languagesChanged, target: $navigatorLanguages }); 57 | 58 | const $languages = combine( 59 | { fromHeader: $headerLanguages, fromNavigator: $navigatorLanguages }, 60 | ({ fromHeader, fromNavigator }) => 61 | fromHeader.length > 0 ? fromHeader : fromNavigator 62 | ); 63 | 64 | const $language = $languages.map( 65 | (languages): string | null => languages[0] ?? null 66 | ); 67 | 68 | const languageChanged = sample({ 69 | clock: languagesChanged, 70 | fn() { 71 | // noop 72 | }, 73 | }); 74 | 75 | return { $languages, $language, languageChanged }; 76 | }; 77 | 78 | trackPreferredLanguages['@@trigger'] = () => { 79 | const setup = createEvent(); 80 | const teardown = createEvent(); 81 | 82 | const { languageChanged } = trackPreferredLanguages({ setup, teardown }); 83 | 84 | return { setup, teardown, fired: languageChanged }; 85 | }; 86 | 87 | trackPreferredLanguages.$acceptLanguageHeader = $acceptLanguageHeader; 88 | 89 | export { trackPreferredLanguages }; 90 | -------------------------------------------------------------------------------- /apps/website/docs/web-api/network_status.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Network status 3 | --- 4 | 5 | # Network status 6 | 7 | Allows tracking network status with [_Events_](https://effector.dev/en/api/effector/event/) and [_Stores_](https://effector.dev/docs/api/effector/store). 8 | 9 | ::: info 10 | 11 | Uses [Navigator.onLine](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine), [Window: online event](https://developer.mozilla.org/en-US/docs/Web/API/Window/online_event) and [Window: offline event](https://developer.mozilla.org/en-US/docs/Web/API/Window/offline_event) under the hood 12 | 13 | ::: 14 | 15 | ## Usage 16 | 17 | All you need to do is to create an integration by calling `trackNetworkStatus` with an integration options: 18 | 19 | - `setup`: after this [_Event_](https://effector.dev/en/api/effector/event/) all listeners will be installed, and the integration will be ready to use; it is required because it is better to use [explicit initialization _Event_ in the application](/magazine/explicit_start). 20 | - `teardown?`: after this [_Event_](https://effector.dev/en/api/effector/event/) all listeners will be removed, and the integration will be ready to be destroyed. 21 | 22 | ```ts 23 | import { trackNetworkStatus } from '@withease/web-api'; 24 | 25 | const { online, offline, $online, $offline } = trackNetworkStatus({ 26 | setup: appStarted, 27 | }); 28 | ``` 29 | 30 | Returns an object with: 31 | 32 | - `online`: [_Event_](https://effector.dev/en/api/effector/event/) that fires on connection restore 33 | - `offline`: [_Event_](https://effector.dev/en/api/effector/event/) that fires on connection loss 34 | - `$online`: [_Store_](https://effector.dev/docs/api/effector/store) with `true` if connection is restored and `false` if connection is lost 35 | - `$offline`: [_Store_](https://effector.dev/docs/api/effector/store) with `true` if connection is lost and `false` if connection is restored 36 | 37 | ::: tip 38 | It supports [`@@trigger` protocol](/protocols/trigger). Since it allows firing only one [_Event_](https://effector.dev/en/api/effector/event/) `trackNetworkStatus` triggers `online` as a `fired` in case of [`@@trigger` protocol](/protocols/trigger). 39 | 40 | ```ts 41 | import { trackNetworkStatus } from '@withease/web-api'; 42 | 43 | somethingExpectsTrigger(trackNetworkStatus); 44 | ``` 45 | 46 | ::: 47 | 48 | ## Live demo 49 | 50 | Let us show you a live demo of how it works. The following demo displays a `$online` and `$offilne` values of the current network status. _Turn off Wi-Fi to see how it works._ 51 | 52 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /apps/website/docs/web-api/screen_orientation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Screen orientation 3 | --- 4 | 5 | # Screen orientation 6 | 7 | Allows tracking device screen orientation with [_Events_](https://effector.dev/en/api/effector/event/) and [_Stores_](https://effector.dev/docs/api/effector/store). 8 | 9 | ::: info 10 | 11 | Uses [Screen Orientation API](https://developer.mozilla.org/en-US/docs/Web/API/Screen_Orientation_API) under the hood 12 | 13 | ::: 14 | 15 | ## Usage 16 | 17 | All you need to do is to create an integration by calling `trackScreenOrientation` with an integration options: 18 | 19 | - `setup`: after this [_Event_](https://effector.dev/en/api/effector/event/) all listeners will be installed, and the integration will be ready to use; it is required because it is better to use [explicit initialization _Event_ in the application](/magazine/explicit_start). 20 | - `teardown?`: after this [_Event_](https://effector.dev/en/api/effector/event/) all listeners will be removed, and the integration will be ready to be destroyed. 21 | 22 | ```ts 23 | import { trackScreenOrientation } from '@withease/web-api'; 24 | 25 | const { $type, $angle, $portrait, $landscape } = trackScreenOrientation({ 26 | setup: appStarted, 27 | }); 28 | ``` 29 | 30 | Returns an object with: 31 | 32 | - `$type`: [_Store_](https://effector.dev/docs/api/effector/store) with current orientation type, one of "portrait-primary", "portrait-secondary", "landscape-primary", or "landscape-secondary" 33 | - `$angle`: [_Store_](https://effector.dev/docs/api/effector/store) with a `number` which represents the current orientation angle in degrees 34 | - `$portrait`: [_Store_](https://effector.dev/docs/api/effector/store) with a `boolean` which states if device is in portrait orientation 35 | - `$landscape`: [_Store_](https://effector.dev/docs/api/effector/store) with a `boolean` which states if device is in landscape orientation 36 | 37 | ::: tip 38 | It supports [`@@trigger` protocol](/protocols/trigger). Since it allows firing only one [_Event_](https://effector.dev/en/api/effector/event/) `trackScreenOrientation` triggers any updates of `$type` as a `fired` in case of [`@@trigger` protocol](/protocols/trigger). 39 | 40 | ```ts 41 | import { trackScreenOrientation } from '@withease/web-api'; 42 | 43 | somethingExpectsTrigger(trackScreenOrientation); 44 | ``` 45 | 46 | ::: 47 | 48 | ## Live demo 49 | 50 | Let us show you a live demo of how it works. The following demo displays `$type`, `$angle`, `$portrait` and `$landscape` values of the current screen orientation. _Rotate your device to see how it works._ 51 | 52 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /apps/website/docs/statements/ecosystem.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Ecosystem 6 | 7 | The main goal of With Ease project is providing not only an information about Effector itself and its ecosystem but also to be an **opinionated guide** for developers who are using Effector in their projects. 8 | 9 | The part of this guidance is a list of libraries that could be used in your application with no fear of breaking changes or lack of support. We manually select libraries that are following the same principles as Effector itself: stability, performance, developer experience and maintenance. 10 | 11 | ## Stability 12 | 13 | Effector is [renowned for its stability](https://effector.dev/en/core-principles/releases/): major versions are released once a year or even less frequently, any breaking change is preceded by a deprecation warning for at least one year. 14 | 15 | All of the libraries in the list are following the same rules of releases as Effector itself (or even stricter). 16 | 17 | ::: tip 18 | Some of the libraries can be in their `0.x.x` version, but it doesn't mean that they are unstable. It means that the _API is not stabilized yet_, but its soundness is guaranteed and it is safe to use in production. Read the documentation of the library to get more information and make a decision. 19 | ::: 20 | 21 | ## Performance 22 | 23 | Effector is small and performant by design. It is a goal of the project to keep the bundle size as small as possible and to make the runtime as fast as possible. Only libraries that keeps in line with this principle are included in the list. They not necessarily should be small, but they should be designed _to be as small as possible_: no unnecessary dependencies, no unnecessary code, tree-shakable, and so on. 24 | 25 | ## Developer experience 26 | 27 | Effector has its unique API approach that is designed to be declarative and easy to work with. The libraries have to follow the spirit of Effector and provide a similar developer experience. They should integrate with Effector seamlessly, provide a clear and concise API aligned with Effector's principles, and provide a [best-in-class TS-support](/statements/typescript). 28 | 29 | ## Maintenance 30 | 31 | We believe that the library should be maintained well. It does not necessarily mean that the library should be updated every day (or should to be updated at all), but it should be maintained — issues should be answered, PRs should be reviewed, and the library should be updated when it is necessary. We do not respect stale-bots and auto-closing issues, we respect the human touch and the human approach to the library. 32 | 33 | ## Selected libraries 34 | 35 | 38 | -------------------------------------------------------------------------------- /apps/website/docs/web-api/media_query.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Media query 3 | --- 4 | 5 | # Media query 6 | 7 | Allows tracking any media query matching state with [_Events_](https://effector.dev/en/api/effector/event/) and [_Stores_](https://effector.dev/docs/api/effector/store). 8 | 9 | ::: info 10 | 11 | Uses [Window.matchMedia](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia) under the hood 12 | 13 | ::: 14 | 15 | ## Usage 16 | 17 | ### Single query 18 | 19 | All you need to do is to create an integration by calling `trackMediaQuery` with query to track an integration options: 20 | 21 | - `setup`: after this [_Event_](https://effector.dev/en/api/effector/event/) all listeners will be installed, and the integration will be ready to use; it is required because it is better to use [explicit initialization _Event_ in the application](/magazine/explicit_start). 22 | - `teardown?`: after this [_Event_](https://effector.dev/en/api/effector/event/) all listeners will be removed, and the integration will be ready to be destroyed. 23 | 24 | ```ts 25 | import { trackMediaQuery } from '@withease/web-api'; 26 | 27 | const { $matches, matched } = trackMediaQuery('(max-width: 600px)', { 28 | setup: appStarted, 29 | }); 30 | ``` 31 | 32 | Returns an object with: 33 | 34 | - `$matches`: [_Store_](https://effector.dev/docs/api/effector/store) with `true` if query is matches current state and `false` if it is not 35 | - `matched`: [_Event_](https://effector.dev/en/api/effector/event/) fired when query starts to match current state 36 | 37 | ::: tip 38 | It supports [`@@trigger` protocol](/protocols/trigger). Since it allows firing only one [_Event_](https://effector.dev/en/api/effector/event/) `trackMediaQuery` triggers `matched` as a `fired` in case of [`@@trigger` protocol](/protocols/trigger). 39 | 40 | ```ts 41 | import { trackMediaQuery } from '@withease/web-api'; 42 | 43 | somethingExpectsTrigger(trackMediaQuery('(max-width: 600px)')); 44 | ``` 45 | 46 | To use it as a `@@trigger` protocol you do not have to pass `setup` and `teardown` options. 47 | 48 | ::: 49 | 50 | ### Multiple queries 51 | 52 | You can track multiple queries by calling `trackMediaQueries` with queries to track and integration options: 53 | 54 | ```ts 55 | import { trackMediaQuery } from '@withease/web-api'; 56 | 57 | const { mobile, desktop } = trackMediaQuery( 58 | { mobile: '(max-width: 600px)', desktop: '(min-width: 601px)' }, 59 | { setup: appStarted } 60 | ); 61 | 62 | mobile.$matches; // Store 63 | mobile.matched; // Event 64 | 65 | desktop.$matches; // Store 66 | desktop.matched; // Event 67 | ``` 68 | 69 | ## Live demo 70 | 71 | Let us show you a live demo of how it works. The following demo displays a `$matches` value of the query in the screen. _Change the screen size to see how it works._ 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /apps/web-api-demo/test/geolocation.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | const GEOLOCATION_PAGE = '/geolocation.html'; 4 | 5 | test.use({ 6 | geolocation: { longitude: 41.890221, latitude: 12.492348 }, 7 | permissions: ['geolocation'], 8 | }); 9 | 10 | test('request', async ({ page, context }) => { 11 | await page.goto(GEOLOCATION_PAGE); 12 | 13 | const latitudeContainer = await page.$('#latitude'); 14 | const longitudeContainer = await page.$('#longitude'); 15 | const getLocationButton = await page.$('#get-location'); 16 | 17 | // By default it should be null 18 | expect(await latitudeContainer!.textContent()).toBe('null'); 19 | expect(await longitudeContainer!.textContent()).toBe('null'); 20 | 21 | // After requesting the location, it should be updated 22 | await getLocationButton!.click(); 23 | expect(await latitudeContainer!.textContent()).toBe('12.492348'); 24 | expect(await longitudeContainer!.textContent()).toBe('41.890221'); 25 | 26 | // Change geolocation, values should NOT be updated 27 | await context.setGeolocation({ longitude: 22.492348, latitude: 32.890221 }); 28 | expect(await latitudeContainer!.textContent()).toBe('12.492348'); 29 | expect(await longitudeContainer!.textContent()).toBe('41.890221'); 30 | // Request the location again, values should be updated 31 | await getLocationButton!.click(); 32 | expect(await latitudeContainer!.textContent()).toBe('32.890221'); 33 | expect(await longitudeContainer!.textContent()).toBe('22.492348'); 34 | }); 35 | 36 | test('watch', async ({ page, context }) => { 37 | await page.goto(GEOLOCATION_PAGE); 38 | 39 | const latitudeContainer = await page.$('#latitude'); 40 | const longitudeContainer = await page.$('#longitude'); 41 | const startWatchingButton = await page.$('#start-watching'); 42 | const stopWatchingButton = await page.$('#stop-watching'); 43 | 44 | // By default it should be null 45 | expect(await latitudeContainer!.textContent()).toBe('null'); 46 | expect(await longitudeContainer!.textContent()).toBe('null'); 47 | 48 | // After watching the location, it should be updated immediately 49 | await startWatchingButton!.click(); 50 | expect(await latitudeContainer!.textContent()).toBe('12.492348'); 51 | expect(await longitudeContainer!.textContent()).toBe('41.890221'); 52 | 53 | // Change geolocation, values should be updated immediately 54 | await context.setGeolocation({ longitude: 22.492348, latitude: 32.890221 }); 55 | expect(await latitudeContainer!.textContent()).toBe('32.890221'); 56 | expect(await longitudeContainer!.textContent()).toBe('22.492348'); 57 | 58 | // Stop watching and change geolocation, values should NOT be updated 59 | await stopWatchingButton!.click(); 60 | await context.setGeolocation({ longitude: 42.492348, latitude: 52.890221 }); 61 | expect(await latitudeContainer!.textContent()).toBe('32.890221'); 62 | expect(await longitudeContainer!.textContent()).toBe('22.492348'); 63 | }); 64 | -------------------------------------------------------------------------------- /apps/website/docs/protocols/trigger.md: -------------------------------------------------------------------------------- 1 | # `@@trigger` 2 | 3 | Protocol that allows start watching some external trigger and react on it with universal API. 4 | 5 | ::: tip Packages that use `@@trigger` 6 | 7 | - [`@farfetched/core`](https://farfetched.pages.dev/tutorial/trigger_api.html#external-triggers) 8 | 9 | ::: 10 | 11 | ::: tip Known `@@trigger` 12 | 13 | - all integrations from [`@withease/web-api`](/web-api/) 14 | - method `interval` from [`patronum`](https://patronum.effector.dev/methods/interval/) 15 | 16 | ::: 17 | 18 | ## Formulae 19 | 20 | Trigger is an any object with the field `@@trigger` that a function that returns an object with fields: 21 | 22 | - `fired`: [_Event_](https://effector.dev/en/api/effector/event/), external consumers will listen it to determine when trigger was activated 23 | - `setup`: [_EventCallable_](https://effector.dev/en/api/effector/event/), external consumers will call it to set up trigger 24 | - `teardown`: [_EventCallable_](https://effector.dev/en/api/effector/event/), external consumers will call it to stop trigger 25 | 26 | ::: tip 27 | [_Events_](https://effector.dev/en/api/effector/event/) `setup` and `teardown` are presented in protocol, because it is better to provide [explicit start of the application](/magazine/explicit_start). 28 | ::: 29 | 30 | ## Single `fired` 31 | 32 | Since `@@trigger` supports only one `fired` [_Event_](https://effector.dev/en/api/effector/event/), any operator that supports `@@trigger` protocol has to choose reasonable [_Event_](https://effector.dev/en/api/effector/event/) to use it as `fired`. 33 | 34 | E.g., [`trackPageVisibility`](/web-api/page_visibility) returns [_Events_](https://effector.dev/en/api/effector/event/) `visible` and `hidden`, but `visible` seems more reasonable `fired` [_Event_](https://effector.dev/en/api/effector/event/). 35 | 36 | ## Example 37 | 38 | Let's create simple trigger that will be activated every second after starting: 39 | 40 | ```ts 41 | import { 42 | createEvent, 43 | createStore, 44 | createEffect, 45 | scopeBind, 46 | sample, 47 | } from 'effector'; 48 | 49 | const intervalTrigger = { 50 | '@@trigger': () => { 51 | const setup = createEvent(); 52 | const fired = createEvent(); 53 | const teardown = createEvent(); 54 | 55 | const $interval = createStore(null); 56 | 57 | const startInternalFx = createEffect(() => { 58 | const boundFired = scopeBind(fired, { safe: true }); 59 | 60 | return setInterval(boundFired, 1000); 61 | }); 62 | 63 | const stopIntervalFx = createEffect(clearInterval); 64 | 65 | sample({ clock: setup, target: startInternalFx }); 66 | sample({ clock: startIntervalFx.doneData, target: $interval }); 67 | sample({ clock: teardown, source: $interval, target: stopIntervalFx }); 68 | sample({ clock: stopIntervalFx.done, target: $interval.reinit }); 69 | 70 | return { setup, fired }; 71 | }, 72 | }; 73 | ``` 74 | 75 | That is it, we can use `intervalTrigger` everywhere as a trigger! 76 | -------------------------------------------------------------------------------- /packages/web-api/src/shared.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Event, 3 | type Store, 4 | type EventCallable, 5 | type StoreWritable, 6 | attach, 7 | createEffect, 8 | createEvent, 9 | createStore, 10 | sample, 11 | scopeBind, 12 | } from 'effector'; 13 | 14 | export type Setupable = { 15 | setup: Event; 16 | teardown?: Event; 17 | }; 18 | 19 | export function readValue(getter: () => T, defaultValue: T): T { 20 | try { 21 | const value = getter(); 22 | 23 | if (value === undefined) { 24 | return defaultValue; 25 | } 26 | 27 | return value; 28 | } catch (e) { 29 | return defaultValue; 30 | } 31 | } 32 | 33 | export function setupListener( 34 | { 35 | add, 36 | remove, 37 | readPayload, 38 | }: { 39 | add: (listener: (value: any) => void) => void; 40 | remove: (listener: (value: any) => void) => void; 41 | readPayload: () => T; 42 | }, 43 | config: Setupable 44 | ): Event; 45 | 46 | export function setupListener( 47 | { 48 | add, 49 | remove, 50 | }: { 51 | add: (listener: (value: T extends void ? any : T) => void) => void; 52 | remove: (listener: (value: T extends void ? any : T) => void) => void; 53 | }, 54 | config: Setupable 55 | ): Event; 56 | 57 | export function setupListener( 58 | { 59 | add, 60 | remove, 61 | readPayload, 62 | }: { 63 | add: (listener: (value: T) => void) => void; 64 | remove: (listener: (value: T) => void) => void; 65 | readPayload?: () => T; 66 | }, 67 | config: Setupable 68 | ): Event { 69 | const event = createEvent(); 70 | 71 | const $subscription = createStore<((value: T) => void) | null>(null, { 72 | serialize: 'ignore', 73 | }); 74 | 75 | const startWatchingFx = createEffect(() => { 76 | const boundEvent = scopeBind(event, { safe: true }); 77 | let listener = boundEvent; 78 | 79 | if (readPayload) { 80 | listener = () => boundEvent(readPayload()); 81 | } 82 | 83 | add(listener); 84 | 85 | return listener; 86 | }); 87 | 88 | const stopWatchingFx = attach({ 89 | source: $subscription, 90 | effect(subscription) { 91 | if (!subscription) return; 92 | remove(subscription); 93 | }, 94 | }); 95 | 96 | sample({ clock: config.setup, target: startWatchingFx }); 97 | sample({ 98 | clock: startWatchingFx.doneData, 99 | filter: Boolean, 100 | target: $subscription, 101 | }); 102 | 103 | if (config.teardown) { 104 | sample({ clock: config.teardown, target: stopWatchingFx }); 105 | } 106 | sample({ clock: stopWatchingFx.done, target: $subscription.reinit! }); 107 | 108 | return event; 109 | } 110 | 111 | export function readonly(unit: StoreWritable): Store; 112 | export function readonly(unit: EventCallable): Event; 113 | 114 | export function readonly(unit: any) { 115 | return unit.map((v: any) => v); 116 | } 117 | -------------------------------------------------------------------------------- /packages/i18next/src/reporting.test.ts: -------------------------------------------------------------------------------- 1 | import { allSettled, createEvent, createStore, fork } from 'effector'; 2 | import { createInstance } from 'i18next'; 3 | import { describe, expect, test, vi } from 'vitest'; 4 | 5 | import { createI18nextIntegration } from './integration'; 6 | 7 | describe('integration.reporting.missingKey', () => { 8 | test('do not call on exists key', async () => { 9 | const instance = createInstance({ 10 | resources: { th: { common: { key: 'value' } } }, 11 | lng: 'th', 12 | saveMissing: true, 13 | }); 14 | 15 | const listener = vi.fn(); 16 | 17 | const setup = createEvent(); 18 | 19 | const { $t, reporting } = createI18nextIntegration({ 20 | instance, 21 | setup, 22 | }); 23 | 24 | reporting.missingKey.watch(listener); 25 | 26 | const $result = $t.map((t) => t('common:key') ?? null); 27 | 28 | const scope = fork(); 29 | 30 | await allSettled(setup, { scope }); 31 | 32 | expect(scope.getState($result)).toBe('value'); 33 | expect(listener).not.toBeCalled(); 34 | }); 35 | 36 | test('call on absent key', async () => { 37 | const instance = createInstance({ 38 | resources: { th: { common: { key: 'value' } } }, 39 | lng: 'th', 40 | saveMissing: true, 41 | }); 42 | 43 | const listener = vi.fn(); 44 | 45 | const setup = createEvent(); 46 | 47 | const { $t, reporting } = createI18nextIntegration({ 48 | instance, 49 | setup, 50 | }); 51 | 52 | reporting.missingKey.watch(listener); 53 | 54 | const $result = $t.map((t) => t('common:other_key') ?? null); 55 | 56 | const scope = fork(); 57 | 58 | await allSettled(setup, { scope }); 59 | 60 | expect(scope.getState($result)).toBe('other_key'); 61 | expect(listener).toBeCalled(); 62 | expect(listener).toBeCalledWith({ 63 | key: 'other_key', 64 | lngs: ['dev'], 65 | namespace: 'common', 66 | res: 'other_key', 67 | }); 68 | }); 69 | 70 | test('stop calling after teardown', async () => { 71 | const instance = createInstance({ 72 | resources: { th: { common: { key: 'value' } } }, 73 | lng: 'th', 74 | saveMissing: true, 75 | }); 76 | 77 | const listener = vi.fn(); 78 | 79 | const setup = createEvent(); 80 | const teardown = createEvent(); 81 | 82 | const { reporting, translated } = createI18nextIntegration({ 83 | instance, 84 | setup, 85 | teardown, 86 | }); 87 | 88 | const $key = createStore('other_key'); 89 | 90 | reporting.missingKey.watch(listener); 91 | 92 | const $result = translated`common:${$key}`; 93 | 94 | const scope = fork(); 95 | 96 | await allSettled(setup, { scope }); 97 | 98 | expect(scope.getState($result)).toBe('other_key'); 99 | expect(listener).toBeCalledTimes(1); 100 | 101 | await allSettled(teardown, { scope }); 102 | await allSettled($key, { scope, params: 'one_more_key' }); 103 | 104 | expect(listener).toBeCalledTimes(1); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /packages/web-api/src/media_query.ts: -------------------------------------------------------------------------------- 1 | import { 2 | sample, 3 | createStore, 4 | createEvent, 5 | type Event, 6 | type Store, 7 | } from 'effector'; 8 | 9 | import { readValue, setupListener, type Setupable } from './shared'; 10 | import { type TriggerProtocol } from './trigger_protocol'; 11 | 12 | type Result = { 13 | $matches: Store; 14 | matched: Event; 15 | }; 16 | 17 | type Query = string; 18 | 19 | function trackMediaQuery(mq: Query, c: Setupable): Result; 20 | function trackMediaQuery( 21 | mq: Query 22 | ): ((c: Setupable) => Result) & TriggerProtocol; 23 | 24 | function trackMediaQuery>( 25 | mq: T, 26 | c: Setupable 27 | ): { [key in keyof T]: Result }; 28 | function trackMediaQuery>( 29 | mq: T 30 | ): { [key in keyof T]: ((c: Setupable) => Result) & TriggerProtocol }; 31 | 32 | function trackMediaQuery( 33 | mq: Query | Record, 34 | config?: Setupable 35 | ): any { 36 | // single query 37 | if (typeof mq === 'string') { 38 | if (config) { 39 | return tracker(mq, config); 40 | } else { 41 | const track = (finalConfig: Setupable) => tracker(mq, finalConfig); 42 | 43 | track['@@trigger'] = () => { 44 | const setup = createEvent(); 45 | const teardown = createEvent(); 46 | 47 | const { matched } = tracker(mq, { setup, teardown }); 48 | 49 | return { setup, teardown, fired: matched }; 50 | }; 51 | 52 | return track; 53 | } 54 | } 55 | // multiple queries 56 | else { 57 | if (config) { 58 | const resuls = {} as Record; 59 | 60 | for (const [mqKey, mqValue] of Object.entries(mq)) { 61 | resuls[mqKey] = trackMediaQuery(mqValue, config); 62 | } 63 | 64 | return resuls; 65 | } else { 66 | const results = {} as Record Result>; 67 | 68 | for (const [mqKey, mqValue] of Object.entries(mq)) { 69 | results[mqKey] = (finalConfig: Setupable) => 70 | trackMediaQuery(mqValue, finalConfig); 71 | } 72 | 73 | return results; 74 | } 75 | } 76 | } 77 | 78 | function tracker(query: string, config: Setupable): Result { 79 | const mq = readValue(() => window.matchMedia(query), null); 80 | 81 | const changed = setupListener( 82 | { 83 | add: (listener) => mq?.addEventListener('change', listener), 84 | remove: (listener) => mq?.removeEventListener('change', listener), 85 | }, 86 | config 87 | ); 88 | 89 | const $matches = createStore(mq?.matches ?? false, { 90 | serialize: 'ignore', 91 | }).on(changed, (_, event) => event.matches); 92 | 93 | const matched = createEvent(); 94 | 95 | sample({ 96 | clock: [$matches.updates, config.setup], 97 | filter: $matches, 98 | fn: (): void => { 99 | // ... 100 | }, 101 | target: matched, 102 | }); 103 | 104 | return { $matches, matched }; 105 | } 106 | 107 | export { trackMediaQuery }; 108 | -------------------------------------------------------------------------------- /apps/website/docs/magazine/dependency_injection.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Dependency injection 3 | date: 2023-06-19 4 | --- 5 | 6 | # Dependency injection 7 | 8 | Effector provides a simple way to inject dependencies into your application — Fork API. Let us take a look at how it works. 9 | 10 | :::tip 11 | Application has to follow [some rules to work with Fork API](/magazine/fork_api_rules) 12 | ::: 13 | 14 | ## Why 15 | 16 | Sometimes you need to inject some dependencies into your application in particular environment. For example, you want to disable logger in tests. The easiest way to do it is to declare global variable and check it in your code: 17 | 18 | ```ts{4} 19 | // app.ts 20 | import { createEffect } from "effector"; 21 | 22 | const logEnabled = Boolean(process.env.IS_TEST); 23 | 24 | const logFx = createEffect((message) => { 25 | if (!logEnabled) { 26 | return; 27 | } 28 | 29 | console.log(message); 30 | }); 31 | 32 | sample({ clock: somethingHappened, target: logFx }); 33 | ``` 34 | 35 | But it is not the best way. What if we want to enable it back for a particular test? We have to change the code and support one more variable. So, it will lead to a mess in the code. 36 | 37 | Other reason is that you may want to use different implementations of a logger in different environments. For example, in browser you want to send logs to some external system (like Rollbar or Sentry) and on server you want to write logs to `stdout`. 38 | 39 | ## How 40 | 41 | To solve these problems we can use Fork API. It allows us to create a new instance of the application with different dependencies. Let us take a look at how it works. 42 | 43 | ```ts 44 | // app.ts 45 | 46 | // Store instance of a logger in a Store 47 | const $logger = createStore(null); 48 | 49 | const logFx = attach({ 50 | source: $logger, 51 | effect: (logger, message) => logger?.(message), 52 | }); 53 | 54 | sample({ clock: somethingHappened, target: logFx }); 55 | ``` 56 | 57 | That is it, now we can inject logger into our application. 58 | 59 | ::: code-group 60 | 61 | ```ts{5-7} [In tests] 62 | import { fork, allSettled } from "effector"; 63 | 64 | describe("app", () => { 65 | it("should not log anything", async () => { 66 | const scope = fork({ 67 | values: [[$logger, null]], 68 | }); 69 | 70 | await allSettled(somethingHappened, { scope }); 71 | 72 | expect(console.log).not.toBeCalled(); 73 | }); 74 | }); 75 | ``` 76 | 77 | ```ts{4-6} [On server] 78 | import { fork, allSettled } from "effector"; 79 | 80 | function handleHttp(req, res) { 81 | const scope = fork({ 82 | values: [[$logger, console.log]], 83 | }); 84 | 85 | await allSettled(somethingHappened, { scope }); 86 | 87 | // render the app 88 | } 89 | ``` 90 | 91 | ```ts{3-5} [In browser] 92 | import { fork, allSettled } from "effector"; 93 | 94 | const scope = fork({ 95 | values: [[$logger, Rollbar.log]], 96 | }); 97 | 98 | await allSettled(somethingHappened, { scope }); 99 | ``` 100 | 101 | ::: 102 | 103 | We can inject any dependencies into our application in particular environment without changing the code. 104 | 105 | ## Recap 106 | 107 | - Follow [the rules](/magazine/fork_api_rules) to work with Fork API 108 | - Use Fork API as a dependency injection 109 | -------------------------------------------------------------------------------- /packages/factories/src/issue-33.test-d.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expectTypeOf } from 'vitest'; 2 | import { 3 | type Validator, 4 | type SourcedField, 5 | type DynamicallySourcedField, 6 | type ParamsDeclaration, 7 | type Query, 8 | } from '@farfetched/core'; 9 | import { type Json, type Store } from 'effector'; 10 | import { type Runtype, Record, Number } from 'runtypes'; 11 | 12 | import { createFactory } from './create_factory'; 13 | import { invoke } from './invoke'; 14 | 15 | describe('factories, issue #33', () => { 16 | test('infer complex type', () => { 17 | const createAriadneQuery = createFactory(ariadneData); 18 | 19 | const resultQuery = invoke(() => 20 | createAriadneQuery({ 21 | graphQL: { 22 | query: 'Some Query', 23 | operationName: 'map_v2', 24 | variables: {} as Store, 25 | }, 26 | contract: Record({ map_v2: Record({ val: Number }) }), 27 | mapData: (response) => response.map_v2, 28 | }) 29 | ); 30 | 31 | expectTypeOf(resultQuery).toMatchTypeOf< 32 | Query< 33 | void, 34 | { 35 | val: number; 36 | }, 37 | unknown, 38 | null 39 | > 40 | >(); 41 | }); 42 | }); 43 | 44 | interface BaseAriadneDataConfig< 45 | Params, 46 | P, 47 | D, 48 | S, 49 | ValidatorSource = void, 50 | OperatorSource = void 51 | > { 52 | graphQL: { 53 | query: string; 54 | variables?: SourcedField; 55 | operationName: string; 56 | }; 57 | contract: Runtype

; 58 | mapData: DynamicallySourcedField; 59 | validate?: Validator<{ data: P }, Params, ValidatorSource>; 60 | 61 | enabled?: Store; 62 | } 63 | 64 | // With params and no initialData 65 | function ariadneData< 66 | Params, 67 | P, 68 | D, 69 | S, 70 | ValidatorSource = void, 71 | OperatorSource = void 72 | >( 73 | config: { params: ParamsDeclaration } & BaseAriadneDataConfig< 74 | Params, 75 | P, 76 | D, 77 | S, 78 | ValidatorSource, 79 | OperatorSource 80 | > 81 | ): Query; 82 | 83 | // no params and no initialData 84 | function ariadneData< 85 | _Params, 86 | P, 87 | D, 88 | S, 89 | ValidatorSource = void, 90 | OperatorSource = void 91 | >( 92 | config: BaseAriadneDataConfig 93 | ): Query; 94 | 95 | // With params and initialData 96 | function ariadneData< 97 | Params, 98 | P, 99 | D, 100 | S, 101 | ValidatorSource = void, 102 | OperatorSource = void 103 | >( 104 | config: { 105 | initialData: D; 106 | params: ParamsDeclaration; 107 | } & BaseAriadneDataConfig 108 | ): Query; 109 | 110 | // no params and initialData 111 | function ariadneData< 112 | _Params, 113 | P, 114 | D, 115 | S, 116 | ValidatorSource = void, 117 | OperatorSource = void 118 | >( 119 | config: { 120 | initialData: D; 121 | } & BaseAriadneDataConfig 122 | ): Query; 123 | 124 | function ariadneData(config: any): any { 125 | return {} as any; 126 | } 127 | -------------------------------------------------------------------------------- /packages/i18next/src/is_ready.test.ts: -------------------------------------------------------------------------------- 1 | import { allSettled, createEvent, createStore, fork } from 'effector'; 2 | import { createInstance, type i18n } from 'i18next'; 3 | import { describe, expect, test } from 'vitest'; 4 | 5 | import { createI18nextIntegration } from './integration'; 6 | 7 | describe('integration.$isReady', () => { 8 | test('not ready if not initialized', async () => { 9 | const setup = createEvent(); 10 | 11 | const { $isReady } = createI18nextIntegration({ 12 | instance: createStore(null), 13 | setup, 14 | }); 15 | 16 | const scope = fork(); 17 | 18 | expect(scope.getState($isReady)).toBeFalsy(); 19 | }); 20 | 21 | test('not ready if initialized without instance', async () => { 22 | const setup = createEvent(); 23 | 24 | const { $isReady } = createI18nextIntegration({ 25 | instance: createStore(null), 26 | setup, 27 | }); 28 | 29 | const scope = fork(); 30 | 31 | await allSettled(setup, { scope }); 32 | 33 | expect(scope.getState($isReady)).toBeFalsy(); 34 | }); 35 | 36 | test('ready after initialized with instance (static)', async () => { 37 | const instance = createInstance({ 38 | resources: { th: { common: { foo: 'bar' } } }, 39 | lng: 'th', 40 | }); 41 | 42 | const setup = createEvent(); 43 | 44 | const { $isReady } = createI18nextIntegration({ 45 | instance, 46 | setup, 47 | }); 48 | 49 | const scope = fork(); 50 | 51 | expect(scope.getState($isReady)).toBeFalsy(); 52 | 53 | await allSettled(setup, { scope }); 54 | 55 | expect(scope.getState($isReady)).toBeTruthy(); 56 | }); 57 | 58 | test('ready after initialized with instance (store)', async () => { 59 | const instance = createInstance({ 60 | resources: { th: { common: { foo: 'bar' } } }, 61 | lng: 'th', 62 | }); 63 | 64 | const setup = createEvent(); 65 | 66 | const { $isReady } = createI18nextIntegration({ 67 | instance: createStore(instance), 68 | setup, 69 | }); 70 | 71 | const scope = fork(); 72 | 73 | expect(scope.getState($isReady)).toBeFalsy(); 74 | 75 | await allSettled(setup, { scope }); 76 | 77 | expect(scope.getState($isReady)).toBeTruthy(); 78 | }); 79 | 80 | test('ready after initialized with instance (lazy store)', async () => { 81 | const $instance = createStore(null); 82 | const instance = createInstance({ 83 | resources: { th: { common: { foo: 'bar' } } }, 84 | lng: 'th', 85 | }); 86 | 87 | const setup = createEvent(); 88 | 89 | const { $isReady } = createI18nextIntegration({ 90 | instance: $instance, 91 | setup, 92 | }); 93 | 94 | const scope = fork(); 95 | 96 | expect(scope.getState($isReady)).toBeFalsy(); 97 | 98 | await allSettled(setup, { scope }); 99 | expect(scope.getState($isReady)).toBeFalsy(); 100 | 101 | await allSettled($instance, { scope, params: instance }); 102 | expect(scope.getState($isReady)).toBeTruthy(); 103 | }); 104 | 105 | test('not ready after teardown', async () => { 106 | const instance = createInstance({ 107 | resources: { th: { common: { foo: 'bar' } } }, 108 | lng: 'th', 109 | }); 110 | 111 | const setup = createEvent(); 112 | const teardown = createEvent(); 113 | 114 | const { $isReady } = createI18nextIntegration({ 115 | instance, 116 | setup, 117 | teardown, 118 | }); 119 | 120 | const scope = fork(); 121 | 122 | expect(scope.getState($isReady)).toBeFalsy(); 123 | 124 | await allSettled(setup, { scope }); 125 | expect(scope.getState($isReady)).toBeTruthy(); 126 | 127 | await allSettled(teardown, { scope }); 128 | expect(scope.getState($isReady)).toBeFalsy(); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /packages/factories/src/invoke.ts: -------------------------------------------------------------------------------- 1 | import { factoryCalledDirectly, invokeAcceptsOnlyFactories } from './errors'; 2 | 3 | /* 4 | * The following variables are used for checking that factory is called inside invoke function on correct nesting level 5 | */ 6 | export let invokeLevel = 0; 7 | let invokeCount = 0; 8 | let factoryCalledCount = 0; 9 | 10 | /** 11 | * Have to be called inside factory created by createFactory 12 | * @private 13 | */ 14 | export function markFactoryAsCalled() { 15 | factoryCalledCount += 1; 16 | } 17 | 18 | export function invoke any>( 19 | factory: C 20 | ): OverloadReturn>; 21 | 22 | export function invoke< 23 | C extends (...args: any) => any, 24 | P extends OverloadParameters[0] 25 | >(factory: C, params: P): OverloadReturn>; 26 | 27 | export function invoke< 28 | C extends (...args: any) => any, 29 | P extends OverloadParameters[0] 30 | >(factory: C, params?: P): OverloadReturn> { 31 | /* Increase invoke level before factory calling */ 32 | invokeLevel += 1; 33 | invokeCount += 1; 34 | 35 | const result = factory(params); 36 | 37 | /* And descrese in after */ 38 | invokeLevel -= 1; 39 | 40 | const haveToThrowBecauseOfCalledFactory = factoryCalledCount === 0; 41 | let haveToThrowErrorBecauseInvokeLevel = false; 42 | 43 | if (invokeLevel === 0 /* Ending of nexted invoke calls */) { 44 | haveToThrowErrorBecauseInvokeLevel /* Amount of invokes and factoies does not match */ = 45 | factoryCalledCount !== invokeCount; 46 | 47 | /* Reset related variables */ 48 | factoryCalledCount = 0; 49 | invokeCount = 0; 50 | } 51 | 52 | if (haveToThrowBecauseOfCalledFactory) { 53 | throw invokeAcceptsOnlyFactories(); 54 | } 55 | 56 | if (haveToThrowErrorBecauseInvokeLevel) { 57 | throw factoryCalledDirectly(); 58 | } 59 | 60 | return result; 61 | } 62 | 63 | /* 64 | * The reason for the following types is that by default TypeScript does not 65 | * support overloaded functions in generics — https://github.com/microsoft/TypeScript/issues/14107 66 | * 67 | * But we need to support overloads in `invoke` function because it is used in 68 | * many factories in Effector's ecosystem and we don't want to break them. 69 | * 70 | * The following types are adapted from the following comment: 71 | * https://github.com/microsoft/TypeScript/issues/14107#issuecomment-1146738780 72 | * 73 | * Changes from the original implementation: 74 | * 1. OverloadReturn were changed because the original implementation does not infer 75 | * exact return type but do infer union of all possible return types. 76 | * 2. OverloadReturn supports only single-agrument functions because @withease/factories 77 | * does not support multiple arguments in general. 78 | */ 79 | 80 | type OverloadProps = Pick; 81 | 82 | type OverloadUnionRecursive< 83 | TOverload, 84 | TPartialOverload = unknown 85 | > = TOverload extends (...args: infer TArgs) => infer TReturn 86 | ? TPartialOverload extends TOverload 87 | ? never 88 | : 89 | | OverloadUnionRecursive< 90 | TPartialOverload & TOverload, 91 | TPartialOverload & 92 | ((...args: TArgs) => TReturn) & 93 | OverloadProps 94 | > 95 | | ((...args: TArgs) => TReturn) 96 | : never; 97 | 98 | type OverloadUnion any> = Exclude< 99 | OverloadUnionRecursive<(() => never) & TOverload>, 100 | TOverload extends () => never ? never : () => never 101 | >; 102 | 103 | type OverloadParameters any> = Parameters< 104 | OverloadUnion 105 | >; 106 | 107 | type OverloadReturn any> = 108 | /* 109 | * Function with no arguments (() => any) is a special case 110 | * because it extends (...args: any[]) => any in TypeScript. 111 | * 112 | * So we need to handle it separately to prevent incorrect inference. 113 | */ 114 | F extends () => any 115 | ? /* 116 | * In case of functions without arguments 117 | * we need to return ReturnType because it is the only possible return type. 118 | */ 119 | P extends void 120 | ? ReturnType 121 | : never 122 | : /* 123 | * In case of function with single argument 124 | * We need to find correct overload and return its return type. 125 | */ 126 | F extends (params: P) => infer R 127 | ? R 128 | : never; 129 | -------------------------------------------------------------------------------- /apps/website/docs/magazine/no_methods.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Prefer Operators to Methods 3 | date: 2024-01-26 4 | --- 5 | 6 | # Prefer Operators to Methods 7 | 8 | In Effector, there are two ways to create a new unit from an existing one: 9 | 10 | - Methods, e.g. `event.map(...)`, `event.filter(...)`, `store.map(...)` 11 | - Operators, e.g. `combine(...)` and `sample(...)` 12 | 13 | In most cases, operators are more powerful and flexible than methods. You can add new features to operators without rewriting the code. Let us see how it works on a few examples. 14 | 15 | ## `combine` 16 | 17 | Let us say you have a derived [_Store_](https://effector.dev/docs/api/effector/store) to calculate a discount percentage for user: 18 | 19 | ```ts 20 | const $discountPercentage = $user.map((user) => { 21 | if (user.isPremium) return 20; 22 | return 0; 23 | }); 24 | ``` 25 | 26 | Some time later, you need to add a new feature: use current market conditions to calculate a discount percentage. In this case, you will need to completely rewrite the code: 27 | 28 | ```ts 29 | /* [!code --:4] */ const $discountPercentage = $user.map((user) => { 30 | if (user.isPremium) return 20; 31 | return 0; 32 | }); 33 | 34 | /* [!code ++:8] */ const $discountPercentage = combine( 35 | { user: $user, market: $market }, 36 | ({ user, market }) => { 37 | if (user.isPremium) return 20; 38 | if (market.isChristmas) return 10; 39 | return 0; 40 | } 41 | ); 42 | ``` 43 | 44 | But if you use `combine` from the very beginning, you will be able to add a new feature without rewriting the code: 45 | 46 | ```ts 47 | const $discountPercentage = combine( 48 | { 49 | user: $user, 50 | market: $market, // [!code ++] 51 | }, 52 | ({ user, market }) => { 53 | if (user.isPremium) return 20; 54 | if (market.isChristmas) return 10; // [!code ++] 55 | return 0; 56 | } 57 | ); 58 | ``` 59 | 60 | ## `sample` 61 | 62 | It is even more noticeable when you need to filter an [_Event_](https://effector.dev/en/api/effector/event/) by a payload. Let us say you have an [_Event_](https://effector.dev/en/api/effector/event/) representing form submission and derived [_Event_](https://effector.dev/en/api/effector/event/) representing valid form submission: 63 | 64 | ```ts 65 | const formSubmitted = createEvent(); 66 | 67 | const validFormSubmitted = formSubmitted.filter({ 68 | fn: (form) => { 69 | return form.isValid(); 70 | }, 71 | }); 72 | ``` 73 | 74 | Some time later, you need to add a new feature: use external service to validate form instead of using `isValid` method. In this case, you will need to completely rewrite the code: 75 | 76 | ```ts 77 | /* [!code --:5] */ const validFormSubmitted = formSubmitted.filter({ 78 | fn: (form) => { 79 | return form.isValid(); 80 | }, 81 | }); 82 | 83 | /* [!code ++:5] */ const validFormSubmitted = sample({ 84 | clock: formSubmitted, 85 | source: $externalValidator, 86 | filter: (validator, form) => validator(form), 87 | }); 88 | ``` 89 | 90 | But if you use `sample` from the very beginning, you will be able to add a new feature without rewriting the code: 91 | 92 | ```ts 93 | const validFormSubmitted = sample({ 94 | clock: formSubmitted, 95 | filter: (form) => form.isValid(), // [!code --] 96 | source: $externalValidator, // [!code ++] 97 | filter: (validator, form) => validator(form), // [!code ++] 98 | }); 99 | ``` 100 | 101 | With a `sample` we can go even further and add payload transformation just by adding a new argument: 102 | 103 | ```ts 104 | const validFormSubmitted = sample({ 105 | clock: formSubmitted, 106 | source: $externalValidator, 107 | filter: (validator, form) => validator(form), 108 | fn: (_, form) => form.toJson(), // [!code ++] 109 | }); 110 | ``` 111 | 112 | Cool, right? But it is not the end. We can add a new feature: use external [_Store_](https://effector.dev/docs/api/effector/store) to enrich the payload: 113 | 114 | ```ts 115 | const validFormSubmitted = sample({ 116 | clock: formSubmitted, 117 | source: { 118 | validator: $externalValidator, 119 | userName: $userName, // [!code ++] 120 | }, 121 | filter: ({ validator }, form) => validator(form), 122 | fn: ({ userName }, form) => ({ 123 | ...form.toJson(), 124 | userName, // [!code ++] 125 | }), 126 | }); 127 | ``` 128 | 129 | ## Summary 130 | 131 | Prefer `sample` to `event.filter`/`event.map` and `combine` to `store.map` to make your code more extensible and transformable. 132 | 133 | ::: tip Exception 134 | 135 | There is only one exception when you have to use method instead of operator: `event.prepend(...)` does not have an operator equivalent. 136 | 137 | ::: 138 | -------------------------------------------------------------------------------- /packages/i18next/src/translated.test.ts: -------------------------------------------------------------------------------- 1 | import { allSettled, createEvent, createStore, fork } from 'effector'; 2 | import { createInstance } from 'i18next'; 3 | import { describe, expect, test } from 'vitest'; 4 | 5 | import { createI18nextIntegration } from './integration'; 6 | 7 | describe('integration.translated', () => { 8 | describe('overload: key', () => { 9 | test('supports simple key', async () => { 10 | const instance = createInstance({ 11 | resources: { th: { common: { key: 'valueOne' } } }, 12 | lng: 'th', 13 | }); 14 | 15 | const setup = createEvent(); 16 | 17 | const { translated } = createI18nextIntegration({ 18 | instance, 19 | setup, 20 | }); 21 | 22 | const $result = translated('common:key'); 23 | 24 | const scope = fork(); 25 | 26 | await allSettled(setup, { scope }); 27 | 28 | expect(scope.getState($result)).toBe('valueOne'); 29 | }); 30 | 31 | test('supports simple key and language change', async () => { 32 | const instance = createInstance({ 33 | resources: { 34 | th: { common: { key: 'valueOne' } }, 35 | en: { common: { key: 'valueTwo' } }, 36 | }, 37 | lng: 'th', 38 | }); 39 | 40 | const setup = createEvent(); 41 | 42 | const { translated, changeLanguageFx } = createI18nextIntegration({ 43 | instance, 44 | setup, 45 | }); 46 | 47 | const $result = translated('common:key'); 48 | 49 | const scope = fork(); 50 | 51 | await allSettled(setup, { scope }); 52 | 53 | expect(scope.getState($result)).toBe('valueOne'); 54 | 55 | await allSettled(changeLanguageFx, { scope, params: 'en' }); 56 | 57 | expect(scope.getState($result)).toBe('valueTwo'); 58 | }); 59 | }); 60 | 61 | describe('overload: template literal', () => { 62 | test('changes after key store changed', async () => { 63 | const instance = createInstance({ 64 | resources: { th: { common: { one: 'valueOne', two: 'valueTwo' } } }, 65 | lng: 'th', 66 | }); 67 | 68 | const setup = createEvent(); 69 | 70 | const { translated } = createI18nextIntegration({ 71 | instance, 72 | setup, 73 | }); 74 | 75 | const $key = createStore('one'); 76 | 77 | const $result = translated`common:${$key}`; 78 | 79 | const scope = fork(); 80 | 81 | await allSettled(setup, { scope }); 82 | 83 | expect(scope.getState($result)).toBe('valueOne'); 84 | 85 | await allSettled($key, { scope, params: 'two' }); 86 | 87 | expect(scope.getState($result)).toBe('valueTwo'); 88 | }); 89 | }); 90 | 91 | describe('overload: key with variables', () => { 92 | test('changes after variables store changed', async () => { 93 | const instance = createInstance({ 94 | resources: { th: { common: { key: 'valueOne {{name}}' } } }, 95 | lng: 'th', 96 | }); 97 | 98 | const setup = createEvent(); 99 | 100 | const { translated } = createI18nextIntegration({ 101 | instance, 102 | setup, 103 | }); 104 | 105 | const $name = createStore('wow'); 106 | 107 | const $result = translated('common:key', { name: $name }); 108 | 109 | const scope = fork(); 110 | 111 | await allSettled(setup, { scope }); 112 | 113 | expect(scope.getState($result)).toBe('valueOne wow'); 114 | 115 | await allSettled($name, { scope, params: 'kek' }); 116 | 117 | expect(scope.getState($result)).toBe('valueOne kek'); 118 | }); 119 | 120 | test('changes after language changed', async () => { 121 | const instance = createInstance({ 122 | resources: { 123 | th: { common: { key: 'valueOne {{name}}' } }, 124 | en: { common: { key: 'valueTwo {{name}}' } }, 125 | }, 126 | lng: 'th', 127 | }); 128 | 129 | const setup = createEvent(); 130 | 131 | const { translated, changeLanguageFx } = createI18nextIntegration({ 132 | instance, 133 | setup, 134 | }); 135 | 136 | const $name = createStore('wow'); 137 | 138 | const $result = translated('common:key', { name: $name }); 139 | 140 | const scope = fork(); 141 | 142 | await allSettled(setup, { scope }); 143 | 144 | expect(scope.getState($result)).toBe('valueOne wow'); 145 | 146 | await allSettled(changeLanguageFx, { scope, params: 'en' }); 147 | 148 | expect(scope.getState($result)).toBe('valueTwo wow'); 149 | 150 | await allSettled($name, { scope, params: 'kek' }); 151 | 152 | expect(scope.getState($result)).toBe('valueTwo kek'); 153 | }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /apps/website/docs/web-api/preferred_languages.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Preferred languages 3 | --- 4 | 5 | # Preferred languages 6 | 7 | Allows tracking user's preferred languages with [_Events_](https://effector.dev/en/api/effector/event/) and [_Stores_](https://effector.dev/docs/api/effector/store). 8 | 9 | ::: info 10 | 11 | Uses [Navigator.languages](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/languages) and [Window: languagechange event](https://https://developer.mozilla.org/en-US/docs/Web/API/Window/languagechange_event) under the hood 12 | 13 | ::: 14 | 15 | ## Usage 16 | 17 | All you need to do is to create an integration by calling `trackPreferredLanguages` with an integration options: 18 | 19 | - `setup`: after this [_Event_](https://effector.dev/en/api/effector/event/) all listeners will be installed, and the integration will be ready to use; it is required because it is better to use [explicit initialization _Event_ in the application](/magazine/explicit_start). 20 | - `teardown?`: after this [_Event_](https://effector.dev/en/api/effector/event/) all listeners will be removed, and the integration will be ready to be destroyed. 21 | 22 | ```ts 23 | import { trackPreferredLanguages } from '@withease/web-api'; 24 | 25 | const { $language, $languages, languageChanged } = trackPreferredLanguages({ 26 | setup: appStarted, 27 | }); 28 | ``` 29 | 30 | Returns an object with: 31 | 32 | - `$language`: [_Store_](https://effector.dev/docs/api/effector/store) with user's preferred language 33 | - `$languages`: [_Store_](https://effector.dev/docs/api/effector/store) with array of user's preferred languages sorted by priority 34 | - `languageChanged`: [_Event_](https://effector.dev/en/api/effector/event/) that fires on preferred language change 35 | 36 | ::: tip 37 | It supports [`@@trigger` protocol](/protocols/trigger). Since it allows firing only one [_Event_](https://effector.dev/en/api/effector/event/) `trackPreferredLanguages` triggers `languageChanged` as a `fired` in case of [`@@trigger` protocol](/protocols/trigger). 38 | 39 | ```ts 40 | import { trackPreferredLanguages } from '@withease/web-api'; 41 | 42 | somethingExpectsTrigger(trackPreferredLanguages); 43 | ``` 44 | 45 | ::: 46 | 47 | ## Live demo 48 | 49 | Let us show you a live demo of how it works. The following demo displays a `$languages` value. _Change your system or browser language to see how it works._ 50 | 51 | 54 | 55 | 56 | 57 | ## Service-side rendering (SSR) 58 | 59 | It uses browser's APIs like `window.addEventListener` and `navigator.languages` under the hood, which are not available in server-side environment. However, it is possible to use it while SSR by passing header `Accept-Language` from the user's request to the special [_Store_](https://effector.dev/docs/api/effector/store) `trackPreferredLanguages.$acceptLanguageHeader` while `fork` in the server-side environment. Every server-side framework has its own way to do it, there are some examples: 60 | 61 | ::: details Fastify 62 | 63 | ```ts 64 | // server.ts 65 | import { trackPreferredLanguages } from '@withease/web-api'; 66 | 67 | fastify.get('*', { 68 | async handler(request, reply) { 69 | const scope = fork({ 70 | values: [ 71 | [ 72 | trackPreferredLanguages.$acceptLanguageHeader, 73 | request.headers['Accept-Language'], 74 | ], 75 | ], 76 | }); 77 | 78 | await allSettled(appStarted); 79 | 80 | // render HTML and return it 81 | 82 | return reply.send(html); 83 | }, 84 | }); 85 | ``` 86 | 87 | ::: 88 | 89 | ::: details Express 90 | 91 | ```ts 92 | // server.ts 93 | import { trackPreferredLanguages } from '@withease/web-api'; 94 | 95 | app.get('*', (req, res) => { 96 | const scope = fork({ 97 | values: [ 98 | [ 99 | trackPreferredLanguages.$acceptLanguageHeader, 100 | req.get('Accept-Language'), 101 | ], 102 | ], 103 | }); 104 | 105 | allSettled(appStarted) 106 | .then(() => { 107 | // render HTML and return it 108 | return html; 109 | }) 110 | .then((html) => { 111 | res.send(html); 112 | }); 113 | }); 114 | ``` 115 | 116 | ::: 117 | 118 | ::: details NestJS 119 | 120 | ```ts 121 | // server.ts 122 | import { trackPreferredLanguages } from '@withease/web-api'; 123 | 124 | @Controller() 125 | export class SSRController { 126 | @Get('*') 127 | async render(@Headers('Accept-Language') acceptLanguageHeader: string) { 128 | const scope = fork({ 129 | values: [ 130 | [trackPreferredLanguages.$acceptLanguageHeader, acceptLanguageHeader], 131 | ], 132 | }); 133 | 134 | await allSettled(appStarted); 135 | 136 | // render HTML and return it 137 | 138 | return html; 139 | } 140 | } 141 | ``` 142 | 143 | ::: 144 | -------------------------------------------------------------------------------- /apps/website/docs/factories/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: [2, 3] 3 | --- 4 | 5 | # factories 6 | 7 | In Effector's world any factory is a function that returns a set of [_Stores_](https://effector.dev/docs/api/effector/store), [_Events_](https://effector.dev/en/api/effector/event/) or [_Effects_](https://effector.dev/docs/api/effector/effect). It's a way to encapsulate some logic and reuse it in different places. 8 | 9 | If your application has any unit-tests or meant to be rendered on the server (SSR) factories have to be added to `factories` field in config of [`effector/babel-plugin`](https://effector.dev/docs/api/effector/babel-plugin/) or [`@effector/swc-plugin`](https://effector.dev/en/api/effector/swc-plugin/). The reasons of this limitation are described in [this article](https://effector.dev/en/explanation/sids/). 10 | 11 | In real world it is easy to add any third-party library that uses factories to the config because it has an exact import path. But adding factories from your own code is a bit more complicated. There are no automatic ways to validate that all factories are added to the config. This library is solving this problem: just add `@withease/factories` to the config and use it to create and invoke factories. 12 | 13 | Also, this library covers a few edge-cases that are really important for SSR. To learn more about them, read the [motivation](./motivation). 14 | 15 | ## Installation 16 | 17 | First, you need to install package: 18 | 19 | ::: code-group 20 | 21 | ```sh [pnpm] 22 | pnpm install @withease/factories 23 | ``` 24 | 25 | ```sh [yarn] 26 | yarn add @withease/factories 27 | ``` 28 | 29 | ```sh [npm] 30 | npm install @withease/factories 31 | ``` 32 | 33 | ::: 34 | 35 | Second, you need to setup [`effector/babel-plugin`](https://effector.dev/docs/api/effector/babel-plugin/) or [`@effector/swc-plugin`](https://effector.dev/en/api/effector/swc-plugin/). Please follow the instructions in the corresponding documentation. 36 | 37 | That's it! Now you can use `@withease/factories` to create and invoke factories across your application. 38 | 39 | ## API 40 | 41 | ### `createFactory` 42 | 43 | To create a factory you need to call `createFactory` with a factory creator callback: 44 | 45 | ```js 46 | import { createStore, createEvent, sample } from 'effector'; 47 | import { createFactory } from '@withease/factories'; 48 | 49 | const createCounter = createFactory(({ initialValue }) => { 50 | const $counter = createStore(initialValue); 51 | 52 | const increment = createEvent(); 53 | const decrement = createEvent(); 54 | 55 | sample({ 56 | clock: increment, 57 | source: $counter, 58 | fn: (counter) => counter + 1, 59 | target: $counter, 60 | }); 61 | 62 | sample({ 63 | clock: decrement, 64 | source: $counter, 65 | fn: (counter) => counter - 1, 66 | target: $counter, 67 | }); 68 | 69 | return { 70 | $counter, 71 | increment, 72 | decrement, 73 | }; 74 | }); 75 | ``` 76 | 77 | ### `invoke` 78 | 79 | Anywhere in your application you can invoke a factory by calling `invoke` with a factory and its arguments: 80 | 81 | ::: warning 82 | You have to invoke factories only in the top-level of your application. It means that you **must not** invoke it during component rendering or in any other place that can be called multiple times. Otherwise, you will get a memory leak. 83 | 84 | This limitation is applied to any factory, not only to factories created with `@withease/factories`. 85 | ::: 86 | 87 | ```ts 88 | import { invoke } from '@withease/factories'; 89 | 90 | const { $counter, increment, decrement } = invoke(createCounter, { 91 | initialValue: 2, 92 | }); 93 | ``` 94 | 95 | Now we can use `$counter`, `increment`, and `decrement` in our components. Here is how you might use them in different UI frameworks: 96 | 97 | ::: details Example usage in React 98 | 99 | ```jsx 100 | import { useUnit } from 'effector-react'; 101 | import { $counter, increment, decrement } from './model'; // assuming you've invoked your factory in `model.js`/`model.ts` 102 | 103 | const CounterComponent = () => { 104 | const counter = useUnit($counter); 105 | const [onIncrement, onDecrement] = useUnit(increment, decrement); 106 | 107 | return ( 108 |

109 |

Counter: {counter}

110 | 111 | 112 |
113 | ); 114 | }; 115 | ``` 116 | 117 | ::: 118 | 119 | ::: details Example usage in Vue 120 | 121 | ```html 122 | 129 | 130 | 136 | ``` 137 | 138 | ::: 139 | -------------------------------------------------------------------------------- /packages/factories/src/factories.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, vi } from 'vitest'; 2 | 3 | import { createFactory } from './create_factory'; 4 | import { invoke } from './invoke'; 5 | 6 | describe('factories', () => { 7 | test('invoke calls original creator with passed params', () => { 8 | const mock = vi.fn(); 9 | const factory = createFactory((params: number[]) => { 10 | mock(params); 11 | 12 | return params.at(0); 13 | }); 14 | 15 | const valueOne = invoke(factory, [1, 2, 3]); 16 | expect(mock).toHaveBeenCalledWith([1, 2, 3]); 17 | expect(valueOne).toBe(1); 18 | 19 | const valueTwo = invoke(factory, [2, 3, 4]); 20 | expect(mock).toHaveBeenCalledWith([2, 3, 4]); 21 | expect(valueTwo).toBe(2); 22 | }); 23 | 24 | test('throw error if try to call factory directly', () => { 25 | const myFactory = createFactory((params: number[]) => params.at(0)); 26 | 27 | expect(() => myFactory([1, 2, 3])).toThrowErrorMatchingInlineSnapshot( 28 | `[Error: Do not call factory directly, pass it to invoke function instead]` 29 | ); 30 | }); 31 | 32 | test('throw error if pass function with more than 1 argument', () => { 33 | expect(() => 34 | // @ts-expect-error It's obvious error, this test for JS-users 35 | createFactory((first: number, second: number) => { 36 | return 1; 37 | }) 38 | ).toThrowErrorMatchingInlineSnapshot( 39 | `[Error: createFactory does not support functions with more than 1 argument]` 40 | ); 41 | }); 42 | 43 | test('throw error if call non-factory function in invoke', () => { 44 | expect(() => 45 | invoke((params: number[]) => params.at(0), [1, 2, 3]) 46 | ).toThrowErrorMatchingInlineSnapshot( 47 | `[Error: Function passed to invoke is not created by createFactory]` 48 | ); 49 | }); 50 | }); 51 | 52 | describe('nested factories', () => { 53 | test('throw error on call un-invoked factory inside invoked factoy', () => { 54 | const internalFactory = createFactory(() => { 55 | return 1; 56 | }); 57 | 58 | const externalFactory = createFactory(() => { 59 | const internal = internalFactory(); 60 | return internal; 61 | }); 62 | 63 | expect(() => invoke(externalFactory)).toThrowErrorMatchingInlineSnapshot( 64 | `[Error: Do not call factory directly, pass it to invoke function instead]` 65 | ); 66 | }); 67 | 68 | test('do not throw error on call invoked factory inside invoked factoy', () => { 69 | const internalFactory = createFactory(() => { 70 | return 1; 71 | }); 72 | 73 | const externalFactory = createFactory(() => { 74 | const internal = invoke(internalFactory); 75 | return internal; 76 | }); 77 | 78 | expect(() => invoke(externalFactory)).not.toThrowError(); 79 | }); 80 | 81 | test('valid many nested factories', () => { 82 | const internalFactory = createFactory(() => { 83 | return 1; 84 | }); 85 | 86 | const externalFactory = createFactory(() => { 87 | const internal2 = invoke(internalFactory); 88 | const internal1 = invoke(internalFactory); 89 | return { internal1, internal2 }; 90 | }); 91 | 92 | expect(() => invoke(externalFactory)).not.toThrowError(); 93 | }); 94 | 95 | test('invalid many nested factories', () => { 96 | const internalFactory = createFactory(() => { 97 | return 1; 98 | }); 99 | 100 | const externalFactory = createFactory(() => { 101 | const internal2 = invoke(internalFactory); 102 | const internal1 = internalFactory(); 103 | return { internal1, internal2 }; 104 | }); 105 | 106 | expect(() => invoke(externalFactory)).toThrowErrorMatchingInlineSnapshot( 107 | `[Error: Do not call factory directly, pass it to invoke function instead]` 108 | ); 109 | }); 110 | 111 | test('valid very many nested factories', () => { 112 | const veryInternalFactory = createFactory(() => { 113 | return 1; 114 | }); 115 | 116 | const internalFactory = createFactory(() => { 117 | return invoke(veryInternalFactory); 118 | }); 119 | 120 | const externalFactory = createFactory(() => { 121 | const internal2 = invoke(internalFactory); 122 | const internal1 = invoke(internalFactory); 123 | return { internal1, internal2 }; 124 | }); 125 | 126 | expect(() => invoke(externalFactory)).not.toThrowError(); 127 | }); 128 | 129 | test('invalid very many nested factories', () => { 130 | const veryInternalFactory = createFactory(() => { 131 | return 1; 132 | }); 133 | 134 | const internalFactory = createFactory(() => { 135 | return veryInternalFactory(); 136 | }); 137 | 138 | const externalFactory = createFactory(() => { 139 | const internal2 = invoke(internalFactory); 140 | const internal1 = invoke(internalFactory); 141 | return { internal1, internal2 }; 142 | }); 143 | 144 | expect(() => invoke(externalFactory)).toThrowErrorMatchingInlineSnapshot( 145 | `[Error: Do not call factory directly, pass it to invoke function instead]` 146 | ); 147 | }); 148 | }); 149 | -------------------------------------------------------------------------------- /apps/website/docs/magazine/no_domains.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: You Don't Need Domains 3 | date: 2024-01-26 4 | --- 5 | 6 | # You Don't Need Domains 7 | 8 | [_Domain_](https://effector.dev/docs/api/effector/domain) in Effector is a namespace for [_Events_](https://effector.dev/en/api/effector/event/), [_Effects_](https://effector.dev/docs/api/effector/effect) and [_Stores_](https://effector.dev/docs/api/effector/store). It could be used for two purposes: 9 | 10 | 1. Semantic grouping of units 11 | 2. Bulk operations on units 12 | 13 | However, in most cases, you do not need [_Domains_](https://effector.dev/docs/api/effector/domain) at all. Let us see why. 14 | 15 | ## Semantic Grouping 16 | 17 | JavaScript does have semantic grouping of entities: it is **modules**. Since you do not have an option not to use modules, you will be using them to group your units anyway. So, why do you need another grouping mechanism? 18 | 19 | ::: code-group 20 | 21 | ```ts [module] 22 | // 👇 all units are already grouped by module 23 | // src/features/counter.ts 24 | 25 | import { createEvent, createStore, sample } from 'effector'; 26 | 27 | export const increment = createEvent(); 28 | export const decrement = createEvent(); 29 | 30 | export const $counter = createStore(0); 31 | 32 | sample({ 33 | source: $counter, 34 | clock: increment, 35 | fn: (counter) => counter + 1, 36 | target: $counter, 37 | }); 38 | 39 | sample({ 40 | source: $counter, 41 | clock: decrement, 42 | fn: (counter) => counter - 1, 43 | target: $counter, 44 | }); 45 | ``` 46 | 47 | ```ts [module and domain] 48 | // 👇 all units are already grouped by module 49 | // src/features/counter.ts 50 | 51 | import { createDomain, createEvent, createStore, sample } from 'effector'; 52 | 53 | // AND by domain, so it is redundant 54 | const counterDomain = createDomain(); 55 | 56 | export const increment = createEvent({ domain: counterDomain }); 57 | export const decrement = createEvent({ domain: counterDomain }); 58 | 59 | export const $counter = createStore(0, { domain: counterDomain }); 60 | 61 | sample({ 62 | source: $counter, 63 | clock: increment, 64 | fn: (counter) => counter + 1, 65 | target: $counter, 66 | }); 67 | 68 | sample({ 69 | source: $counter, 70 | clock: decrement, 71 | fn: (counter) => counter - 1, 72 | target: $counter, 73 | }); 74 | ``` 75 | 76 | ::: 77 | 78 | ## Bulk Operations 79 | 80 | But [_Domains_](https://effector.dev/docs/api/effector/domain) are not only about grouping. They also allow you to perform bulk operations on units. 81 | 82 | For example, you can reset values of all [_Stores_](https://effector.dev/docs/api/effector/store) in the [_Domain_](https://effector.dev/docs/api/effector/domain) with the following code: 83 | 84 | ```ts 85 | import { createDomain, createStore, createEvent } from 'effector'; 86 | 87 | const domain = createDomain(); 88 | 89 | export const someEvent = createEvent({ domain }); 90 | 91 | export const $store1 = createStore(0, { domain }); 92 | export const $store2 = createStore(0, { domain }); 93 | export const $store3 = createStore(0, { domain }); 94 | 95 | // 👇 callback will be called on every Store in the Domain 96 | domain.onCreateStore((store) => { 97 | store.reset(someEvent); 98 | }); 99 | ``` 100 | 101 | This approach has a significant drawback: **it is implicit**. In case of creating a new [_Store_](https://effector.dev/docs/api/effector/store) in the [_Domain_](https://effector.dev/docs/api/effector/domain), you will have to remember that trigger of `someEvent` will reset the new [_Store_](https://effector.dev/docs/api/effector/store) as well. It is really easy to forget about it. 102 | 103 | Things become even worse if you have more than one bulk operations in the [_Domain_](https://effector.dev/docs/api/effector/domain). 104 | 105 | Instead of using [_Domains_](https://effector.dev/docs/api/effector/domain), you can **explicitly** perform bulk operations on units. The previous example can be rewritten as follows: 106 | 107 | ```ts 108 | import { createDomain, createStore, createEvent } from 'effector'; 109 | 110 | const domain = createDomain(); // [!code --] 111 | 112 | export const someEvent = createEvent({ 113 | domain, // [!code --] 114 | }); 115 | 116 | export const $store1 = createStore(0, { 117 | domain, // [!code --] 118 | }); 119 | export const $store2 = createStore(0, { 120 | domain, // [!code --] 121 | }); 122 | export const $store3 = createStore(0, { 123 | domain, // [!code --] 124 | }); 125 | 126 | // 👇 callback will be called on every Store in the Domain 127 | domain.onCreateStore((store) => { 128 | store.reset(someEvent); // [!code --] 129 | }); 130 | 131 | // 👇 now it is explicit 132 | resetMany({ stores: [$store1, $store2, $store3], reset: someEvent }); // [!code ++] 133 | 134 | function resetMany({ stores, reset }) { 135 | for (const unit of stores) { 136 | unit.reset(reset); 137 | } 138 | } 139 | ``` 140 | 141 | This approach not only more explicit but also less verbose, because you do not need to specify [_Domain_](https://effector.dev/docs/api/effector/domain) for every unit. 142 | 143 | ## Summary 144 | 145 | - **Do not use [_Domains_](https://effector.dev/docs/api/effector/domain)** for semantic grouping - use modules instead 146 | - **Do not use [_Domains_](https://effector.dev/docs/api/effector/domain)** for bulk operations - use explicit functions instead 147 | -------------------------------------------------------------------------------- /apps/website/scripts/jsdoc.mjs: -------------------------------------------------------------------------------- 1 | import glob from 'glob'; 2 | import { readFile, writeFile } from 'node:fs/promises'; 3 | import { promisify } from 'node:util'; 4 | import { resolve } from 'node:path'; 5 | import * as babelParser from '@babel/parser'; 6 | import { parse as parseComment } from 'comment-parser'; 7 | import { asyncWalk } from 'estree-walker'; 8 | import prettier from 'prettier'; 9 | import { groupBy } from 'lodash-es'; 10 | 11 | const files = await promisify(glob)('../../packages/*/src/**/*.ts', { 12 | absolute: true, 13 | }); 14 | 15 | const apis = new Map(); 16 | 17 | await Promise.all( 18 | files.map(async (file) => { 19 | const packageName = file.match(/packages\/([^/]+)\//)[1]; 20 | 21 | if (!apis.has(packageName)) { 22 | apis.set(packageName, []); 23 | } 24 | 25 | const packageApis = apis.get(packageName); 26 | 27 | const content = await readFile(file, 'utf-8'); 28 | 29 | asyncWalk( 30 | babelParser.parse(content, { 31 | sourceType: 'module', 32 | plugins: ['typescript', 'jsx', 'estree', 'decorators-legacy'], 33 | }), 34 | { 35 | async enter(node) { 36 | if (node.type !== 'ExportNamedDeclaration') { 37 | return; 38 | } 39 | 40 | let kind = ''; 41 | let name = ''; 42 | switch (node.declaration?.type) { 43 | case 'TSTypeAliasDeclaration': 44 | name = node.declaration.id.name; 45 | kind = 'type'; 46 | break; 47 | case 'FunctionDeclaration': 48 | name = node.declaration.id.name; 49 | kind = 'function'; 50 | break; 51 | case 'TSDeclareFunction': 52 | name = node.declaration.id.name; 53 | kind = 'function'; 54 | break; 55 | case 'VariableDeclaration': 56 | name = node.declaration.declarations[0].id.name; 57 | kind = 'variable'; 58 | break; 59 | } 60 | 61 | const comments = 62 | node.leadingComments?.filter( 63 | (comment) => comment.type === 'CommentBlock' 64 | ) ?? []; 65 | 66 | if (!name || !kind || comments?.length === 0) { 67 | return; 68 | } 69 | 70 | const parsedDocs = comments 71 | .filter((comment) => comment.value.startsWith('*')) 72 | .flatMap((comment) => 73 | // comment-parser requires /* */ around the comment 74 | parseComment('/*' + comment.value + '*/') 75 | ); 76 | 77 | for (const doc of parsedDocs) { 78 | const privateTag = doc.tags.find((tag) => tag.tag === 'private'); 79 | 80 | if (privateTag) { 81 | continue; 82 | } 83 | 84 | const exampleTags = doc.tags.filter((tag) => tag.tag === 'example'); 85 | 86 | let examples = await Promise.all( 87 | exampleTags.map((tag) => 88 | prettier.format(tag.description, { parser: 'babel' }) 89 | ) 90 | ); 91 | 92 | const overloadTag = doc.tags.find((tag) => tag.tag === 'overload'); 93 | 94 | const sinceTag = doc.tags.find((tag) => tag.tag === 'since'); 95 | 96 | packageApis.push({ 97 | kind, 98 | name, 99 | description: doc.description, 100 | examples, 101 | alias: overloadTag?.name, 102 | since: sinceTag?.name, 103 | }); 104 | } 105 | }, 106 | } 107 | ); 108 | }) 109 | ); 110 | 111 | for (const [packageName, packageApis] of apis) { 112 | if (packageApis.length === 0) { 113 | continue; 114 | } 115 | 116 | const groupedApis = groupBy(packageApis, (api) => api.name); 117 | 118 | const filePath = resolve('docs', packageName, 'api.md'); 119 | 120 | const content = ['# APIs', 'Full list of available APIs.']; 121 | 122 | for (const [name, overloads] of Object.entries(groupedApis)) { 123 | const tsOnly = overloads.every((api) => api.kind === 'type'); 124 | const sinceAll = overloads.every((api) => api.since); 125 | 126 | content.push( 127 | `## \`${name}\` ${[ 128 | tsOnly && '', 129 | sinceAll && ``, 130 | ] 131 | .filter(Boolean) 132 | .join('')}` 133 | ); 134 | 135 | if (overloads.length === 1) { 136 | const [onlyOverload] = overloads; 137 | content.push(onlyOverload.description); 138 | content.push( 139 | ...onlyOverload.examples.map((example) => '```ts\n' + example + '\n```') 140 | ); 141 | } else { 142 | content.push('Is has multiple overloads 👇'); 143 | for (const overload of overloads) { 144 | content.push( 145 | `### \`${overload.alias ?? overload.name}\` ${[ 146 | !sinceAll && 147 | overload.since && 148 | ``, 149 | ].join(' ')}` 150 | ); 151 | content.push(overload.description); 152 | content.push( 153 | ...overload.examples.map((example) => '```ts\n' + example + '\n```') 154 | ); 155 | } 156 | } 157 | } 158 | 159 | await writeFile(filePath, content.join('\n\n')); 160 | } 161 | -------------------------------------------------------------------------------- /apps/website/docs/i18next/index.md: -------------------------------------------------------------------------------- 1 | # i18next 2 | 3 | A powerful internationalization framework for Effector which is based on [i18next](https://www.i18next.com/). 4 | 5 | ## Installation 6 | 7 | First, you need to install integration and its peer dependency: 8 | 9 | ::: code-group 10 | 11 | ```sh [pnpm] 12 | pnpm install @withease/i18next i18next 13 | ``` 14 | 15 | ```sh [yarn] 16 | yarn add @withease/i18next i18next 17 | ``` 18 | 19 | ```sh [npm] 20 | npm install @withease/i18next i18next 21 | ``` 22 | 23 | ::: 24 | 25 | ## Initialization 26 | 27 | All you need to do is to create an integration by calling `createI18nextIntegration` with an integration options: 28 | 29 | - `instance`: an instance of i18next in various forms. 30 | - `setup`: after this [_Event_](https://effector.dev/en/api/effector/event/) all listeners will be installed, and the integration will be ready to use; it is required because it is better to use [explicit initialization _Event_ in the application](/magazine/explicit_start). 31 | - `teardown?`: after this [_Event_](https://effector.dev/en/api/effector/event/) all listeners will be removed, and the integration will be ready to be destroyed. 32 | 33 | ### Use replaceable static `i18next` instance 34 | 35 | In the simplest case, you can pass an i18next instance to the integration. 36 | 37 | ```ts{9-11} 38 | import i18next from 'i18next'; 39 | import { createStore, createEvent, fork, allSettled } from 'effector'; 40 | import { createI18nextIntegration } from '@withease/i18next'; 41 | 42 | // Event that should be called after application initialization 43 | const appStarted = createEvent(); 44 | 45 | // Create Store for i18next instance 46 | const $i18nextInstance = createStore(i18next.createInstance(/* ... */), { 47 | serialize: 'ignore', 48 | }); 49 | 50 | const integration = createI18nextIntegration({ 51 | // Pass Store with i18next instance to the integration 52 | instance: $i18nextInstance, 53 | setup: appStarted, 54 | }); 55 | 56 | // You can replace $someInstance later during runtime 57 | // e.g., during fork on client or server 58 | ``` 59 | 60 | ### Use replaceable asynchronous `i18next` instance 61 | 62 | Sometimes you need to create an instance asynchronously. In this case, you can pass an [_Effect_](https://effector.dev/docs/api/effector/effect) that creates an instance. 63 | 64 | ```ts{9-11} 65 | import i18next from 'i18next'; 66 | import { createStore, createEvent, fork, allSettled } from 'effector'; 67 | import { createI18nextIntegration } from '@withease/i18next'; 68 | 69 | // Event that should be called after application initialization 70 | const appStarted = createEvent(); 71 | 72 | // Create Effect that creates i18next instance 73 | const createI18nextFx = createEffect(() => 74 | i18next.use(/* ... */).init(/* ... */) 75 | ); 76 | 77 | const integration = createI18nextIntegration({ 78 | // Pass Effect that creates i18next instance to the integration 79 | instance: createI18nextFx, 80 | setup: appStarted, 81 | }); 82 | 83 | // You can replace createI18nextFx later during runtime 84 | // e.g., during fork on client or server 85 | ``` 86 | 87 | ### Use static `i18next` instance 88 | 89 | Even though it is better to use a replaceable instance to [avoid global state](/magazine/global_variables) and make it possible to replace the instance during runtime, you can pass a static instance as well. 90 | 91 | ```ts{9} 92 | import i18next from 'i18next'; 93 | import { createStore, createEvent } from 'effector'; 94 | import { createI18nextIntegration } from '@withease/i18next'; 95 | 96 | // Event that should be called after application initialization 97 | const appStarted = createEvent(); 98 | 99 | const integration = createI18nextIntegration({ 100 | instance: i18next.createInstance(/* ... */), 101 | setup: appStarted, 102 | }); 103 | ``` 104 | 105 | ### Use static asynchronous `i18next` instance 106 | 107 | The same approach can be used with an asynchronous instance. 108 | 109 | ```ts{9} 110 | import i18next from 'i18next'; 111 | import { createStore, createEvent } from 'effector'; 112 | import { createI18nextIntegration } from '@withease/i18next'; 113 | 114 | // Event that should be called after application initialization 115 | const appStarted = createEvent(); 116 | 117 | const integration = createI18nextIntegration({ 118 | instance: () => i18next.use(/* ... */).init(/* ... */), 119 | setup: appStarted, 120 | }); 121 | ``` 122 | 123 | ## Usage 124 | 125 | Returned from `createI18nextIntegration` integration contains the following fields: 126 | 127 | - [`$t`](/i18next/t) is a [_Store_](https://effector.dev/docs/api/effector/store) containing a [translation function](https://www.i18next.com/overview/api#t) 128 | - [`translated`](/i18next/translated) which can be used as a shorthand for `$t` 129 | - [`$isReady`](/i18next/is_ready) is a [_Store_](https://effector.dev/docs/api/effector/store) containing a boolean value that indicates whether the integration is ready to use 130 | - [`reporting`](/i18next/reporting) is an object with the fields that allow you to track different events of the integration 131 | - [`$language`](/i18next/language) is a [_Store_](https://effector.dev/docs/api/effector/store) containing the current language 132 | - [`changeLanguageFx`](/i18next/change_language) is an [_Effect_](https://effector.dev/docs/api/effector/effect) that changes the current language 133 | - [`$instance`](/i18next/instance) is a [_Store_](https://effector.dev/docs/api/effector/store) containing the instance of i18next that is used by the integration 134 | -------------------------------------------------------------------------------- /apps/website/docs/magazine/explicit_start.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Explicit start of the app 3 | date: 2024-01-26 4 | --- 5 | 6 | # Explicit start of the app 7 | 8 | In Effector [_Events_](https://effector.dev/en/api/effector/event/) can not be triggered implicitly. It gives you more control over the app's lifecycle and helps to avoid unexpected behavior. 9 | 10 | ## The code 11 | 12 | In the simplest case, you can just create something like `appStarted` [_Event_](https://effector.dev/en/api/effector/event/) and trigger it right after the app initialization. Let us pass through the code line by line and explain what's going on here. 13 | 14 | 1. Create start [_Event_](https://effector.dev/en/api/effector/event/) 15 | 16 | This [_Event_](https://effector.dev/en/api/effector/event/) will be used to trigger the start of the app. For example, you can attach some global listeners after this it. 17 | 18 | ```ts{3} 19 | import { createEvent, fork, allSettled } from "effector"; 20 | 21 | const appStarted = createEvent(); 22 | 23 | const scope = fork(); 24 | 25 | await allSettled(appStarted, { scope }); 26 | ``` 27 | 28 | 2. Create isolated [_Scope_](https://effector.dev/docs/api/effector/scope) 29 | 30 | Fork API allows you to create isolated [_Scope_](https://effector.dev/docs/api/effector/scope) which will be used across the app. It helps you to [prevent using global state](/magazine/global_variables) and avoid unexpected behavior. 31 | 32 | ```ts{5} 33 | import { createEvent, fork, allSettled } from "effector"; 34 | 35 | const appStarted = createEvent(); 36 | 37 | const scope = fork(); 38 | 39 | await allSettled(appStarted, { scope }); 40 | ``` 41 | 42 | 3. Trigger start [_Event_](https://effector.dev/en/api/effector/event/) on the patricular [_Scope_](https://effector.dev/docs/api/effector/scope) 43 | 44 | [`allSettled`](https://effector.dev/docs/api/effector/allSettled) function allows you to start an [_Event_](https://effector.dev/en/api/effector/event/) on particular [_Scope_](https://effector.dev/docs/api/effector/scope) and wait until all computations will be finished. 45 | 46 | ```ts{7} 47 | import { createEvent, fork, allSettled } from "effector"; 48 | 49 | const appStarted = createEvent(); 50 | 51 | const scope = fork(); 52 | 53 | await allSettled(appStarted, { scope }); 54 | ``` 55 | 56 | ## The reasons 57 | 58 | The main reason for this approach is it allows you to control the app's lifecycle. It helps you to avoid unexpected behavior and make your app more predictable in some cases. Let us say we have a module with the following code: 59 | 60 | ```ts 61 | // app.ts 62 | import { createStore, createEvent, sample, scopeBind } from 'effector'; 63 | 64 | const $counter = createStore(0); 65 | const increment = createEvent(); 66 | 67 | const startIncrementationIntervalFx = createEffect(() => { 68 | const boundIncrement = scopeBind(increment, { safe: true }); 69 | 70 | setInterval(() => { 71 | boundIncrement(); 72 | }, 1000); 73 | }); 74 | 75 | sample({ 76 | clock: increment, 77 | source: $counter, 78 | fn: (counter) => counter + 1, 79 | target: $counter, 80 | }); 81 | 82 | startIncrementationIntervalFx(); 83 | ``` 84 | 85 | ### Tests 86 | 87 | We believe that any serious application has to be testable, so we have to isolate application lifecycle inside particular test-case. In case of implicit start (start of model logic by module execution), it will be impossible to test the app's behavior in different states. 88 | 89 | ::: tip 90 | [`scopeBind`](https://effector.dev/docs/api/effector/scopeBind) function allows you to bind an [_Event_](https://effector.dev/en/api/effector/event/) to particular [_Scope_](https://effector.dev/docs/api/effector/scope), more details you can find in the article [about Fork API rules](/magazine/fork_api_rules). 91 | ::: 92 | 93 | Now, to test the app's behavior, we have to mock `setInterval` function and check that `$counter` value is correct after particular time. 94 | 95 | ```ts 96 | // app.test.ts 97 | import { $counter } from './app'; 98 | 99 | test('$counter should be 5 after 5 seconds', async () => { 100 | // ... test 101 | }); 102 | 103 | test('$counter should be 10 after 10 seconds', async () => { 104 | // ... test 105 | }); 106 | ``` 107 | 108 | But, counter will be started immediately after the module execution, and we will not be able to test the app's behavior in different states. 109 | 110 | ### SSR 111 | 112 | In case of SSR, we have to start all application's logic on every user's request, and it will be impossible to do with implicit start. 113 | 114 | ```ts 115 | // server.ts 116 | import * as app from './app'; 117 | 118 | function handleRequest(req, res) { 119 | // ... 120 | } 121 | ``` 122 | 123 | But, counter will be started immediately after the module execution (aka application initialization), and we will not be able to start the app's logic on every user's request. 124 | 125 | ### Add explicit start 126 | 127 | Let us rewrite the code and add explicit start of the app. 128 | 129 | ```ts 130 | // app.ts 131 | import { createStore, createEvent, sample, scopeBind } from 'effector'; 132 | 133 | const $counter = createStore(0); 134 | const increment = createEvent(); 135 | 136 | const startIncrementationIntervalFx = createEffect(() => { 137 | const boundIncrement = scopeBind(increment, { safe: true }); 138 | 139 | setInterval(() => { 140 | boundIncrement(); 141 | }, 1000); 142 | }); 143 | 144 | sample({ 145 | clock: increment, 146 | source: $counter, 147 | fn: (counter) => counter + 1, 148 | target: $counter, 149 | }); 150 | 151 | startIncrementationIntervalFx(); // [!code --] 152 | const appStarted = createEvent(); // [!code ++] 153 | sample({ clock: appStarted, target: startIncrementationIntervalFx }); // [!code ++] 154 | ``` 155 | 156 | That is it! Now we can test the app's behavior in different states and start the app's logic on every user's request. 157 | 158 | :::tip 159 | In real-world applications, it is better to add not only explicit start of the app, but also explicit stop of the app. It will help you to avoid memory leaks and unexpected behavior. 160 | ::: 161 | 162 | ## One more thing 163 | 164 | In this recipe, we used application-wide `appStarted` [_Event_](https://effector.dev/en/api/effector/event/) to trigger the start of the app. However, in real-world applications, it is better to use more granular [_Events_](https://effector.dev/en/api/effector/event/) to trigger the start of the particular part of the app. 165 | 166 | ## Recap 167 | 168 | - Do not execute any logic just on module execution 169 | - Use explicit start [_Event_](https://effector.dev/en/api/effector/event/) of the application 170 | -------------------------------------------------------------------------------- /apps/website/docs/contracts/index.md: -------------------------------------------------------------------------------- 1 | 15 | 16 | # contracts 17 | 18 | Extremely small library (less than **{{maxSize}}** controlled by CI) for creating [_Contracts_](/protocols/contract) that allows you to introduce data validation on edges of the application with no performance compromises. 19 | 20 | ## Installation 21 | 22 | First, you need to install package: 23 | 24 | ::: code-group 25 | 26 | ```sh [pnpm] 27 | pnpm install @withease/contracts 28 | ``` 29 | 30 | ```sh [yarn] 31 | yarn add @withease/contracts 32 | ``` 33 | 34 | ```sh [npm] 35 | npm install @withease/contracts 36 | ``` 37 | 38 | ::: 39 | 40 | ## Creating a _Contract_ 41 | 42 | `@withease/contracts` exports bunch of utilities that can be used to create a _Contract_, read the full API reference [here](/contracts/api). Any of the utilities returns a _Contract_ object, that accepts something `unknown` and checks if it is something concrete defined by the used utility. 43 | 44 | 45 | 46 | ## Extracting types from a _Contract_ 47 | 48 | `@withease/contracts` provides a special type `UnContract` that can be used to extract a type from a _Contract_. 49 | 50 | ```ts 51 | import { type UnContract, obj, str, num } from '@withease/contracts'; 52 | 53 | const UserContract = obj({ 54 | id: num, 55 | name: str, 56 | email: str, 57 | }); 58 | 59 | // type User = { id: number, name: string, email: string } 60 | type User = UnContract; 61 | ``` 62 | 63 | ## Usage of a _Contract_ 64 | 65 | `@withease/contracts` is designed to be compatible with Effector's ecosystem without additional interop, so most of the time you can pass created [_Contract_](/protocols/contract) to other Effector's libraries as is. 66 | 67 | ### Farfetched 68 | 69 | [Farfetched](https://ff.effector.dev) is the advanced data fetching tool for web applications based of Effector. It suggests to ensure that data received from the server is conforms desired [_Contract_](/protocols/contract). 70 | 71 | ```ts 72 | import { createJsonQuery } from '@farfetched/core'; 73 | import { obj, str, arr, val, or } from '@withease/contracts'; 74 | 75 | const characterQuery = createJsonQuery({ 76 | params: declareParams<{ id: number }>(), 77 | request: { 78 | method: 'GET', 79 | url: ({ id }) => `https://rickandmortyapi.com/api/character/${id}`, 80 | }, 81 | response: { 82 | // after receiving data from the server 83 | // check if it is conforms the Contract to ensure 84 | // API does not return something unexpected 85 | contract: obj({ 86 | id: str, 87 | name: str, 88 | status: Status, 89 | species: str, 90 | type: str, 91 | gender: Gender, 92 | origin: obj({ name: str, url: str }), 93 | location: obj({ name: str, url: str }), 94 | image: or(val('Female'), val('Male'), val('Genderless')), 95 | episode: arr(str), 96 | }), 97 | }, 98 | }); 99 | ``` 100 | 101 | ### effector-storage 102 | 103 | [`effector-storage`](https://github.com/yumauri/effector-storage) is a small module for Effector to sync stores with different storages (local storage, session storage, async storage, IndexedDB, cookies, server side storage, etc). 104 | 105 | Since data is stored in an external storage it is important to validate it before using it in the application. 106 | 107 | ```ts 108 | import { createStore } from 'effector'; 109 | import { persist } from 'effector-storage'; 110 | import { num } from '@withease/contracts'; 111 | 112 | const $counter = createStore(0); 113 | 114 | persist({ 115 | store: $counter, 116 | key: 'counter', 117 | // after reading value from a storage check if a value is number 118 | // to avoid pushing invalid data to the Store 119 | contract: num, 120 | }); 121 | ``` 122 | 123 | ## Integration with other libraries 124 | 125 | Since `@withease/contracts` is compatible [_Contract_](/protocols/contract) protocol it can be used with any library that supports it. 126 | 127 | For instance, you can define a part of a [_Contract_](/protocols/contract) with [Zod](https://zod.dev/) and combine it with `@withease/contracts`: 128 | 129 | ```ts 130 | import { z } from 'zod'; 131 | import { arr, obj } from '@withease/contracts'; 132 | import { zodContract } from '@farfetched/zod'; 133 | 134 | const User = z.object({ 135 | name: z.string(), 136 | }); 137 | 138 | const MyContract = arr( 139 | obj({ 140 | // 👇 easily integrate Zod via compatibility layer 141 | users: zodContract(User), 142 | }) 143 | ); 144 | ``` 145 | 146 | The full list of libraries that support _Contract_ protocol can be found [here](/protocols/contract). 147 | 148 | ## Differences from other libraries 149 | 150 |
151 | It is extremely small and we mean it 👇 152 | 153 |
154 |
155 | 156 | 157 | 158 | ::: tip 159 | Data fetched directly from https://esm.run/ and updates on every commit. 160 | ::: 161 | 162 |
163 |
164 | It is significantly smaller than other libraries for creating _Contracts_. 165 |
166 | 167 | Of course smaller size is comes with some trade-offs, but we believe that in most cases it is worth it. `@withease/contracts` covers most of the common cases but does not try to be a silver bullet for all possible cases. It does not aim to have the following features from other libraries: 168 | 169 | - Branded types ([like in Runtypes](https://github.com/runtypes/runtypes?tab=readme-ov-file#branded-types)) 170 | - Advanced string-validators ([like IP-validation in Zod](https://zod.dev/?id=ip-addresses)) 171 | - Promise schemas ([like in Zod](https://zod.dev/?id=promise)) 172 | - Error i18n ([like in Valibot](https://valibot.dev/guides/internationalization/)) 173 | - ...and many other features that are not needed in _most_ of the cases 174 | 175 | ::: tip Q: What if I started a project with `@withease/contracts` and then realized that I need some of the features that are not covered by it? 176 | A: No worries! You can easily integrate `@withease/contracts` with other libraries that have the features you need. Check out the [Integration with other libraries](#integration-with-other-libraries) section for more details. 177 | ::: 178 | -------------------------------------------------------------------------------- /apps/website/docs/redux/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: [2, 3] 3 | --- 4 | 5 | # @withease/redux 6 | 7 | Minimalistic package to allow simpler migration from Redux to Effector. 8 | Also, can handle any other use case, where one needs to communicate with Redux Store from Effector's code. 9 | 10 | :::info 11 | This is an API reference article, for the Redux -> Effector migration guide [see the "Migrating from Redux to Effector" article](/magazine/migration_from_redux). 12 | ::: 13 | 14 | ## Installation 15 | 16 | First, you need to install package: 17 | 18 | ::: code-group 19 | 20 | ```sh [pnpm] 21 | pnpm install @withease/redux 22 | ``` 23 | 24 | ```sh [yarn] 25 | yarn add @withease/redux 26 | ``` 27 | 28 | ```sh [npm] 29 | npm install @withease/redux 30 | ``` 31 | 32 | ::: 33 | 34 | ## API 35 | 36 | ### `createReduxIntegration` 37 | 38 | Effector <-> Redux interoperability works through special "interop" object, which provides Effector-compatible API to Redux Store. 39 | 40 | ```ts 41 | const myReduxStore = configureStore({ 42 | // ... 43 | }); 44 | 45 | const reduxInterop = createReduxIntegration({ 46 | reduxStore: myReduxStore, 47 | setup: appStarted, 48 | }); 49 | ``` 50 | 51 | Explicit `setup` event is required to initialize the interoperability. Usually it would be an `appStarted` event or any other "app's lifecycle" event. 52 | 53 | You can read more about this practice [in the "Explicit start of the app" article](/magazine/explicit_start). 54 | 55 | #### Async setup 56 | 57 | You can also defer interop object initialization and Redux Store creation. 58 | 59 | The `createReduxIntegration` overload without explicit `reduxStore` allows you to pass the Store via `setup` event later. 60 | 61 | ```ts 62 | // src/shared/redux-interop 63 | export const startReduxInterop = createEvent(); 64 | export const reduxInterop = createReduxIntegration({ 65 | setup: startReduxInterop, 66 | }); 67 | 68 | // src/entrypoint.ts 69 | import { startReduxInterop } from 'shared/redux-interop'; 70 | 71 | const myReduxStore = configureStore({ 72 | // ... 73 | }); 74 | 75 | startReduxInterop(myReduxStore); 76 | // or, if you use the Fork API 77 | allSettled(startReduxInterop, { 78 | scope: clientScope, 79 | params: myReduxStore, 80 | }); 81 | ``` 82 | 83 | In that case the type support for `reduxInterop.$state` will be slightly worse and `reduxInterop.dispatch` will be no-op (and will show warnings in console) until interop object is provided with Redux Store. 84 | 85 | ☝️ This is useful, if your project has cyclic dependencies. 86 | 87 | ### Interoperability object 88 | 89 | Redux Interoperability object provides few useful APIs. 90 | 91 | #### `reduxInterop.$state` 92 | 93 | This is an Effector's Store, which contains **the state** of the provided instance of Redux Store. 94 | 95 | It is useful, as it allows to represent any part of Redux state as an Effector store. 96 | 97 | ```ts 98 | import { combine } from 'effector'; 99 | 100 | const $user = combine(reduxInterop.$state, (x) => x.user); 101 | ``` 102 | 103 | :::tip 104 | Notice, that `reduxInterop.$state` store will use Redux Store typings, if those are provided. So it is recommended to properly type your Redux Store. 105 | ::: 106 | 107 | #### `reduxInterop.dispatch` 108 | 109 | This is an Effector's Effect, which calls Redux Store's `dispatch` method under the hood. 110 | Since it is a normal [Effect](https://effector.dev/en/api/effector/effect) - it supports all methods of `Effect` type. 111 | 112 | :::tip 113 | It is recommended to create separate events for each specific action via `.prepend` method of `Effect`. 114 | ::: 115 | 116 | ```ts 117 | const updateUserName = reduxInterop.dispatch.prepend((name: string) => 118 | userSlice.changeName(name) 119 | ); 120 | 121 | sample({ 122 | clock: saveButtonClicked, 123 | source: $nextName, 124 | target: updateUserName, 125 | }); 126 | ``` 127 | 128 | It is also possible to convert a Redux Thunk to `Effect` by using Effector's [`attach` operator](https://effector.dev/en/api/effector/attach/). 129 | 130 | ```ts 131 | import { createAsyncThunk } from '@reduxjs/toolkit'; 132 | import { attach } from 'effector'; 133 | 134 | const someThunk = createAsyncThunk( 135 | 'some/thunk', 136 | async (p: number, { dispatch }) => { 137 | await new Promise((resolve) => setTimeout(resolve, p)); 138 | 139 | return dispatch(someSlice.actions.doSomething()); 140 | } 141 | ); 142 | 143 | /** 144 | * This is a redux-thunk, converted into an effector Effect. 145 | * 146 | * This allows gradual migration from redux-thunks to effector Effects 147 | */ 148 | const someThunkFx = attach({ 149 | mapParams: (p: number) => someThunk(p), 150 | effect: interop.dispatch, 151 | }); 152 | 153 | const promise = someThunkFx(42); 154 | // ☝️ `someThunk` will be dispatched under the hood 155 | // `someThunkFx` will return an Promise, which will be resolved once someThunk is resolved 156 | ``` 157 | 158 | #### `reduxInterop.$reduxStore` 159 | 160 | This is an Effector's Store, which contains provided instance of Redux Store. 161 | 162 | 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! 163 | 164 | So even if the logic is mixed between the two like this: 165 | 166 | ```ts 167 | // app code 168 | const myReduxStore = configureStore({ 169 | // ... 170 | }); 171 | 172 | const reduxInterop = createReduxIntegration({ 173 | reduxStore: myReduxStore, 174 | setup: appStarted, 175 | }); 176 | 177 | // user model 178 | const $user = combine(reduxInterop.$state, (x) => x.user); 179 | 180 | const updateUserName = reduxInterop.dispatch.prepend((name: string) => 181 | userSlice.changeName(name) 182 | ); 183 | 184 | sample({ 185 | clock: saveButtonClicked, 186 | source: $nextName, 187 | target: updateUserName, 188 | }); 189 | ``` 190 | 191 | It is still possible to write a proper test like this: 192 | 193 | ```ts 194 | test('username updated after save button click', async () => { 195 | const mockStore = configureStore({ 196 | // ... 197 | }); 198 | 199 | const scope = fork({ 200 | values: [ 201 | // Providing mock version of the redux store 202 | [reduxInterop.$reduxStore, mockStore], 203 | // Mocking anything else, if needed 204 | [$nextName, 'updated'], 205 | ], 206 | }); 207 | 208 | await allSettled(appStarted, { scope }); 209 | 210 | expect(scope.getState($userName)).toBe('initial'); 211 | 212 | await allSettled(saveButtonClicked, { scope }); 213 | 214 | expect(scope.getState($userName)).toBe('updated'); 215 | }); 216 | ``` 217 | 218 | ☝️ This test will be especially useful in the future, when this part of logic will be ported to Effector. 219 | 220 | :::tip 221 | 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. 222 | ::: 223 | -------------------------------------------------------------------------------- /packages/redux/src/integration.ts: -------------------------------------------------------------------------------- 1 | import type { Unit, StoreWritable, Store, Effect } from 'effector'; 2 | import type { Store as ReduxStore, Action } from 'redux'; 3 | import { 4 | createStore, 5 | createEvent, 6 | is, 7 | sample, 8 | attach, 9 | scopeBind, 10 | } from 'effector'; 11 | 12 | /** 13 | * Type for any thunk-like thing, which can be dispatched to Redux store 14 | * 15 | * Since generally Thunk is a any function, we can't type it properly 16 | */ 17 | type AnyThunkLikeThing = (...args: any[]) => any; 18 | 19 | /** 20 | * 21 | * Utility function to create an Effector API to interact with Redux store, 22 | * useful for cases like soft migration from Redux to Effector. 23 | * 24 | * If `reduxStore` is not provided in initial config, then it must be provided via `setup` event call. 25 | * If Redux Store is not provided, then it will be set to `null` initially and `reduxInterop.dispatch` will complain about it. 26 | * 27 | * @param config - interop config 28 | * @param config.reduxStore - (optional) initial redux store instance 29 | * @param config.setup - effector unit which will setup subscription to the store 30 | * @returns Interop API object 31 | */ 32 | export function createReduxIntegration< 33 | State = unknown, 34 | Act extends Action = { type: string; [k: string]: unknown }, 35 | Ext extends {} = {} 36 | >(config: { 37 | setup: Unit>; 38 | }): { 39 | /** 40 | * Effector store containing the Redux store 41 | * 42 | * You can use it to substitute Redux store instance, while writing tests via Effector's Fork API 43 | * @example 44 | * ``` 45 | * const scope = fork({ 46 | * values: [ 47 | * [reduxInterop.$reduxStore, reduxStoreMock] 48 | * ] 49 | * }) 50 | * ``` 51 | */ 52 | $reduxStore: StoreWritable>; 53 | /** 54 | * Effector's event, which will trigger Redux store dispatch 55 | * 56 | * @example 57 | * ``` 58 | * const updateName = reduxInterop.dispatch.prepend((name: string) => updateNameAction(name)); 59 | * ``` 60 | */ 61 | dispatch: Effect; 62 | /** 63 | * Effector store containing the state of the Redux store 64 | * 65 | * You can use it to subscribe to the Redux store state changes in Effector 66 | * @example 67 | * ``` 68 | * const $userName = combine(reduxInterop.$state, state => state.user.name) 69 | * ``` 70 | */ 71 | $state: Store; 72 | }; 73 | /** 74 | * 75 | * Utility function to create an Effector API to interact with Redux store, 76 | * useful for cases like soft migration from Redux to Effector. 77 | * 78 | * @param config - interop config 79 | * @param config.reduxStore - a redux store 80 | * @param config.setup - effector unit which will setup subscription to the store 81 | * @returns Interop API object 82 | */ 83 | export function createReduxIntegration< 84 | State = unknown, 85 | Act extends Action = { type: string; [k: string]: unknown }, 86 | Ext extends {} = {} 87 | >(config: { 88 | reduxStore: ReduxStore; 89 | setup: Unit; 90 | }): { 91 | /** 92 | * Effector store containing the Redux store 93 | * 94 | * You can use it to substitute Redux store instance, while writing tests via Effector's Fork API 95 | * @example 96 | * ``` 97 | * const scope = fork({ 98 | * values: [ 99 | * [reduxInterop.$reduxStore, reduxStoreMock] 100 | * ] 101 | * }) 102 | * ``` 103 | */ 104 | $reduxStore: StoreWritable>; 105 | /** 106 | * Effector's event, which will trigger Redux store dispatch 107 | * 108 | * @example 109 | * ``` 110 | * const updateName = reduxInterop.dispatch.prepend((name: string) => updateNameAction(name)); 111 | * ``` 112 | */ 113 | dispatch: Effect; 114 | /** 115 | * Effector store containing the state of the Redux store 116 | * 117 | * You can use it to subscribe to the Redux store state changes in Effector 118 | * @example 119 | * ``` 120 | * const $userName = combine(reduxInterop.$state, state => state.user.name) 121 | * ``` 122 | */ 123 | $state: Store; 124 | }; 125 | export function createReduxIntegration< 126 | State = unknown, 127 | Act extends Action = { type: string; [k: string]: unknown }, 128 | Ext extends {} = {} 129 | // Implementation type is `(any) => any`, so TS doesn't complain about overloads being "incompatible" 130 | // We do that, because they are incompatible, but that is for a reason - those are intended for different use-cases 131 | // e.g. explicit `reduxStore` overload can just infer the types right away + expect that the store and the state are always available 132 | >(config: any): any { 133 | const { reduxStore, setup } = config; 134 | if (!is.unit(setup)) { 135 | throw new Error('setup must be an effector unit'); 136 | } 137 | 138 | if (reduxStore) { 139 | /** 140 | * Only assert `reduxStore`, if it was provided explicitly 141 | */ 142 | assertReduxStore(reduxStore); 143 | } 144 | 145 | const $reduxStore = createStore(reduxStore ?? null, { 146 | serialize: 'ignore', 147 | name: 'redux/$reduxStore', 148 | }); 149 | 150 | if (!reduxStore) { 151 | /** 152 | * If no `reduxStore` was provided in the initial config, 153 | * then this is an async setup case 154 | * 155 | * So `reduxStore` will be provided by the `setup` event 156 | */ 157 | sample({ 158 | clock: setup, 159 | target: $reduxStore, 160 | }); 161 | } 162 | 163 | const stateUpdated = createEvent(); 164 | 165 | const $state = createStore(reduxStore?.getState() ?? null, { 166 | serialize: 'ignore', 167 | name: 'redux/$state', 168 | skipVoid: false, 169 | }).on(stateUpdated, (_, state) => state); 170 | 171 | const dispatchFx = attach({ 172 | source: $reduxStore, 173 | effect(store, action: Act | AnyThunkLikeThing) { 174 | assertReduxStore(store); 175 | 176 | return store.dispatch(action as Act) as unknown; 177 | }, 178 | }); 179 | 180 | const reduxInteropSetupFx = attach({ 181 | source: $reduxStore, 182 | effect(store) { 183 | assertReduxStore(store); 184 | 185 | const sendUpdate = scopeBind(stateUpdated, { safe: true }); 186 | 187 | sendUpdate(store.getState()); 188 | 189 | store.subscribe(() => { 190 | sendUpdate(store.getState()); 191 | }); 192 | }, 193 | }); 194 | 195 | sample({ 196 | clock: setup, 197 | target: reduxInteropSetupFx, 198 | }); 199 | 200 | /** 201 | * Logging any errors from the interop to the console for simplicity 202 | */ 203 | sample({ 204 | clock: [dispatchFx.failData, reduxInteropSetupFx.failData], 205 | }).watch(console.error); 206 | 207 | return { 208 | $reduxStore, 209 | dispatch: dispatchFx, 210 | $state, 211 | }; 212 | } 213 | 214 | function assertReduxStore(reduxStore: any): asserts reduxStore is ReduxStore { 215 | if ( 216 | !reduxStore || 217 | !reduxStore.dispatch || 218 | !reduxStore.getState || 219 | !reduxStore.subscribe 220 | ) { 221 | throw new Error('reduxStore must be provided and should be a Redux store'); 222 | } 223 | } 224 | --------------------------------------------------------------------------------