├── .husky ├── .gitignore ├── pre-commit └── commit-msg ├── .gitignore ├── packages ├── reca │ ├── .npmignore │ ├── .eslintrc.cjs │ ├── tests │ │ ├── fixtures │ │ │ ├── services │ │ │ │ ├── TestDIService.ts │ │ │ │ └── SpaceXService.ts │ │ │ ├── models │ │ │ │ └── SpaceXCompanyInfo.ts │ │ │ ├── components │ │ │ │ ├── TestDIComponent.tsx │ │ │ │ ├── TestStoreComponent.tsx │ │ │ │ ├── TestAutoStoreComponent.tsx │ │ │ │ ├── SpaceXComponent.tsx │ │ │ │ ├── TestDIWithPropsComponent.tsx │ │ │ │ ├── ToDoComponent.tsx │ │ │ │ └── PerformanceComponent.tsx │ │ │ └── stores │ │ │ │ ├── DIStore.ts │ │ │ │ ├── ToDoAutoStore.ts │ │ │ │ ├── DIWithPropsStore.ts │ │ │ │ ├── ToDoStore.ts │ │ │ │ ├── SpaceXStore.ts │ │ │ │ ├── LiveCycleStore.ts │ │ │ │ └── LiveCycleAutoStore.ts │ │ ├── performance.spec.tsx │ │ ├── di.spec.tsx │ │ ├── readme.spec.tsx │ │ └── livecycle.spec.tsx │ ├── README.md │ ├── ts-loader.js │ ├── src │ │ ├── decorators │ │ │ ├── NotRedraw.ts │ │ │ └── NotAuto.ts │ │ ├── index.ts │ │ ├── config.ts │ │ ├── hooks │ │ │ ├── UseStore.ts │ │ │ ├── UseServerStore.ts │ │ │ └── UseClientStore.ts │ │ └── stores │ │ │ ├── Store.ts │ │ │ └── AutoStore.ts │ ├── eslint.config.js │ ├── tsconfig.json │ ├── tsconfig.build.json │ ├── LICENSE │ ├── package.json │ └── CHANGELOG.md └── reca-docs │ ├── app │ ├── favicon.ico │ ├── layout.tsx │ ├── globals.css │ ├── page.module.css │ └── page.tsx │ ├── src │ └── pages │ │ └── index │ │ └── IndexPage.tsx │ ├── next.config.ts │ ├── .gitignore │ ├── eslint.config.js │ ├── public │ ├── vercel.svg │ └── next.svg │ ├── package.json │ ├── tsconfig.json │ └── README.md ├── .commitlintrc.json ├── eslint.config.js ├── tsconfig.json ├── .editorconfig ├── .github ├── workflows │ ├── test-pr.yml │ ├── codeql.yml │ ├── dependabot-auto-merge.yml │ └── npm-publish.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── PULL_REQUEST_TEMPLATE.md ├── package.json ├── LICENSE ├── SECURITY.md ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── CHANGELOG.md └── README.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged --quiet -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | .vscode -------------------------------------------------------------------------------- /packages/reca/.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !dist/**/* 3 | !package.json 4 | !README.md 5 | !LICENSE -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@commitlint/config-conventional" 4 | ] 5 | } -------------------------------------------------------------------------------- /packages/reca-docs/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabEG/reca/HEAD/packages/reca-docs/app/favicon.ico -------------------------------------------------------------------------------- /packages/reca-docs/src/pages/index/IndexPage.tsx: -------------------------------------------------------------------------------- 1 | 2 | export const IndexPage = () => ( 3 |
4 | 123 5 |
6 | ); 7 | -------------------------------------------------------------------------------- /packages/reca/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | extends: ["./../../.eslintrc.cjs"], 5 | ignorePatterns: ["node_modules/*", "dist/*"] 6 | }; 7 | -------------------------------------------------------------------------------- /packages/reca-docs/next.config.ts: -------------------------------------------------------------------------------- 1 | import type {NextConfig} from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | output: "export" 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /packages/reca/tests/fixtures/services/TestDIService.ts: -------------------------------------------------------------------------------- 1 | import {reflection} from "first-di"; 2 | 3 | @reflection 4 | export class TestDIService { 5 | 6 | public seed: number = Math.random(); 7 | 8 | } 9 | -------------------------------------------------------------------------------- /packages/reca/README.md: -------------------------------------------------------------------------------- 1 | # ReCA 2 | 3 | > **Note:** This file is replaced by the README.md from the repository root during npm publishing. 4 | 5 | For full documentation, please see the [main README](../../README.md). 6 | -------------------------------------------------------------------------------- /packages/reca/ts-loader.js: -------------------------------------------------------------------------------- 1 | import {register} from "node:module"; 2 | import {pathToFileURL} from "node:url"; 3 | import "reflect-metadata"; 4 | import "global-jsdom/register"; 5 | 6 | register("@swc-node/register/esm", pathToFileURL("./")); 7 | -------------------------------------------------------------------------------- /packages/reca/src/decorators/NotRedraw.ts: -------------------------------------------------------------------------------- 1 | export const notRedraw = (): MethodDecorator => ( 2 | target: object, 3 | propertyKey: string | symbol 4 | ): void => { 5 | Reflect.defineMetadata("reca:notRedraw", true, target, propertyKey); 6 | }; 7 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import labegStyle from "@labeg/code-style"; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default [ 5 | ...labegStyle, 6 | { 7 | rules: { 8 | // add rules here 9 | } 10 | } 11 | ]; 12 | -------------------------------------------------------------------------------- /packages/reca/src/decorators/NotAuto.ts: -------------------------------------------------------------------------------- 1 | export const notAuto = (): MethodDecorator | PropertyDecorator => ( 2 | target: object, 3 | propertyKey: string | symbol 4 | ): void => { 5 | Reflect.defineMetadata("reca:notAuto", true, target, propertyKey); 6 | }; 7 | -------------------------------------------------------------------------------- /packages/reca/eslint.config.js: -------------------------------------------------------------------------------- 1 | import rootStyles from "./../../eslint.config.js"; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default [ 5 | ...rootStyles, 6 | { 7 | rules: { 8 | // add rules here 9 | } 10 | } 11 | ]; 12 | -------------------------------------------------------------------------------- /packages/reca/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "experimentalDecorators": true, 6 | "emitDecoratorMetadata": true, 7 | "noEmit": true 8 | }, 9 | "include": [ 10 | "./src/**/*", 11 | "./tests/**/*" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/reca/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from "./hooks/UseStore.js"; 3 | 4 | export * from "./stores/Store.js"; 5 | export * from "./stores/AutoStore.js"; 6 | 7 | export * from "./decorators/NotAuto.js"; 8 | export * from "./decorators/NotRedraw.js"; 9 | 10 | export {reflection} from "first-di"; 11 | export * from "./config.js"; 12 | -------------------------------------------------------------------------------- /packages/reca/tests/fixtures/models/SpaceXCompanyInfo.ts: -------------------------------------------------------------------------------- 1 | 2 | export class SpaceXCompanyInfo { 3 | 4 | public name: string = ""; 5 | 6 | public founder: string = ""; 7 | 8 | public employees: number = 0; 9 | 10 | public applyData (json: object): this { 11 | Object.assign(this, json); 12 | return this; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /packages/reca/tests/fixtures/components/TestDIComponent.tsx: -------------------------------------------------------------------------------- 1 | 2 | import {useStore} from "../../../src/index.js"; 3 | import {DIStore} from "../stores/DIStore.js"; 4 | 5 | export const TesDIComponent = (): JSX.Element => { 6 | const store = useStore(DIStore); 7 | 8 | return ( 9 |
10 | {store.diService.seed} 11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "noEmit": true, 11 | "jsx": "react-jsx", 12 | "experimentalDecorators": true, 13 | "emitDecoratorMetadata": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/reca/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "forceConsistentCasingInFileNames": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "declaration": true, 10 | "sourceMap": true, 11 | "outDir": "dist" 12 | }, 13 | "include": [ 14 | "./src/**/*" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/reca/tests/fixtures/stores/DIStore.ts: -------------------------------------------------------------------------------- 1 | import {reflection} from "first-di"; 2 | import {Store} from "../../../src/index.js"; 3 | import {TestDIService} from "../services/TestDIService"; 4 | 5 | @reflection 6 | export class DIStore extends Store { 7 | 8 | // eslint-disable-next-line @typescript-eslint/parameter-properties 9 | public constructor (public diService: TestDIService) { 10 | super(); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /packages/reca/tests/fixtures/components/TestStoreComponent.tsx: -------------------------------------------------------------------------------- 1 | import {useStore} from "../../../src/index.js"; 2 | import type {ILiveCycleStoreProps} from "../stores/LiveCycleStore"; 3 | import {LiveCycleStore} from "../stores/LiveCycleStore"; 4 | 5 | export const TestStoreComponent = (props: ILiveCycleStoreProps): JSX.Element => { 6 | const store = useStore(LiveCycleStore, props); 7 | 8 | return ( 9 |
10 | {store.state} 11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | charset = utf-8 4 | 5 | # Code files 6 | [*.cs,*.csx,*.js,*.jsx,*.ts,*,tsx,*.css,*.scss] 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | trim_trailing_whitespace = true 11 | max_line_length = 140 12 | quote_type = double 13 | curly_bracket_next_line = true 14 | spaces_around_operators = true 15 | spaces_around_brackets = true 16 | indent_brace_style = Allman 17 | continuation_indent_size = 4 18 | 19 | [*.xml] 20 | indent_style = space 21 | -------------------------------------------------------------------------------- /packages/reca/tests/fixtures/components/TestAutoStoreComponent.tsx: -------------------------------------------------------------------------------- 1 | 2 | import {useStore} from "../../../src/index.js"; 3 | import type {ILiveCycleStoreProps} from "../stores/LiveCycleAutoStore.js"; 4 | import {LiveCycleAutoStore} from "../stores/LiveCycleAutoStore.js"; 5 | 6 | export const TestAutoStoreComponent = (props: ILiveCycleStoreProps): JSX.Element => { 7 | const store = useStore(LiveCycleAutoStore, props); 8 | 9 | return ( 10 |
11 | {store.state} 12 |
13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/reca-docs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /packages/reca-docs/eslint.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import {dirname} from "path"; 3 | import {fileURLToPath} from "url"; 4 | import {FlatCompat} from "@eslint/eslintrc"; 5 | import rootStyles from "./../../eslint.config.js"; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = dirname(__filename); 9 | 10 | const compat = new FlatCompat({ 11 | baseDirectory: __dirname 12 | }); 13 | 14 | const eslintConfig = [ 15 | ...compat.extends("next/core-web-vitals", "next/typescript"), 16 | ...rootStyles 17 | ]; 18 | 19 | export default eslintConfig; 20 | -------------------------------------------------------------------------------- /packages/reca-docs/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/reca/tests/fixtures/components/SpaceXComponent.tsx: -------------------------------------------------------------------------------- 1 | import {useStore} from "../../../src/index.js"; 2 | import {SpaceXStore} from "../stores/SpaceXStore.js"; 3 | 4 | export const SpaceXComponent = (): JSX.Element => { 5 | const store = useStore(SpaceXStore); 6 | 7 | return ( 8 |
9 |

10 | Company: 11 | {" "} 12 | 13 | {store.companyInfo.name} 14 |

15 | 16 |

17 | Founder: 18 | {" "} 19 | 20 | {store.companyInfo.founder} 21 |

22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/reca/tests/fixtures/stores/ToDoAutoStore.ts: -------------------------------------------------------------------------------- 1 | import {AutoStore} from "../../../src/index.js"; 2 | import type {FormEvent} from "react"; 3 | 4 | export class ToDoAutoStore extends AutoStore { 5 | 6 | public currentTodo: string = ""; 7 | 8 | public todos: string[] = []; 9 | 10 | public handleAddTodo (): void { 11 | this.todos.push(this.currentTodo); 12 | } 13 | 14 | public handleDeleteTodo (index: number): void { 15 | this.todos.splice(index, 1); 16 | } 17 | 18 | public handleCurrentEdit (event: FormEvent): void { 19 | this.currentTodo = event.currentTarget.value; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /packages/reca-docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reca-docs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "next dev --turbopack", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint --fix" 11 | }, 12 | "dependencies": { 13 | "react": "^19", 14 | "react-dom": "^19", 15 | "next": "^16.0.10", 16 | "reca": "^2.0.0" 17 | }, 18 | "devDependencies": { 19 | "typescript": "^5", 20 | "@types/node": "^25", 21 | "@types/react": "^19", 22 | "@types/react-dom": "^19", 23 | "rimraf": "^6.1.2", 24 | "eslint": "^9", 25 | "eslint-config-next": "16.0.10" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/reca/tests/fixtures/stores/DIWithPropsStore.ts: -------------------------------------------------------------------------------- 1 | import {reflection} from "first-di"; 2 | import {Store} from "../../../src/index.js"; 3 | import type {ITestDIWithPropsComponent} from "../components/TestDIWithPropsComponent"; 4 | import {TestDIService} from "../services/TestDIService"; 5 | 6 | @reflection 7 | export class DIWithPropsStore

extends Store

{ 8 | 9 | public test: number = 0; 10 | 11 | public constructor ( 12 | // eslint-disable-next-line @typescript-eslint/parameter-properties 13 | public diService: TestDIService, 14 | props: P 15 | ) { 16 | super(); 17 | 18 | this.test = props.test; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /packages/reca/tests/fixtures/stores/ToDoStore.ts: -------------------------------------------------------------------------------- 1 | import {Store} from "../../../src/index.js"; 2 | import type {FormEvent} from "react"; 3 | 4 | export class ToDoStore extends Store { 5 | 6 | public currentTodo: string = ""; 7 | 8 | public todos: string[] = []; 9 | 10 | public handleAddTodo (): void { 11 | this.todos.push(this.currentTodo); 12 | this.redraw(); 13 | } 14 | 15 | public handleDeleteTodo (index: number): void { 16 | this.todos.splice(index, 1); 17 | this.redraw(); 18 | } 19 | 20 | public handleCurrentEdit (event: FormEvent): void { 21 | this.currentTodo = event.currentTarget.value; 22 | this.redraw(); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /packages/reca-docs/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type {Metadata} from "next"; 2 | import {Inter} from "next/font/google"; 3 | import "./globals.css"; 4 | import {JSX} from "react"; 5 | 6 | // eslint-disable-next-line new-cap 7 | const inter = Inter({subsets: ["latin"]}); 8 | 9 | export const metadata: Metadata = { 10 | title: "Create Next App", 11 | description: "Generated by create next app" 12 | }; 13 | 14 | interface IRootLayout { 15 | readonly children: React.ReactNode; 16 | } 17 | 18 | const RootLayout = ({children}: IRootLayout): JSX.Element => ( 19 | 20 | 21 | {children} 22 | 23 | 24 | ); 25 | 26 | export default RootLayout; 27 | -------------------------------------------------------------------------------- /packages/reca/tests/performance.spec.tsx: -------------------------------------------------------------------------------- 1 | 2 | import {render} from "@testing-library/react"; 3 | import {PerformanceComponent} from "./fixtures/components/PerformanceComponent"; 4 | import {expect} from "chai"; 5 | import {describe, it} from "node:test"; 6 | 7 | describe("Store component must work fast", () => { 8 | it("count store performance", () => { 9 | const comp = render(); 10 | 11 | // Think about relative performance timer 12 | 13 | expect(Number(comp.container.querySelector(".store > .result-time")?.textContent)).to.be.lessThan(80); // 8 14 | 15 | expect(Number(comp.container.querySelector(".auto-store > .result-time")?.textContent)).to.be.lessThan(160); // 16 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/reca/tests/fixtures/components/TestDIWithPropsComponent.tsx: -------------------------------------------------------------------------------- 1 | 2 | import {useStore} from "../../../src/index.js"; 3 | import {DIWithPropsStore} from "../stores/DIWithPropsStore.js"; 4 | 5 | export interface ITestDIWithPropsComponent { 6 | test: number; 7 | } 8 | 9 | export const TestDIWithPropsComponent =

(props: P): JSX.Element => { 10 | const store = useStore(DIWithPropsStore, props); 11 | 12 | const {test} = props; 13 | 14 | return ( 15 |

16 | Seed: 17 | {" "} 18 | 19 | {store.diService.seed} 20 | 21 |
22 | 23 | Props: 24 | {" "} 25 | 26 | {test} 27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /packages/reca/tests/fixtures/services/SpaceXService.ts: -------------------------------------------------------------------------------- 1 | import {reflection} from "first-di"; 2 | import {SpaceXCompanyInfo} from "../models/SpaceXCompanyInfo"; 3 | 4 | @reflection 5 | export class SpaceXService { 6 | 7 | // eslint-disable-next-line @typescript-eslint/class-methods-use-this 8 | public async getCompanyInfo (): Promise { 9 | const response = await fetch("https://api.spacexdata.com/v3/info"); 10 | const json: unknown = await response.json(); 11 | 12 | // ... and manies manies lines of logics 13 | 14 | if (typeof json === "object" && json !== null) { 15 | return new SpaceXCompanyInfo().applyData(json); 16 | } 17 | throw new Error("SpaceXService.getCompanyInfo: response object is not json"); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /packages/reca-docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "plugins": [ 19 | { 20 | "name": "next" 21 | } 22 | ], 23 | "paths": { 24 | "@/*": ["./*"] 25 | } 26 | }, 27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 28 | "exclude": ["node_modules"] 29 | } 30 | -------------------------------------------------------------------------------- /packages/reca/src/config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file */ 2 | 3 | import {resolve} from "first-di"; 4 | import type {ClassConstructor} from "first-di/dist/typings/class-constructor"; 5 | 6 | export class DiConfig { 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | public resolver: (constructor: ClassConstructor, options?: any) => T = resolve; 10 | 11 | } 12 | 13 | export class Config { 14 | 15 | public di: DiConfig = new DiConfig(); 16 | 17 | // From https://gist.github.com/rhysburnie/498bfd98f24b7daf5fd5930c7f3c1b7b 18 | 19 | // eslint-disable-next-line @typescript-eslint/prefer-optional-chain, @typescript-eslint/no-unnecessary-condition 20 | public readonly isBrowser = !(typeof process !== "undefined" && process.versions && process.versions.node); 21 | 22 | } 23 | 24 | export const config = new Config(); 25 | -------------------------------------------------------------------------------- /packages/reca/tests/fixtures/stores/SpaceXStore.ts: -------------------------------------------------------------------------------- 1 | import {reflection} from "first-di"; 2 | import {AutoStore} from "../../../src/index.js"; 3 | import {SpaceXCompanyInfo} from "../models/SpaceXCompanyInfo.js"; 4 | import {SpaceXService} from "../services/SpaceXService.js"; 5 | 6 | @reflection 7 | export class SpaceXStore extends AutoStore { 8 | 9 | public companyInfo: SpaceXCompanyInfo = new SpaceXCompanyInfo(); 10 | 11 | public constructor (private readonly spaceXService: SpaceXService) { 12 | super(); 13 | } 14 | 15 | public activate (): void { 16 | this.fetchCompanyInfo(); 17 | } 18 | 19 | private async fetchCompanyInfo (): Promise { 20 | try { 21 | this.companyInfo = await this.spaceXService.getCompanyInfo(); 22 | } catch (error) { 23 | // Process exceptions, ex: this.logger.error(error.message); 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/test-pr.yml: -------------------------------------------------------------------------------- 1 | name: Test Pull Request 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | types: [ opened, synchronize, reopened ] 7 | 8 | permissions: 9 | contents: read 10 | pull-requests: read 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v6 19 | with: 20 | fetch-depth: 0 21 | ref: ${{ github.event.pull_request.head.sha }} 22 | 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v6 25 | with: 26 | node-version: 'latest' 27 | cache: 'npm' 28 | 29 | - name: Install dependencies 30 | run: npm ci 31 | 32 | - name: Run tests 33 | run: npm test 34 | 35 | - name: Build packages 36 | run: npm run build 37 | 38 | - name: Check for security vulnerabilities 39 | run: npm audit --production 40 | continue-on-error: true 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reca-workspace", 3 | "version": "2.0.18", 4 | "type": "module", 5 | "private": "true", 6 | "scripts": { 7 | "build": "npm run build -w packages/reca", 8 | "lint": "npm run lint -w packages/reca", 9 | "release": "npm run release -w packages/reca && standard-version", 10 | "test": "npm run lint -w packages/reca", 11 | "upgrade": "ncu -u -ws --root && rimraf -g -v **/node_modules **/package-lock.json && npm i", 12 | "prepare": "husky" 13 | }, 14 | "lint-staged": { 15 | "./src/**/*.(ts|tsx|js|jsx)": [ 16 | "eslint --fix" 17 | ] 18 | }, 19 | "workspaces": [ 20 | "packages/*" 21 | ], 22 | "devDependencies": { 23 | "@commitlint/cli": "^20.2.0", 24 | "@commitlint/config-conventional": "^20.2.0", 25 | "@labeg/code-style": "^6.10.13", 26 | "husky": "^9.1.7", 27 | "lint-staged": "^16.2.7", 28 | "rimraf": "^6.1.2", 29 | "standard-version": "^9.5.0", 30 | "npm-check-updates": "^19.2.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/reca/tests/fixtures/stores/LiveCycleStore.ts: -------------------------------------------------------------------------------- 1 | import {reflection, Store} from "../../../src/index.js"; 2 | 3 | export interface ILiveCycleStoreProps { 4 | onLivecycleChange: (val: string) => void; 5 | } 6 | 7 | @reflection 8 | export class LiveCycleStore

extends Store

{ 9 | 10 | public state: string = "init"; 11 | 12 | public constructor (props: P) { 13 | super(); 14 | this.state += " constructor"; 15 | props.onLivecycleChange(this.state); 16 | } 17 | 18 | public activate (props: P): void { 19 | this.state += " activate"; 20 | props.onLivecycleChange(this.state); 21 | } 22 | 23 | public update (props: P): void { 24 | this.state += " update"; 25 | props.onLivecycleChange(this.state); 26 | } 27 | 28 | public afterUpdate (props: P): void { 29 | this.state += " afterUpdate"; 30 | props.onLivecycleChange(this.state); 31 | } 32 | 33 | public dispose (props: P): void { 34 | this.state += " dispose"; 35 | props.onLivecycleChange(this.state); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL Advanced" 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | - cron: '0 0 * * 1' # Каждый понедельник в 00:00 10 | 11 | permissions: 12 | actions: read 13 | contents: read 14 | security-events: write 15 | 16 | jobs: 17 | analyze: 18 | name: Analyze Code 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 360 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | language: [ 'javascript-typescript' ] 26 | 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v6 30 | 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v4 33 | with: 34 | languages: ${{ matrix.language }} 35 | queries: security-extended,security-and-quality 36 | 37 | - name: Autobuild 38 | uses: github/codeql-action/autobuild@v4 39 | 40 | - name: Perform CodeQL Analysis 41 | uses: github/codeql-action/analyze@v4 42 | with: 43 | category: "/language:${{matrix.language}}" 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Eugene Labutin 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 | -------------------------------------------------------------------------------- /packages/reca/tests/fixtures/stores/LiveCycleAutoStore.ts: -------------------------------------------------------------------------------- 1 | import {AutoStore, reflection} from "../../../src/index.js"; 2 | 3 | export interface ILiveCycleStoreProps { 4 | onLivecycleChange: (val: string) => void; 5 | } 6 | 7 | @reflection 8 | export class LiveCycleAutoStore

extends AutoStore

{ 9 | 10 | public state: string = "init"; 11 | 12 | public constructor (props: P) { 13 | super(); 14 | this.state += " constructor"; 15 | props.onLivecycleChange(this.state); 16 | } 17 | 18 | public activate (props: P): void { 19 | this.state += " activate"; 20 | props.onLivecycleChange(this.state); 21 | } 22 | 23 | public update (props: P): void { 24 | this.state += " update"; 25 | props.onLivecycleChange(this.state); 26 | } 27 | 28 | public afterUpdate (props: P): void { 29 | this.state += " afterUpdate"; 30 | props.onLivecycleChange(this.state); 31 | } 32 | 33 | public dispose (props: P): void { 34 | this.state += " dispose"; 35 | props.onLivecycleChange(this.state); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /packages/reca/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Eugene Labutin 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 | -------------------------------------------------------------------------------- /packages/reca/src/hooks/UseStore.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/rules-of-hooks */ 2 | import type {Store} from "../stores/Store.js"; 3 | import {useClientStore} from "./UseClientStore.js"; 4 | import {useServerStore} from "./UseServerStore.js"; 5 | 6 | /** 7 | * Todo: add DI here 8 | * 9 | * @param store 10 | * @param props 11 | * @returns 12 | */ 13 | export const useStore =

>( 14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 15 | store: new (...params: any[]) => T, 16 | props?: P 17 | ): T => { 18 | if (typeof window !== "undefined") { 19 | return useClientStore(store, props); 20 | } 21 | return useServerStore(store, props); 22 | }; 23 | 24 | /** 25 | * Todo: add DI here 26 | * 27 | * @param store 28 | * @param props 29 | * @returns 30 | */ 31 | export const useStoreAsync = async

>( 32 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 33 | store: new (...params: any[]) => T, 34 | props?: P 35 | ): Promise => { 36 | await Promise.resolve(); 37 | 38 | if (typeof window !== "undefined") { 39 | return useClientStore(store, props); 40 | } 41 | return useServerStore(store, props); 42 | }; 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: '[BUG] ' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Description 11 | 12 | A clear and concise description of the bug. 13 | 14 | ## Steps to Reproduce 15 | 16 | 1. Install `reca` version X.X.X 17 | 2. Create a component with '...' 18 | 3. Run the application 19 | 4. See error 20 | 21 | ## Expected Behavior 22 | 23 | What you expected to happen. 24 | 25 | ## Actual Behavior 26 | 27 | What actually happened. 28 | 29 | ## Code Sample 30 | 31 | ```typescript 32 | // Minimal code example that demonstrates the issue 33 | ``` 34 | 35 | ## Component/Store Code 36 | 37 | ```typescript 38 | // Your component or store code 39 | ``` 40 | 41 | ## Environment 42 | 43 | - **reca version**: [e.g., 2.0.4] 44 | - **React version**: [e.g., 18.3.1] 45 | - **Node.js version**: [e.g., 20.10.0] 46 | - **npm/pnpm/yarn version**: [e.g., npm 10.2.3] 47 | - **Operating System**: [e.g., Windows 11, macOS 14, Ubuntu 22.04] 48 | - **TypeScript version**: [e.g., 5.3.3] 49 | - **Build tool**: [e.g., Vite, Next.js, CRA] 50 | 51 | ## Additional Context 52 | 53 | Add any other context, screenshots, or error messages here. 54 | 55 | ## Possible Solution 56 | 57 | If you have ideas on how to fix this, please share them. 58 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot Auto-merge 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | enable-auto-merge: 13 | runs-on: ubuntu-latest 14 | # Срабатывает ТОЛЬКО для Dependabot PR 15 | if: github.actor == 'dependabot[bot]' 16 | 17 | steps: 18 | - name: Dependabot metadata 19 | id: metadata 20 | uses: dependabot/fetch-metadata@v2 21 | with: 22 | github-token: "${{ secrets.GITHUB_TOKEN }}" 23 | 24 | - name: Wait for test completion 25 | uses: lewagon/wait-on-check-action@v1.4.1 26 | with: 27 | ref: ${{ github.event.pull_request.head.sha }} 28 | check-name: 'test' 29 | repo-token: ${{ secrets.GITHUB_TOKEN }} 30 | wait-interval: 10 31 | 32 | - name: Approve PR 33 | run: gh pr review --approve "$PR_URL" 34 | env: 35 | PR_URL: ${{ github.event.pull_request.html_url }} 36 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | - name: Merge PR 39 | run: gh pr merge --squash "$PR_URL" 40 | env: 41 | PR_URL: ${{ github.event.pull_request.html_url }} 42 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | -------------------------------------------------------------------------------- /packages/reca/tests/di.spec.tsx: -------------------------------------------------------------------------------- 1 | import {TesDIComponent} from "./fixtures/components/TestDIComponent.js"; 2 | import {TestDIWithPropsComponent} from "./fixtures/components/TestDIWithPropsComponent.js"; 3 | import {render} from "@testing-library/react"; 4 | import {expect} from "chai"; 5 | import {describe, it} from "node:test"; 6 | 7 | describe("Dependency injection must work", () => { 8 | it("di must resolve dependencies", () => { 9 | const comp1 = render(); 10 | const comp2 = render(); 11 | expect(comp1.container.innerHTML).to.equal(comp2.container.innerHTML); 12 | 13 | comp1.rerender(); 14 | expect(comp1.container.innerHTML).to.equal(comp2.container.innerHTML); 15 | 16 | comp1.unmount(); 17 | comp2.unmount(); 18 | }); 19 | 20 | it("di must resolve props", () => { 21 | const comp1 = render(); 22 | const comp2 = render(); 23 | expect(comp1.container.innerHTML).to.equal(comp2.container.innerHTML); 24 | 25 | comp1.rerender(); 26 | expect(comp1.container.innerHTML).to.equal(comp2.container.innerHTML); 27 | 28 | comp1.unmount(); 29 | comp2.unmount(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/reca-docs/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/reca/src/hooks/UseServerStore.ts: -------------------------------------------------------------------------------- 1 | import type {ClassConstructor} from "first-di/dist/typings/class-constructor"; 2 | import {config} from "../config.js"; 3 | import type {Store} from "../stores/Store.js"; 4 | 5 | /** 6 | * Todo: add DI here 7 | * 8 | * @param store 9 | * @param props 10 | * @returns 11 | */ 12 | export const useServerStore =

>( 13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 | store: new (...params: any[]) => T, 15 | props?: P 16 | ): T => { 17 | // Resolve dependencies 18 | const constructorParams: unknown[] = Reflect 19 | .getMetadata("design:paramtypes", store) as ([] | null) ?? []; 20 | 21 | const resolvedParams = constructorParams.map((param: unknown) => { 22 | if (typeof param === "function" && "prototype" in param) { // Check is class 23 | if (param.prototype === Object.prototype) { // Typescript interface in props (props: P) 24 | return props; 25 | } 26 | 27 | // True class 28 | return config.di.resolver(param as ClassConstructor); 29 | } 30 | 31 | // Else props object 32 | return props; 33 | }); 34 | 35 | // eslint-disable-next-line new-cap 36 | const resolvedStore = new store(...resolvedParams); 37 | 38 | return resolvedStore; 39 | }; 40 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # NPM dependencies 4 | - package-ecosystem: "npm" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | day: "wednesday" 9 | time: "09:00" 10 | open-pull-requests-limit: 1 11 | groups: 12 | all-minor-patch: 13 | patterns: 14 | - "*" 15 | update-types: 16 | - "minor" 17 | - "patch" 18 | eslint-plugins: 19 | patterns: 20 | - "eslint*" 21 | - "@typescript-eslint/*" 22 | - "@stylistic/*" 23 | react-ecosystem: 24 | patterns: 25 | - "react*" 26 | - "@types/react" 27 | development-dependencies: 28 | dependency-type: "development" 29 | exclude-patterns: 30 | - "eslint*" 31 | - "react*" 32 | commit-message: 33 | prefix: "chore" 34 | prefix-development: "chore" 35 | include: "scope" 36 | labels: 37 | - "dependencies" 38 | assignees: 39 | - "LabEG" 40 | 41 | # GitHub Actions 42 | - package-ecosystem: "github-actions" 43 | directory: "/" 44 | schedule: 45 | interval: "weekly" 46 | day: "wednesday" 47 | time: "09:00" 48 | commit-message: 49 | prefix: "ci" 50 | include: "scope" 51 | labels: 52 | - "dependencies" 53 | assignees: 54 | - "LabEG" 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: '[FEATURE] ' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Feature Description 11 | 12 | A clear and concise description of the feature you'd like to see. 13 | 14 | ## Problem Statement 15 | 16 | What problem would this feature solve? Is your feature request related to a problem? 17 | 18 | **Example:** "I'm frustrated when [...]" 19 | 20 | ## Proposed Solution 21 | 22 | Describe the solution you'd like to see implemented. 23 | 24 | ## Alternatives Considered 25 | 26 | Describe any alternative solutions or features you've considered. 27 | 28 | ## Use Cases 29 | 30 | Provide specific examples of how this feature would be used: 31 | 32 | 1. Use case 1... 33 | 2. Use case 2... 34 | 35 | ## Code Examples 36 | 37 | If applicable, provide example code showing how you envision using this feature: 38 | 39 | ```typescript 40 | // Example of proposed usage 41 | ``` 42 | 43 | ## Impact 44 | 45 | - **Who benefits**: [developers, teams, specific use cases] 46 | - **Breaking changes**: [yes/no - explain if yes] 47 | - **Backward compatibility**: [maintained/affected] 48 | 49 | ## Additional Context 50 | 51 | Add any other context, screenshots, or examples about the feature request here. 52 | 53 | ## Willingness to Contribute 54 | 55 | - [ ] I'm willing to submit a PR to implement this feature 56 | - [ ] I can help with testing 57 | - [ ] I can help with documentation 58 | -------------------------------------------------------------------------------- /packages/reca-docs/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: NPM Publish 2 | 3 | # This workflow uses actions that are not certified by GitHub. 4 | # They are provided by a third-party and are governed by 5 | # separate terms of service, privacy policy, and support 6 | # documentation. 7 | 8 | on: 9 | push: 10 | branches: [ main ] 11 | schedule: 12 | - cron: '0 12 * * 3' # Every Wednesday at 12:00 UTC (3 hours after Dependabot runs at 09:00) 13 | workflow_dispatch: 14 | 15 | permissions: 16 | contents: write 17 | id-token: write # Required for npm provenance 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v6 26 | with: 27 | fetch-depth: 0 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v6 31 | with: 32 | node-version: 'latest' 33 | registry-url: https://registry.npmjs.org/ 34 | cache: 'npm' 35 | 36 | - name: Configure Git 37 | run: | 38 | git config --global user.email "labeg@mail.ru" 39 | git config --global user.name "Eugene Labutin" 40 | 41 | - name: Install dependencies 42 | run: npm ci 43 | 44 | - name: Run tests 45 | run: npm test 46 | 47 | - name: Build packages 48 | run: npm run build 49 | 50 | - name: Create release 51 | run: npm run release 52 | 53 | - name: Push changes and tags 54 | run: git push && git push --tags 55 | 56 | - name: Copy README to package 57 | run: cp README.md packages/reca/README.md 58 | 59 | - name: Publish to NPM 60 | run: npm publish --provenance --access public -w packages/reca 61 | -------------------------------------------------------------------------------- /packages/reca/tests/fixtures/components/ToDoComponent.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-array-index-key */ 2 | /* eslint-disable react/jsx-no-bind */ 3 | /* eslint-disable @typescript-eslint/unbound-method */ 4 | /* eslint-disable react/jsx-key */ 5 | 6 | import {useStore} from "../../../src/index.js"; 7 | import {ToDoAutoStore} from "../stores/ToDoAutoStore.js"; 8 | 9 | export const ToDoComponent = (): JSX.Element => { 10 | const store = useStore(ToDoAutoStore); 11 | 12 | return ( 13 |
14 |
15 | { 16 | store.todos.map((todo, index) => ( 17 |
21 | {todo} 22 | 23 | 30 |
31 | )) 32 | } 33 |
34 | 35 |
36 | 40 | 41 | 47 |
48 |
49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /packages/reca/tests/fixtures/components/PerformanceComponent.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/rules-of-hooks */ 2 | /* eslint-disable max-statements */ 3 | /* eslint-disable max-lines-per-function */ 4 | /* eslint-disable react/jsx-key */ 5 | 6 | import {useStore} from "../../../src/index.js"; 7 | import {ToDoAutoStore} from "../stores/ToDoAutoStore.js"; 8 | import {ToDoStore} from "../stores/ToDoStore.js"; 9 | 10 | export const PerformanceComponent = (): JSX.Element => { 11 | const array = new Array(1000).fill(true); 12 | 13 | // Calculate store 14 | const beginTimeSt = performance.now(); 15 | const storesSt = array.map(() => useStore(ToDoStore)); 16 | const resultSt = performance.now() - beginTimeSt; 17 | 18 | // Calculate auto-store 19 | const beginTimeAu = performance.now(); 20 | const storesAu = array.map(() => useStore(ToDoAutoStore)); 21 | const resultAu = performance.now() - beginTimeAu; 22 | 23 | const textSt = storesSt.reduce((previousValue, currentValue) => previousValue + currentValue.currentTodo, ""); 24 | const textAu = storesAu.reduce((previousValue, currentValue) => previousValue + currentValue.currentTodo, ""); 25 | 26 | return ( 27 |
28 |
29 | Result Time: 30 | {" "} 31 | 32 | 33 | {resultSt} 34 | 35 |
36 | 37 |
38 | Result Time: 39 | {" "} 40 | 41 | 42 | {resultAu} 43 | 44 |
45 | 46 |
47 | {textSt} 48 | 49 | {textAu} 50 |
51 |
52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /packages/reca/tests/readme.spec.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/non-nullable-type-assertion-style */ 2 | 3 | import {render, fireEvent, waitFor, screen} from "@testing-library/react"; 4 | import {SpaceXComponent} from "./fixtures/components/SpaceXComponent"; 5 | import {ToDoComponent} from "./fixtures/components/ToDoComponent"; 6 | import {expect} from "chai"; 7 | import {describe, it} from "node:test"; 8 | 9 | describe("Readme samples must work", () => { 10 | it("todo sample must add end delete todo items", () => { 11 | const comp = render(); 12 | 13 | // Todos list empty 14 | expect(comp.container.querySelector(".todos-list")?.outerHTML).to.equal("
"); 15 | 16 | // Inputs created 17 | expect(comp.container.querySelector(".todos-input")?.innerHTML).to.equal(""); 18 | 19 | // Input todo 20 | fireEvent.input( 21 | comp.container.querySelector(".todos-input > input") as HTMLInputElement, 22 | {target: {value: "First ToDo"}} 23 | ); 24 | 25 | fireEvent.click(comp.container.querySelector(".todos-input > button") as HTMLButtonElement); 26 | 27 | // Check first todo 28 | expect(comp.container.querySelector(".todo")?.textContent).to.equal("First ToDoX"); 29 | 30 | // Remove todo 31 | fireEvent.click(comp.container.querySelector(".todo-delete") as HTMLButtonElement); 32 | 33 | // Check list empty 34 | expect(comp.container.querySelector(".todos-list")?.outerHTML).to.equal("
"); 35 | }); 36 | 37 | it("todo sample must resolve di and show info from service", async () => { 38 | const comp = render(); 39 | 40 | await waitFor(() => screen.findByText("Company: SpaceX"), {timeout: 5000}); 41 | 42 | expect(comp.container.innerHTML).to.equal("

Company: SpaceX

Founder: Elon Musk

"); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /packages/reca/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reca", 3 | "version": "2.4.3", 4 | "description": "ReCA - React Clean Architecture state manager", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "build": "npm run clean && tsc --project tsconfig.build.json && node ./dist/index.js", 9 | "clean": "rimraf ./dist", 10 | "lint": "eslint --fix ./src/", 11 | "test": "node --import ./ts-loader.js --test --test-reporter=spec --test-reporter-destination=stdout \"tests/**/*.spec.tsx\"", 12 | "test-watch": "node --watch --import ./ts-loader.js --test --test-reporter=spec --test-reporter-destination=stdout \"tests/**/*.spec.tsx\"", 13 | "coverage": "node --import ./ts-loader.js --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=coverage/lcov.info \"tests/**/*.spec.tsx\"", 14 | "release": "standard-version", 15 | "prepublishOnly": "npm run test && npm run build" 16 | }, 17 | "keywords": [ 18 | "react", 19 | "state manager" 20 | ], 21 | "author": "LabEG", 22 | "license": "MIT", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/LabEG/reca.git" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/LabEG/reca/issues" 29 | }, 30 | "homepage": "https://github.com/LabEG/reca#readme", 31 | "files": [ 32 | "dist", 33 | "README.md", 34 | "LICENSE" 35 | ], 36 | "dependencies": { 37 | "first-di": "^3.4.7" 38 | }, 39 | "peerDependencies": { 40 | "react": ">=16.0.0", 41 | "reflect-metadata": ">=0.1.0" 42 | }, 43 | "devDependencies": { 44 | "@testing-library/react": "^16.3.0", 45 | "@swc-node/register": "^1.11.1", 46 | "@types/chai": "^5.2.3", 47 | "@types/react": "^19.2.7", 48 | "chai": "^6.2.1", 49 | "global-jsdom": "^27.0.0", 50 | "jsdom": "^27.3.0", 51 | "react": "^19.2.3", 52 | "react-dom": "^19.2.3", 53 | "reflect-metadata": "^0.2.2", 54 | "rimraf": "^6.1.2", 55 | "standard-version": "^9.5.0", 56 | "ts-node": "^10.9.2", 57 | "typescript": "^5.9.3" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | 5 | ## Related Issue 6 | 7 | 8 | Closes # 9 | 10 | ## Type of Change 11 | 12 | 13 | 14 | - [ ] Bug fix (non-breaking change which fixes an issue) 15 | - [ ] New feature (non-breaking change which adds functionality) 16 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 17 | - [ ] Documentation update 18 | - [ ] Code refactoring 19 | - [ ] Dependency update 20 | 21 | ## Changes Made 22 | 23 | 24 | 25 | - 26 | - 27 | - 28 | 29 | ## Testing 30 | 31 | 32 | 33 | - [ ] I have run `npm test` and all tests pass 34 | - [ ] I have tested the changes manually 35 | - [ ] I have added/updated tests for my changes 36 | 37 | ## Code Quality 38 | 39 | - [ ] My code follows the code style of this project 40 | - [ ] I have performed a self-review of my own code 41 | - [ ] I have commented my code, particularly in hard-to-understand areas 42 | - [ ] My changes generate no new warnings 43 | - [ ] No console.log or debugging code left in 44 | 45 | ## Documentation 46 | 47 | - [ ] I have updated the README.md (if applicable) 48 | - [ ] I have updated the CHANGELOG.md (if applicable) 49 | - [ ] I have added/updated code comments 50 | 51 | ## Breaking Changes 52 | 53 | 54 | 55 | N/A 56 | 57 | ## Screenshots / Examples 58 | 59 | 60 | 61 | ```typescript 62 | // Example code showing the changes 63 | ``` 64 | 65 | ## Checklist 66 | 67 | - [ ] This PR has a descriptive title 68 | - [ ] All commits follow [Conventional Commits](https://www.conventionalcommits.org/) 69 | - [ ] I have read the [CONTRIBUTING](../CONTRIBUTING.md) guide 70 | - [ ] This PR is ready for review 71 | 72 | ## Additional Notes 73 | 74 | 75 | -------------------------------------------------------------------------------- /packages/reca-docs/app/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --max-width: 1100px; 3 | --border-radius: 12px; 4 | --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 5 | 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 6 | 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; 7 | 8 | --foreground-rgb: 0, 0, 0; 9 | --background-start-rgb: 214, 219, 220; 10 | --background-end-rgb: 255, 255, 255; 11 | 12 | --primary-glow: conic-gradient( 13 | from 180deg at 50% 50%, 14 | #16abff33 0deg, 15 | #0885ff33 55deg, 16 | #54d6ff33 120deg, 17 | #0071ff33 160deg, 18 | transparent 360deg 19 | ); 20 | --secondary-glow: radial-gradient( 21 | rgba(255, 255, 255, 1), 22 | rgba(255, 255, 255, 0) 23 | ); 24 | 25 | --tile-start-rgb: 239, 245, 249; 26 | --tile-end-rgb: 228, 232, 233; 27 | --tile-border: conic-gradient( 28 | #00000080, 29 | #00000040, 30 | #00000030, 31 | #00000020, 32 | #00000010, 33 | #00000010, 34 | #00000080 35 | ); 36 | 37 | --callout-rgb: 238, 240, 241; 38 | --callout-border-rgb: 172, 175, 176; 39 | --card-rgb: 180, 185, 188; 40 | --card-border-rgb: 131, 134, 135; 41 | } 42 | 43 | @media (prefers-color-scheme: dark) { 44 | :root { 45 | --foreground-rgb: 255, 255, 255; 46 | --background-start-rgb: 0, 0, 0; 47 | --background-end-rgb: 0, 0, 0; 48 | 49 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); 50 | --secondary-glow: linear-gradient( 51 | to bottom right, 52 | rgba(1, 65, 255, 0), 53 | rgba(1, 65, 255, 0), 54 | rgba(1, 65, 255, 0.3) 55 | ); 56 | 57 | --tile-start-rgb: 2, 13, 46; 58 | --tile-end-rgb: 2, 5, 19; 59 | --tile-border: conic-gradient( 60 | #ffffff80, 61 | #ffffff40, 62 | #ffffff30, 63 | #ffffff20, 64 | #ffffff10, 65 | #ffffff10, 66 | #ffffff80 67 | ); 68 | 69 | --callout-rgb: 20, 20, 20; 70 | --callout-border-rgb: 108, 108, 108; 71 | --card-rgb: 100, 100, 100; 72 | --card-border-rgb: 200, 200, 200; 73 | } 74 | } 75 | 76 | * { 77 | box-sizing: border-box; 78 | padding: 0; 79 | margin: 0; 80 | } 81 | 82 | html, 83 | body { 84 | max-width: 100vw; 85 | overflow-x: hidden; 86 | } 87 | 88 | body { 89 | color: rgb(var(--foreground-rgb)); 90 | background: linear-gradient( 91 | to bottom, 92 | transparent, 93 | rgb(var(--background-end-rgb)) 94 | ) 95 | rgb(var(--background-start-rgb)); 96 | } 97 | 98 | a { 99 | color: inherit; 100 | text-decoration: none; 101 | } 102 | 103 | @media (prefers-color-scheme: dark) { 104 | html { 105 | color-scheme: dark; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /packages/reca/tests/livecycle.spec.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-lines-per-function */ 2 | /* eslint-disable max-statements */ 3 | /* eslint-disable react/jsx-no-bind */ 4 | 5 | import {TestStoreComponent} from "./fixtures/components/TestStoreComponent.js"; 6 | import {render} from "@testing-library/react"; 7 | import {TestAutoStoreComponent} from "./fixtures/components/TestAutoStoreComponent.js"; 8 | import {expect} from "chai"; 9 | import {describe, it} from "node:test"; 10 | 11 | describe("Livecycles must work", () => { 12 | it("Store livecycle", () => { 13 | let state = ""; 14 | const onLivecycleChange = (newState: string): void => { 15 | state = newState; 16 | }; 17 | 18 | /** 19 | * Activate and afterUpdate don't call redraw, 20 | * so state have more livecycle methods than show in view 21 | */ 22 | 23 | // Create component 24 | const comp = render(); 25 | expect(comp.container.innerHTML).to.equal("
init constructor
"); 26 | expect(state).to.equal("init constructor activate"); 27 | 28 | // Update component 29 | comp.rerender(); 30 | expect(comp.container.innerHTML).to.equal("
init constructor activate update
"); 31 | expect(state).to.equal("init constructor activate update afterUpdate"); 32 | 33 | // Delete component 34 | comp.unmount(); 35 | expect(comp.container.innerHTML).to.equal(""); 36 | expect(state).to.equal("init constructor activate update afterUpdate dispose"); 37 | }); 38 | 39 | it("AutoStore livecycle", () => { 40 | let state = ""; 41 | const onLivecycleChange = (newState: string): void => { 42 | state = newState; 43 | }; 44 | 45 | /** 46 | * Activate and afterUpdate don't call redraw, 47 | * so state have more livecycle methods than show in view 48 | */ 49 | 50 | // Create component 51 | const comp = render(); 52 | expect(comp.container.innerHTML).to.equal("
init constructor
"); 53 | expect(state).to.equal("init constructor activate"); 54 | 55 | // Update component 56 | comp.rerender(); 57 | expect(comp.container.innerHTML).to.equal("
init constructor activate update
"); 58 | expect(state).to.equal("init constructor activate update afterUpdate"); 59 | 60 | // Delete component 61 | comp.unmount(); 62 | expect(comp.container.innerHTML).to.equal(""); 63 | expect(state).to.equal("init constructor activate update afterUpdate dispose"); 64 | }); 65 | }); 66 | 67 | -------------------------------------------------------------------------------- /packages/reca/src/hooks/UseClientStore.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | /* eslint-disable max-lines-per-function */ 3 | 4 | import type {ClassConstructor} from "first-di/dist/typings/class-constructor"; 5 | import * as React from "react"; 6 | import {config} from "../config.js"; 7 | import type {Store} from "../stores/Store.js"; 8 | 9 | /** 10 | * Todo: add DI here 11 | * 12 | * @param store 13 | * @param props 14 | * @returns 15 | */ 16 | export const useClientStore =

>( 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 | store: new (...params: any[]) => T, 19 | props?: P 20 | ): T => { 21 | // Render function 22 | const [, setSeed] = React.useState(0); 23 | 24 | // Constructor 25 | let isInit = false; 26 | const [stateStore] = React.useState(() => { 27 | isInit = true; 28 | 29 | // Resolve dependencies 30 | const constructorParams: unknown[] = Reflect 31 | .getMetadata("design:paramtypes", store) as ([] | null) ?? []; 32 | 33 | const resolvedParams = constructorParams.map((param: unknown) => { 34 | if (typeof param === "function" && "prototype" in param) { // Check is class 35 | if (param.prototype === Object.prototype) { // Typescript interface in props (props: P) 36 | return props; 37 | } 38 | 39 | // True class 40 | return config.di.resolver(param as ClassConstructor); 41 | } 42 | 43 | // Else props object 44 | return props; 45 | }); 46 | 47 | // eslint-disable-next-line new-cap 48 | const resolvedStore = new store(...resolvedParams); 49 | 50 | resolvedStore.setRedrawFunction(() => { 51 | setSeed(Math.random()); 52 | }); 53 | 54 | return resolvedStore; 55 | }); 56 | 57 | stateStore.isDrawTime = true; 58 | 59 | // Activate and Dispose(Destructor) methods 60 | React.useEffect(() => { 61 | stateStore.activate(props ?? {} as P); 62 | 63 | return () => stateStore.dispose(props ?? {} as P); 64 | }, []); 65 | 66 | // Update method 67 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 68 | if (!isInit) { 69 | stateStore.update(props ?? {} as P); 70 | } 71 | 72 | // PropsUpdate method 73 | React.useMemo( 74 | () => { 75 | if (!isInit) { 76 | stateStore.propsUpdate(props ?? {} as P); 77 | } 78 | }, 79 | [props ?? {}] 80 | ); 81 | 82 | // AfterUpdate method 83 | React.useEffect(() => { 84 | if (!isInit) { 85 | stateStore.afterUpdate(props ?? {} as P); 86 | } 87 | stateStore.isDrawTime = false; 88 | }); 89 | 90 | return stateStore; 91 | }; 92 | -------------------------------------------------------------------------------- /packages/reca/src/stores/Store.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | 4 | import {config} from "../config.js"; 5 | 6 | export class Store { 7 | 8 | /** 9 | * Property for prevent cycle redraw if call method ot set props in AutoStore 10 | * in view redraw time 11 | */ 12 | public isDrawTime: boolean = true; 13 | 14 | protected redrawFunction: () => void = () => void 0; 15 | 16 | /** 17 | * Method for override in nested store. 18 | * Run after first rendering component in DOM. 19 | * 20 | * @description 21 | * Encapsulate: 22 | * useEffect(() => { 23 | * stateStore.activate(props); 24 | * 25 | * return () => stateStore.dispose(props); 26 | * }, []); 27 | */ 28 | public activate (props: T): void { 29 | // Override 30 | } 31 | 32 | /** 33 | * Method for override in nested store. 34 | * Run before second and others time of rendering component in DOM. 35 | * 36 | * @description 37 | * Encapsulate: 38 | * if (!isInit) { 39 | * stateStore.update(props); 40 | * } 41 | */ 42 | public update (props: T): void { 43 | // Override 44 | } 45 | 46 | /** 47 | * Method for override in nested store. 48 | * Run only if props changed. 49 | * Run before second and others time of rendering component in DOM. 50 | * 51 | * @description 52 | * Encapsulate: 53 | * useMemo( 54 | * () => { 55 | * stateStore.propsUpdate(props); 56 | * }, 57 | * [props] 58 | * ); 59 | */ 60 | public propsUpdate (props: T): void { 61 | // Override 62 | } 63 | 64 | /** 65 | * Method for override in nested store. 66 | * Run after second and others time of rendering component in DOM. 67 | * 68 | * @description 69 | * Encapsulate: 70 | * useEffect(() => { 71 | * if (!isInit) { 72 | * stateStore.afterUpdate(props); 73 | * } 74 | * }); 75 | */ 76 | public afterUpdate (props: T): void { 77 | // Override 78 | } 79 | 80 | /** 81 | * Method for override in nested store. 82 | * Run after second and others rendering component in DOM. 83 | * 84 | * @description 85 | * Encapsulate: 86 | * useEffect(() => { 87 | * stateStore.activate(props); 88 | * 89 | * return () => stateStore.dispose(props); 90 | * }, []); 91 | */ 92 | public dispose (props: T): void { 93 | // Override 94 | } 95 | 96 | public setRedrawFunction (updateFunction: () => void): void { 97 | this.redrawFunction = updateFunction; 98 | } 99 | 100 | /** 101 | * Update view on next requestAnimationFrame 102 | */ 103 | public redraw (): void { 104 | if (this.isDrawTime) { 105 | return; 106 | } 107 | 108 | if (config.isBrowser) { 109 | requestAnimationFrame(() => this.redrawFunction()); 110 | } else { 111 | // SSR don't use redraw, its for unit tests 112 | this.redrawFunction(); 113 | } 114 | } 115 | 116 | /** 117 | * Update view component immediately 118 | */ 119 | public forceRedraw (): void { 120 | this.redrawFunction(); 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /packages/reca/src/stores/AutoStore.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import {Store} from "./Store.js"; 3 | 4 | export class AutoStore extends Store { 5 | 6 | protected dontObserveProperties: (string | symbol) [] = [ 7 | // Methods 8 | "constructor", 9 | "activate", 10 | "update", 11 | "propsUpdate", 12 | "afterUpdate", 13 | "dispose", 14 | "setRedrawFunction", 15 | "redraw", 16 | "forceRedraw", 17 | 18 | // Properties 19 | "isDrawTime", 20 | "dontOverrideMethods", 21 | "redrawFunction" 22 | 23 | ]; 24 | 25 | public constructor () { 26 | super(); 27 | this.injectProperties(); 28 | this.injectMethods(); 29 | } 30 | 31 | /** 32 | * Replace properties by getters and setters, inject redraw 33 | */ 34 | protected injectProperties (): void { 35 | const properties: (string | symbol)[] = Reflect.ownKeys(this); 36 | 37 | for (const property of properties) { 38 | const isNotAuto = Reflect.getMetadata("reca:notAuto", this, property) as unknown; 39 | if (!this.dontObserveProperties.includes(property) && isNotAuto !== true) { 40 | let propValue = Reflect.get(this, property) as unknown; 41 | 42 | Object.defineProperty( 43 | this, 44 | property, 45 | { 46 | get: () => propValue, 47 | set: (value: unknown) => { 48 | propValue = value; 49 | this.redraw(); 50 | } 51 | } 52 | ); 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * Wrap methods, inject redraw 59 | */ 60 | protected injectMethods (): void { 61 | const methods: (string | symbol)[] = Reflect.ownKeys(Object.getPrototypeOf(this) as object); 62 | 63 | for (const method of methods) { 64 | const isNotAuto = Reflect.getMetadata("reca:notAuto", this, method) as unknown; 65 | 66 | if (!this.dontObserveProperties.includes(method) || isNotAuto !== true) { 67 | const methodFunction = Reflect.get(this, method) as (...params: unknown[]) => unknown; 68 | const isNotRedraw = Reflect.getMetadata("reca:notRedraw", this, method) as unknown; 69 | 70 | Reflect.set( 71 | this, 72 | method, 73 | (...params: unknown[]) => { 74 | if (isNotRedraw !== true) { 75 | this.redraw(); // Before method because in method can be error, how stop flow 76 | } 77 | 78 | const maybePromise = methodFunction.apply(this, [...params]); 79 | 80 | if (maybePromise instanceof Promise) { 81 | return maybePromise 82 | .then((data: unknown) => { 83 | if (isNotRedraw !== true) { 84 | this.redraw(); 85 | } 86 | return data; 87 | }) 88 | .catch((error: unknown) => { 89 | if (isNotRedraw !== true) { 90 | this.redraw(); 91 | } 92 | throw error; 93 | }); 94 | } 95 | 96 | return maybePromise; 97 | } 98 | ); 99 | } 100 | } 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We actively support the following versions of `reca` with security updates: 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 2.x | :white_check_mark: | 10 | | 1.x | :x: | 11 | | < 1.0 | :x: | 12 | 13 | ## Reporting a Vulnerability 14 | 15 | We take the security of `reca` seriously. If you discover a security vulnerability, please follow these steps: 16 | 17 | ### How to Report 18 | 19 | 1. **DO NOT** open a public GitHub issue for security vulnerabilities 20 | 2. Send a detailed report to the repository maintainer via: 21 | - GitHub Security Advisory: [Report a vulnerability](https://github.com/LabEG/reca/security/advisories/new) 22 | - Email: Create an issue in the [issue tracker](https://github.com/LabEG/reca/issues) marked as **Security** (if no sensitive details need to be shared) 23 | 24 | ### What to Include 25 | 26 | Please provide the following information in your report: 27 | 28 | - **Description**: A clear description of the vulnerability 29 | - **Impact**: What could an attacker accomplish by exploiting this vulnerability 30 | - **Reproduction**: Step-by-step instructions to reproduce the issue 31 | - **Version**: The version of `reca` affected 32 | - **Environment**: Relevant environment details (Node.js version, React version, etc.) 33 | - **Suggested Fix** (optional): If you have ideas on how to fix the vulnerability 34 | 35 | ### Response Timeline 36 | 37 | - **Initial Response**: Within 48 hours of receiving the report 38 | - **Status Update**: Within 7 days with either a fix timeline or request for more information 39 | - **Resolution**: Security patches will be released as soon as possible, typically within 14 days for critical issues 40 | 41 | ### Security Update Process 42 | 43 | 1. The vulnerability is confirmed and assessed 44 | 2. A fix is developed and tested 45 | 3. A security advisory is prepared 46 | 4. A new version is released with the fix 47 | 5. The security advisory is published with CVE (if applicable) 48 | 49 | ## Security Best Practices 50 | 51 | When using `reca`: 52 | 53 | ### For Package Consumers 54 | 55 | - Always use the latest stable version 56 | - Regularly update dependencies using `npm update` or `npm audit fix` 57 | - Review the [CHANGELOG](./CHANGELOG.md) for security-related updates 58 | - Use `npm audit` to check for known vulnerabilities in dependencies 59 | 60 | ### For Contributors 61 | 62 | - Follow secure coding practices 63 | - Run `npm audit` before submitting pull requests 64 | - Never commit sensitive information (API keys, passwords, tokens) 65 | - Test changes thoroughly with various configurations 66 | 67 | ## Dependency Security 68 | 69 | This package relies on React and minimal dependencies. We: 70 | 71 | - Monitor security advisories for all dependencies 72 | - Update dependencies promptly when security issues are discovered 73 | - Use `npm audit` in our CI/CD pipeline 74 | - Follow semantic versioning to ensure stable updates 75 | 76 | ## Known Security Considerations 77 | 78 | As a state management library, `reca`: 79 | 80 | - **Runs in client-side applications** - ensure proper data sanitization 81 | - **Does not access network resources directly** - network calls are in your code 82 | - **State management only** - security of data depends on your implementation 83 | - **Client-side state** - sensitive data should not be stored in client state 84 | 85 | However, always ensure you: 86 | 87 | - Install packages from official npm registry 88 | - Verify package integrity using `npm audit` 89 | - Review configuration changes before applying 90 | 91 | ## Disclosure Policy 92 | 93 | When a security vulnerability is fixed: 94 | 95 | 1. We will credit the reporter (unless they wish to remain anonymous) 96 | 2. Details will be disclosed after a fix is available 97 | 3. We will publish a security advisory on GitHub 98 | 4. The vulnerability will be documented in the CHANGELOG 99 | 100 | ## Contact 101 | 102 | For any security-related questions or concerns, please: 103 | 104 | - Open a [GitHub Security Advisory](https://github.com/LabEG/reca/security/advisories/new) 105 | - Create an issue at: 106 | 107 | --- 108 | 109 | Thank you for helping keep `reca` and its users safe! 110 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ReCA 2 | 3 | Thank you for your interest in contributing! This document provides guidelines for contributing to ReCA (React Clean Architecture). 4 | 5 | ## Code of Conduct 6 | 7 | By participating in this project, you agree to abide by our [Code of Conduct](./CODE_OF_CONDUCT.md). 8 | 9 | ## How to Contribute 10 | 11 | ### Reporting Bugs 12 | 13 | Before creating bug reports, please check the existing issues. When creating a bug report, include: 14 | 15 | - **Clear title and description** 16 | - **Steps to reproduce** the problem 17 | - **Expected behavior** vs **actual behavior** 18 | - **Code samples** that demonstrate the issue 19 | - **Environment details** (Node.js version, npm version, OS) 20 | 21 | ### Suggesting Enhancements 22 | 23 | Enhancement suggestions are welcome! Please provide: 24 | 25 | - **Clear description** of the enhancement 26 | - **Use cases** and why it would be useful 27 | - **Possible implementation** approach (if you have ideas) 28 | 29 | ### Pull Requests 30 | 31 | 1. **Fork the repository** and create your branch from `main` 32 | 2. **Make your changes** following our coding standards 33 | 3. **Test your changes** by running `npm test` 34 | 4. **Commit your changes** using [Conventional Commits](https://www.conventionalcommits.org/): 35 | - `feat:` for new features 36 | - `fix:` for bug fixes 37 | - `docs:` for documentation changes 38 | - `chore:` for maintenance tasks 39 | - `refactor:` for code refactoring 40 | 5. **Push to your fork** and submit a pull request 41 | 42 | ## Development Setup 43 | 44 | ### Prerequisites 45 | 46 | - Node.js (latest LTS version recommended) 47 | - npm or pnpm 48 | 49 | ### Setup Steps 50 | 51 | ```bash 52 | # Clone your fork 53 | git clone https://github.com/YOUR_USERNAME/reca.git 54 | cd reca 55 | 56 | # Install dependencies 57 | npm install 58 | 59 | # Build packages 60 | npm run build 61 | 62 | # Run tests 63 | npm test 64 | ``` 65 | 66 | ## Coding Standards 67 | 68 | This project uses @labeg/code-style for linting. Key principles: 69 | 70 | ### Code Style 71 | 72 | - **Line length**: Maximum 120 characters 73 | - **Indentation**: 4 spaces 74 | - **Quotes**: Use double quotes `"` or template literals `` ` `` 75 | - **Semicolons**: Always use semicolons 76 | - **Braces**: Always use braces for control structures 77 | - **Clean Architecture**: Keep stores, services, and components separated 78 | 79 | ### Example 80 | 81 | ```typescript 82 | // Good - Clean Store 83 | export class TodoStore extends AutoStore { 84 | public todos: string[] = []; 85 | 86 | public addTodo(todo: string): void { 87 | this.todos.push(todo); 88 | } 89 | } 90 | 91 | // Bad - Logic in component 92 | const Component = () => { 93 | const [todos, setTodos] = useState([]); 94 | // Don't put business logic here 95 | }; 96 | ``` 97 | 98 | ### Commit Messages 99 | 100 | Follow [Conventional Commits](https://www.conventionalcommits.org/): 101 | 102 | ```bash 103 | feat: add support for new ESLint rule 104 | fix: correct TypeScript configuration issue 105 | docs: update README with new examples 106 | chore: upgrade dependencies 107 | ``` 108 | 109 | ## Testing 110 | 111 | Before submitting your PR: 112 | 113 | ```bash 114 | # Run all tests 115 | npm test 116 | 117 | # Run linter 118 | npm run lint 119 | 120 | # Build packages 121 | npm run build 122 | 123 | # Check for security vulnerabilities 124 | npm audit 125 | ``` 126 | 127 | All tests must pass before your PR can be merged. 128 | 129 | ## Review Process 130 | 131 | 1. **Automated checks** run on every PR (tests, linting, security) 132 | 2. **Manual review** by maintainers 133 | 3. **Feedback** may be provided - please address comments 134 | 4. **Approval** - once approved, your PR will be merged 135 | 136 | ## Release Process 137 | 138 | Releases are automated: 139 | 140 | 1. Maintainer merges PR to `main` 141 | 2. Version is bumped automatically 142 | 3. Changelog is generated 143 | 4. Package is published to npm 144 | 5. GitHub release is created 145 | 146 | ## Questions? 147 | 148 | - Open an issue with the `question` label 149 | - Check existing issues and discussions 150 | 151 | ## License 152 | 153 | By contributing, you agree that your contributions will be licensed under the MIT License. 154 | 155 | --- 156 | 157 | Thank you for contributing to ReCA! 🎉 158 | -------------------------------------------------------------------------------- /packages/reca-docs/app/page.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | align-items: center; 6 | padding: 6rem; 7 | min-height: 100vh; 8 | } 9 | 10 | .description { 11 | display: inherit; 12 | justify-content: inherit; 13 | align-items: inherit; 14 | font-size: 0.85rem; 15 | max-width: var(--max-width); 16 | width: 100%; 17 | z-index: 2; 18 | font-family: var(--font-mono); 19 | } 20 | 21 | .description a { 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | gap: 0.5rem; 26 | } 27 | 28 | .description p { 29 | position: relative; 30 | margin: 0; 31 | padding: 1rem; 32 | background-color: rgba(var(--callout-rgb), 0.5); 33 | border: 1px solid rgba(var(--callout-border-rgb), 0.3); 34 | border-radius: var(--border-radius); 35 | } 36 | 37 | .code { 38 | font-weight: 700; 39 | font-family: var(--font-mono); 40 | } 41 | 42 | .grid { 43 | display: grid; 44 | grid-template-columns: repeat(4, minmax(25%, auto)); 45 | max-width: 100%; 46 | width: var(--max-width); 47 | } 48 | 49 | .card { 50 | padding: 1rem 1.2rem; 51 | border-radius: var(--border-radius); 52 | background: rgba(var(--card-rgb), 0); 53 | border: 1px solid rgba(var(--card-border-rgb), 0); 54 | transition: background 200ms, border 200ms; 55 | } 56 | 57 | .card span { 58 | display: inline-block; 59 | transition: transform 200ms; 60 | } 61 | 62 | .card h2 { 63 | font-weight: 600; 64 | margin-bottom: 0.7rem; 65 | } 66 | 67 | .card p { 68 | margin: 0; 69 | opacity: 0.6; 70 | font-size: 0.9rem; 71 | line-height: 1.5; 72 | max-width: 30ch; 73 | } 74 | 75 | .center { 76 | display: flex; 77 | justify-content: center; 78 | align-items: center; 79 | position: relative; 80 | padding: 4rem 0; 81 | } 82 | 83 | .center::before { 84 | background: var(--secondary-glow); 85 | border-radius: 50%; 86 | width: 480px; 87 | height: 360px; 88 | margin-left: -400px; 89 | } 90 | 91 | .center::after { 92 | background: var(--primary-glow); 93 | width: 240px; 94 | height: 180px; 95 | z-index: -1; 96 | } 97 | 98 | .center::before, 99 | .center::after { 100 | content: ''; 101 | left: 50%; 102 | position: absolute; 103 | filter: blur(45px); 104 | transform: translateZ(0); 105 | } 106 | 107 | .logo { 108 | position: relative; 109 | } 110 | /* Enable hover only on non-touch devices */ 111 | @media (hover: hover) and (pointer: fine) { 112 | .card:hover { 113 | background: rgba(var(--card-rgb), 0.1); 114 | border: 1px solid rgba(var(--card-border-rgb), 0.15); 115 | } 116 | 117 | .card:hover span { 118 | transform: translateX(4px); 119 | } 120 | } 121 | 122 | @media (prefers-reduced-motion) { 123 | .card:hover span { 124 | transform: none; 125 | } 126 | } 127 | 128 | /* Mobile */ 129 | @media (max-width: 700px) { 130 | .content { 131 | padding: 4rem; 132 | } 133 | 134 | .grid { 135 | grid-template-columns: 1fr; 136 | margin-bottom: 120px; 137 | max-width: 320px; 138 | text-align: center; 139 | } 140 | 141 | .card { 142 | padding: 1rem 2.5rem; 143 | } 144 | 145 | .card h2 { 146 | margin-bottom: 0.5rem; 147 | } 148 | 149 | .center { 150 | padding: 8rem 0 6rem; 151 | } 152 | 153 | .center::before { 154 | transform: none; 155 | height: 300px; 156 | } 157 | 158 | .description { 159 | font-size: 0.8rem; 160 | } 161 | 162 | .description a { 163 | padding: 1rem; 164 | } 165 | 166 | .description p, 167 | .description div { 168 | display: flex; 169 | justify-content: center; 170 | position: fixed; 171 | width: 100%; 172 | } 173 | 174 | .description p { 175 | align-items: center; 176 | inset: 0 0 auto; 177 | padding: 2rem 1rem 1.4rem; 178 | border-radius: 0; 179 | border: none; 180 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); 181 | background: linear-gradient( 182 | to bottom, 183 | rgba(var(--background-start-rgb), 1), 184 | rgba(var(--callout-rgb), 0.5) 185 | ); 186 | background-clip: padding-box; 187 | backdrop-filter: blur(24px); 188 | } 189 | 190 | .description div { 191 | align-items: flex-end; 192 | pointer-events: none; 193 | inset: auto 0 0; 194 | padding: 2rem; 195 | height: 200px; 196 | background: linear-gradient( 197 | to bottom, 198 | transparent 0%, 199 | rgb(var(--background-end-rgb)) 40% 200 | ); 201 | z-index: 1; 202 | } 203 | } 204 | 205 | /* Tablet and Smaller Desktop */ 206 | @media (min-width: 701px) and (max-width: 1120px) { 207 | .grid { 208 | grid-template-columns: repeat(2, 50%); 209 | } 210 | } 211 | 212 | @media (prefers-color-scheme: dark) { 213 | .vercelLogo { 214 | filter: invert(1); 215 | } 216 | 217 | .logo { 218 | filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); 219 | } 220 | } 221 | 222 | @keyframes rotate { 223 | from { 224 | transform: rotate(360deg); 225 | } 226 | to { 227 | transform: rotate(0deg); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /packages/reca/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [2.4.3](https://github.com/LabEG/reca/compare/v2.0.17...v2.4.3) (2025-12-17) 6 | 7 | ### [2.4.2](https://github.com/LabEG/reca/compare/v2.0.16...v2.4.2) (2025-12-14) 8 | 9 | ### [2.4.1](https://github.com/LabEG/reca/compare/v2.0.15...v2.4.1) (2025-12-14) 10 | 11 | ## [2.4.0](https://github.com/LabEG/reca/compare/v2.0.14...v2.4.0) (2025-12-14) 12 | 13 | 14 | ### Features 15 | 16 | * add README copying step to npm publish workflow ([ee93ac9](https://github.com/LabEG/reca/commit/ee93ac9ffbc358cb0f3a1d595badc05e2e2f2175)) 17 | 18 | ### [2.3.8](https://github.com/LabEG/reca/compare/v2.0.13...v2.3.8) (2025-12-14) 19 | 20 | ### [2.3.7](https://github.com/LabEG/reca/compare/v2.0.12...v2.3.7) (2025-12-14) 21 | 22 | ### [2.3.6](https://github.com/LabEG/reca/compare/v2.0.11...v2.3.6) (2025-12-14) 23 | 24 | ### [2.3.5](https://github.com/LabEG/reca/compare/v2.0.10...v2.3.5) (2025-12-14) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * update build status badge in README.md ([8ce28a1](https://github.com/LabEG/reca/commit/8ce28a1c4d1db9e926020754d024b9ebab9b5c04)) 30 | 31 | ### [2.3.4](https://github.com/LabEG/reca/compare/v2.0.9...v2.3.4) (2025-12-14) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * update README and package.json for accurate badge links and repository metadata ([d834244](https://github.com/LabEG/reca/commit/d834244dd115b98c368d37a271dba2161c49d383)) 37 | 38 | ### [2.3.3](https://github.com/LabEG/reca/compare/v2.0.8...v2.3.3) (2025-12-14) 39 | 40 | 41 | ### Bug Fixes 42 | 43 | * simplify NPM publish command by removing unnecessary directory change ([a218baa](https://github.com/LabEG/reca/commit/a218baa7f722281e27d9e3adc957f9a50a2491cd)) 44 | 45 | ### [2.3.2](https://github.com/LabEG/reca/compare/v2.0.7...v2.3.2) (2025-12-14) 46 | 47 | ### [2.3.1](https://github.com/LabEG/reca/compare/v2.0.6...v2.3.1) (2025-12-14) 48 | 49 | 50 | ### Bug Fixes 51 | 52 | * remove dry-run build verification step from test workflow ([ba43452](https://github.com/LabEG/reca/commit/ba434522259363d3fae55fc41329565e2b39995c)) 53 | 54 | ## [2.3.0](https://github.com/LabEG/reca/compare/v2.0.5...v2.3.0) (2025-12-14) 55 | 56 | 57 | ### Features 58 | 59 | * add issue templates for bug reports and feature requests, and implement security policy ([212d1a7](https://github.com/LabEG/reca/commit/212d1a7eba238117674a9ac93f3371157ab1e7b6)) 60 | 61 | 62 | ### Bug Fixes 63 | 64 | * add test script to package.json for linting during testing ([3f0ca92](https://github.com/LabEG/reca/commit/3f0ca921720cc6e87af7bef2ec392e81572786d3)) 65 | * correct files field in package.json to include necessary files ([2fa2202](https://github.com/LabEG/reca/commit/2fa220248faf3460b085a3056fbfba257ce67804)) 66 | * reorder badges and improve README structure for clarity ([1cd8c17](https://github.com/LabEG/reca/commit/1cd8c17084d55a1a48193dfe1d2c3e2f199f73de)) 67 | * update badge order and remove duplicate Codacy badge in README ([f3d0370](https://github.com/LabEG/reca/commit/f3d0370d4fd373faf4505986f1ea60fa8f9b0ace)) 68 | * update workflows and documentation to use 'main' branch instead of 'master' ([d47bbef](https://github.com/LabEG/reca/commit/d47bbef2dc9bef04fd8ac48f6ae8445a85ebdc93)) 69 | 70 | ## [2.2.0](https://github.com/LabEG/reca/compare/v2.0.4...v2.2.0) (2025-04-10) 71 | 72 | 73 | ### Features 74 | 75 | * update packages versions ([2067058](https://github.com/LabEG/reca/commit/2067058f8500acfcc2e6e6a054858a2fb673ad7d)) 76 | 77 | ### [2.1.3](https://github.com/LabEG/reca/compare/v2.0.3...v2.1.3) (2025-01-03) 78 | 79 | ### [2.1.2](https://github.com/LabEG/reca/compare/v2.0.2...v2.1.2) (2025-01-03) 80 | 81 | ### [2.1.1](https://github.com/LabEG/reca/compare/v2.0.1...v2.1.1) (2025-01-03) 82 | 83 | ## [2.1.0](https://github.com/LabEG/reca/compare/v0.0.8...v2.1.0) (2025-01-03) 84 | 85 | 86 | ### Features 87 | 88 | * add notAuto and notRedraw decorators for auto store ([b14f3ea](https://github.com/LabEG/reca/commit/b14f3eae7b330fb244c1c72038dda041edb75f98)) 89 | * add performance unit test ([9e1e1fe](https://github.com/LabEG/reca/commit/9e1e1feab7e6924823dcafe5abd4d241c936ea93)) 90 | * add support notAuto decorator for auto store properties ([7729767](https://github.com/LabEG/reca/commit/772976752da7f40cfc280d19554ea98db3fddd14)) 91 | * add support server side components ([ab2feee](https://github.com/LabEG/reca/commit/ab2feee6e8aef6c6f0a3c69a5819ec1154a81a9d)) 92 | * add support unit tests for nodejs engine, write unit test for sample todo component ([8937e1a](https://github.com/LabEG/reca/commit/8937e1a7ba7fcef95d70060737ab9be11bf3baa7)) 93 | * update packages versions ([8fb150d](https://github.com/LabEG/reca/commit/8fb150dd83a81fce01ef99983f9dcd4111529480)) 94 | 95 | 96 | ### Bug Fixes 97 | 98 | * exports decorators for module type ([78330cc](https://github.com/LabEG/reca/commit/78330ccb62451c199bc0688f312990f02a35592d)) 99 | * props interface in params detection ([788afc3](https://github.com/LabEG/reca/commit/788afc3627f06b8b757a25b933e6db7585386c8d)) 100 | * test for publishing ([8730a3d](https://github.com/LabEG/reca/commit/8730a3d9453addf8b437e42568a69778bc762b3c)) 101 | 102 | ### [2.0.1](https://github.com/LabEG/reca/compare/v1.1.13...v2.0.1) (2023-12-22) 103 | -------------------------------------------------------------------------------- /packages/reca-docs/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import styles from "./page.module.css"; 3 | import {AutoStore, reflection, useStoreAsync} from "reca"; 4 | import {JSX} from "react"; 5 | 6 | @reflection 7 | class TStore extends AutoStore { 8 | 9 | public prop = 0; 10 | 11 | // eslint-disable-next-line class-methods-use-this 12 | public async testAsync (): Promise { 13 | return await Promise.resolve(5); 14 | } 15 | 16 | } 17 | 18 | // eslint-disable-next-line max-lines-per-function 19 | const Home = async (): Promise => { 20 | // eslint-disable-next-line react-hooks/rules-of-hooks 21 | const store = await useStoreAsync(TStore); 22 | 23 | const num = await store.testAsync(); 24 | // eslint-disable-next-line no-console 25 | console.log("111111111111", store.prop, num); 26 | return ( 27 |
28 |
29 |

30 | Get started by editing 31 | {" "} 32 | 33 | 34 | app/page.tsx 35 | 36 |

37 | 38 | 57 |
58 | 59 |
60 | Next.js Logo 68 |
69 | 70 | 151 |
152 | ); 153 | }; 154 | 155 | export default Home; 156 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | labeg@mail.ru or [discord server](https://discordapp.com/channels/974049080454045796/974049142022209566). 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [2.0.18](https://github.com/LabEG/reca/compare/v2.4.3...v2.0.18) (2025-12-17) 6 | 7 | ### [2.0.17](https://github.com/LabEG/reca/compare/v2.4.2...v2.0.17) (2025-12-14) 8 | 9 | ### [2.0.16](https://github.com/LabEG/reca/compare/v2.4.1...v2.0.16) (2025-12-14) 10 | 11 | ### [2.0.15](https://github.com/LabEG/reca/compare/v2.4.0...v2.0.15) (2025-12-14) 12 | 13 | ### [2.0.14](https://github.com/LabEG/reca/compare/v2.3.8...v2.0.14) (2025-12-14) 14 | 15 | ### [2.0.13](https://github.com/LabEG/reca/compare/v2.3.7...v2.0.13) (2025-12-14) 16 | 17 | ### [2.0.12](https://github.com/LabEG/reca/compare/v2.3.6...v2.0.12) (2025-12-14) 18 | 19 | ### [2.0.11](https://github.com/LabEG/reca/compare/v2.3.5...v2.0.11) (2025-12-14) 20 | 21 | ### [2.0.10](https://github.com/LabEG/reca/compare/v2.3.4...v2.0.10) (2025-12-14) 22 | 23 | ### [2.0.9](https://github.com/LabEG/reca/compare/v2.3.3...v2.0.9) (2025-12-14) 24 | 25 | ### [2.0.8](https://github.com/LabEG/reca/compare/v2.3.2...v2.0.8) (2025-12-14) 26 | 27 | ### [2.0.7](https://github.com/LabEG/reca/compare/v2.3.1...v2.0.7) (2025-12-14) 28 | 29 | ### [2.0.6](https://github.com/LabEG/reca/compare/v2.3.0...v2.0.6) (2025-12-14) 30 | 31 | ### [2.0.5](https://github.com/LabEG/reca/compare/v2.2.0...v2.0.5) (2025-04-10) 32 | 33 | ### [2.0.4](https://github.com/LabEG/reca/compare/v2.1.3...v2.0.4) (2025-01-03) 34 | 35 | ### [2.0.3](https://github.com/LabEG/reca/compare/v2.1.2...v2.0.3) (2025-01-03) 36 | 37 | ### [2.0.2](https://github.com/LabEG/reca/compare/v2.1.1...v2.0.2) (2025-01-03) 38 | 39 | ### [2.0.1](https://github.com/LabEG/reca/compare/v2.1.0...v2.0.1) (2025-01-03) 40 | 41 | ### [1.1.13](https://github.com/LabEG/reca/compare/v1.1.12...v1.1.13) (2023-12-16) 42 | 43 | ### [1.1.12](https://github.com/LabEG/reca/compare/v1.1.11...v1.1.12) (2023-12-16) 44 | 45 | ### [1.1.11](https://github.com/LabEG/reca/compare/v1.1.10...v1.1.11) (2023-12-02) 46 | 47 | ### [1.1.10](https://github.com/LabEG/reca/compare/v1.1.9...v1.1.10) (2023-12-02) 48 | 49 | ### [1.1.9](https://github.com/LabEG/reca/compare/v1.1.8...v1.1.9) (2023-09-01) 50 | 51 | ### [1.1.8](https://github.com/LabEG/reca/compare/v1.1.5...v1.1.8) (2023-07-14) 52 | 53 | ### [1.1.7](https://github.com/LabEG/reca/compare/v1.1.6...v1.1.7) (2023-06-25) 54 | 55 | ### [1.1.6](https://github.com/LabEG/reca/compare/v1.1.2...v1.1.6) (2023-06-25) 56 | 57 | ### [1.1.5](https://github.com/LabEG/reca/compare/v1.1.4...v1.1.5) (2023-02-09) 58 | 59 | ### [1.1.4](https://github.com/LabEG/reca/compare/v1.1.3...v1.1.4) (2023-02-09) 60 | 61 | ### [1.1.3](https://github.com/LabEG/reca/compare/v1.1.2...v1.1.3) (2023-02-08) 62 | 63 | ### [1.1.2](https://github.com/LabEG/reca/compare/v1.1.1...v1.1.2) (2022-12-10) 64 | 65 | ### [1.1.1](https://github.com/LabEG/reca/compare/v1.1.0...v1.1.1) (2022-11-22) 66 | 67 | ## [1.1.0](https://github.com/LabEG/reca/compare/v0.0.13...v1.1.0) (2022-11-22) 68 | 69 | 70 | ### Features 71 | 72 | * add support notAuto decorator for auto store properties ([7729767](https://github.com/LabEG/reca/commit/772976752da7f40cfc280d19554ea98db3fddd14)) 73 | 74 | 75 | ### Bug Fixes 76 | 77 | * exports decorators for module type ([78330cc](https://github.com/LabEG/reca/commit/78330ccb62451c199bc0688f312990f02a35592d)) 78 | 79 | ### [0.0.13](https://github.com/LabEG/reca/compare/v0.0.12...v0.0.13) (2022-07-26) 80 | 81 | 82 | ### Features 83 | 84 | * add notAuto and notRedraw decorators for auto store ([b14f3ea](https://github.com/LabEG/reca/commit/b14f3eae7b330fb244c1c72038dda041edb75f98)) 85 | 86 | ### [0.0.12](https://github.com/LabEG/reca/compare/v0.0.11...v0.0.12) (2022-07-13) 87 | 88 | ### [0.0.11](https://github.com/LabEG/reca/compare/v0.0.10...v0.0.11) (2022-07-13) 89 | 90 | 91 | ### Bug Fixes 92 | 93 | * props interface in params detection ([788afc3](https://github.com/LabEG/reca/commit/788afc3627f06b8b757a25b933e6db7585386c8d)) 94 | * test for publishing ([8730a3d](https://github.com/LabEG/reca/commit/8730a3d9453addf8b437e42568a69778bc762b3c)) 95 | 96 | ### [0.0.10](https://github.com/LabEG/reca/compare/v0.0.9...v0.0.10) (2022-07-01) 97 | 98 | ### [0.0.9](https://github.com/LabEG/reca/compare/v0.0.8...v0.0.9) (2022-06-30) 99 | 100 | 101 | ### Features 102 | 103 | * add performance unit test ([9e1e1fe](https://github.com/LabEG/reca/commit/9e1e1feab7e6924823dcafe5abd4d241c936ea93)) 104 | * add support unit tests for nodejs engine, write unit test for sample todo component ([8937e1a](https://github.com/LabEG/reca/commit/8937e1a7ba7fcef95d70060737ab9be11bf3baa7)) 105 | 106 | ### [0.0.8](https://github.com/LabEG/reca/compare/v0.0.7...v0.0.8) (2022-06-15) 107 | 108 | 109 | ### Features 110 | 111 | * complete autostore, add autostore tests, add propsUpdate livacycle method ([7930f4a](https://github.com/LabEG/reca/commit/7930f4a7f39e8560ea13598f5d442fcc52197b43)) 112 | 113 | ### [0.0.7](https://github.com/LabEG/reca/compare/v0.0.6...v0.0.7) (2022-05-29) 114 | 115 | 116 | ### Bug Fixes 117 | 118 | * autostore self redraw ([5ce5a36](https://github.com/LabEG/reca/commit/5ce5a364f80b4c92b739577eaa2869bf7c804da4)) 119 | 120 | ### [0.0.6](https://github.com/LabEG/reca/compare/v0.0.5...v0.0.6) (2022-05-28) 121 | 122 | 123 | ### Features 124 | 125 | * complete autostore ([72bd447](https://github.com/LabEG/reca/commit/72bd44744c535118e7516221168580aad9a78085)) 126 | * fix build and publish new version ([4c7b67e](https://github.com/LabEG/reca/commit/4c7b67ec163a08ce6061b31463a14b8d24cdf109)) 127 | 128 | 129 | ### Bug Fixes 130 | 131 | * module import ([9c5ea11](https://github.com/LabEG/reca/commit/9c5ea11de04a8bbb0ab557547edab69a99384b87)) 132 | 133 | ### [0.0.5](https://github.com/LabEG/reca/compare/v0.0.4...v0.0.5) (2022-05-28) 134 | 135 | ### [0.0.4](https://github.com/LabEG/reca/compare/v0.0.3...v0.0.4) (2022-05-28) 136 | 137 | 138 | ### Features 139 | 140 | * add support pass props in contructor ([c22c639](https://github.com/LabEG/reca/commit/c22c639d5b5f66394fc50baf9c32d6280e1da045)) 141 | * begin making autostore ([c0dea00](https://github.com/LabEG/reca/commit/c0dea005e561dc1c2f661c7cc04de789281188b6)) 142 | * replace enzyme by resct test library, update react to version 18 ([cca36a7](https://github.com/LabEG/reca/commit/cca36a76b8d0243d976c93181a8001f4947b3f67)) 143 | 144 | ### 0.0.3 (2022-05-11) 145 | 146 | 147 | ### Bug Fixes 148 | 149 | * complete husky setup ([5f2bb1d](https://github.com/LabEG/reca/commit/5f2bb1d5b182b50b6fd4b4286946113d9af6bd38)) 150 | * livecycle methods ([66889f8](https://github.com/LabEG/reca/commit/66889f81ca236efdec14dbb850533a1f6edd5b86)) 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReCA - React Clean Architecture state manager 2 | 3 | [![npm version](https://img.shields.io/npm/v/reca.svg?style=flat)](https://www.npmjs.com/package/reca) 4 | [![GitHub license](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/LabEG/reca/blob/main/LICENSE) 5 | ![npm downloads](https://img.shields.io/npm/dm/reca.svg) 6 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/e9e573d8408945168d14d83c81a103e6)](https://www.codacy.com/gh/LabEG/reca/dashboard?utm_source=github.com&utm_medium=referral&utm_content=LabEG/reca&utm_campaign=Badge_Grade) 7 | ![build status](https://github.com/LabEG/reca/workflows/Test%20Pull%20Request/badge.svg) 8 | [![CodeQL](https://github.com/LabEG/reca/workflows/CodeQL%20Advanced/badge.svg)](https://github.com/LabEG/reca/security/code-scanning) 9 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md) 10 | 11 | Created at the intersection of Functional style and OOP technologies. It is based on the simplicity of the functional style of the view, enriched with OOP technologies for writing business logic. Perfect for beginner developers and complex enterprise applications 12 | 13 | ## Features 14 | 15 | - **Microstores** - calculations state of components don't affect to other components, small CPU usage for update states, 16 | - **Direct Functions Call** - don't need heavy CPU utilization for search function in reducer, just call the function directly, 17 | - **No Boilerplate** - write only business code without those debt, 18 | - **Dependency Injection** - override any part of your application for unit test or other customer, 19 | - **Microfrontend** - perfect support microfrontends out the box without any boilerplates, 20 | - **Simple Data Flow** - don't need search functions call chain for debug your reducers, 21 | - **Code Organization** - structures the code easily even for large enterprise applications, 22 | - **Extra Small Size** - only 1kb of minified code. 23 | 24 | ## Comparison with Other Libraries 25 | 26 | | Feature | ReCA | Zustand | MobX | Redux | 27 | |---------|------|----------|------|-------| 28 | | **Bundle Size** | ~1KB | ~1KB | ~16KB | ~8KB | 29 | | **Boilerplate** | Minimal | Minimal | Medium | Heavy | 30 | | **Learning Curve** | Easy | Easy | Medium | Steep | 31 | | **TypeScript** | Built-in | Good | Good | Good | 32 | | **Performance** | Excellent | Excellent | Excellent | Good | 33 | | **Dependency Injection** | ✅ Built-in | ❌ Manual | ❌ Manual | ❌ Manual | 34 | | **Clean Architecture** | ✅ Native | ❌ Limited | ⚠️ Requires setup | ⚠️ Requires setup | 35 | | **Microstores** | ✅ Yes | ✅ Yes | ✅ Yes | ❌ Monostore | 36 | | **SSR Support** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | 37 | | **Middleware** | Via DI | ✅ Yes | ❌ Limited | ✅ Yes | 38 | | **Async Actions** | ✅ Native | ✅ Native | ✅ Native | ⚠️ Requires thunk/saga | 39 | 40 | ### Why Choose ReCA? 41 | 42 | - **Smallest footprint** - Only 1KB minified, same as Zustand but with more features 43 | - **Zero boilerplate** - No actions, reducers, or dispatchers needed 44 | - **Enterprise-ready** - Built-in DI and Clean Architecture support 45 | - **Developer-friendly** - Simple API, easy to learn and use 46 | - **Flexible** - Choose between AutoStore (automatic) or Store (manual control) 47 | - **Type-safe** - Full TypeScript support out of the box 48 | 49 | ## Installation 50 | 51 | ### Using npm 52 | 53 | ```bash 54 | npm install reca reflect-metadata 55 | ``` 56 | 57 | ### Using yarn 58 | 59 | ```bash 60 | yarn add reca reflect-metadata 61 | ``` 62 | 63 | ### Using pnpm 64 | 65 | ```bash 66 | pnpm add reca reflect-metadata 67 | ``` 68 | 69 | ### Setup 70 | 71 | After installation, import `reflect-metadata` at the entry point of your application (e.g., `index.tsx` or `main.tsx`): 72 | 73 | ```typescript 74 | import "reflect-metadata"; 75 | import React from "react"; 76 | import ReactDOM from "react-dom/client"; 77 | import { App } from "./App"; 78 | 79 | ReactDOM.createRoot(document.getElementById("root")!).render(); 80 | ``` 81 | 82 | > **Note:** The `reflect-metadata` package is required for dependency injection functionality. Make sure to import it before any other imports in your application entry point. 83 | 84 | ## Examples 85 | 86 | ### Quick Start - Counter Example 87 | 88 | A simple example to get you started with ReCA: 89 | 90 | ```typescript 91 | // counter.store.ts 92 | import { AutoStore } from "reca"; 93 | 94 | export class CounterStore extends AutoStore { 95 | public count: number = 0; 96 | 97 | public increment(): void { 98 | this.count++; 99 | } 100 | 101 | public decrement(): void { 102 | this.count--; 103 | } 104 | } 105 | 106 | // Counter.tsx 107 | import { useStore } from "reca"; 108 | import { CounterStore } from "./stores/counter.store"; 109 | 110 | export const Counter = () => { 111 | const store = useStore(CounterStore); 112 | 113 | return ( 114 |
115 |

Count: {store.count}

116 | 117 | 118 |
119 | ); 120 | }; 121 | ``` 122 | 123 | ### ToDo Example 124 | 125 | Create your Store by inheriting from AutoStore, and use it in a component via useStore hook. 126 | 127 | ``` typescript 128 | // todo.store.ts 129 | import {AutoStore} from "reca"; 130 | import type {FormEvent} from "react"; 131 | 132 | export class ToDoStore extends AutoStore { 133 | 134 | public currentTodo: string = ""; 135 | 136 | public todos: string[] = []; 137 | 138 | public handleAddTodo (): void { 139 | this.todos.push(this.currentTodo); 140 | } 141 | 142 | public handleDeleteTodo (index: number): void { 143 | this.todos.splice(index, 1); 144 | } 145 | 146 | public handleCurrentEdit (event: FormEvent): void { 147 | this.currentTodo = event.currentTarget.value; 148 | } 149 | 150 | } 151 | 152 | 153 | // todo.component.ts 154 | import {useStore} from "reca"; 155 | import {ToDoStore} from "../stores/todo.store"; 156 | 157 | export const ToDoComponent = (): JSX.Element => { 158 | const store = useStore(ToDoStore); 159 | 160 | return ( 161 |
162 |
163 | { 164 | store.todos.map((todo, index) => ( 165 |
166 | {todo} 167 | 168 | 175 |
176 | )) 177 | } 178 |
179 | 180 |
181 | 185 | 186 | 192 |
193 |
194 | ); 195 | }; 196 | ``` 197 | 198 | ### Example low-level Store 199 | 200 | Also, if you need uncompromising performance, you can use the low-level Store. But you will need to start redrawing manually using the `this.redraw()` method. Also you must pass arrow function to all used HTMLElement events, such as onClick. 201 | 202 | ``` typescript 203 | // todo.store.ts 204 | import {Store} from "reca"; 205 | import type {FormEvent} from "react"; 206 | 207 | export class ToDoStore extends Store { 208 | 209 | public currentTodo: string = ""; 210 | 211 | public todos: string[] = []; 212 | 213 | public handleAddTodo (): void { 214 | this.todos.push(this.currentTodo); 215 | this.redraw(); 216 | } 217 | 218 | public handleDeleteTodo (index: number): void { 219 | this.todos.splice(index, 1); 220 | this.redraw(); 221 | } 222 | 223 | public handleCurrentEdit (event: FormEvent): void { 224 | this.currentTodo = event.currentTarget.value; 225 | this.redraw(); 226 | } 227 | } 228 | 229 | 230 | // todo.component.ts 231 | import {useStore} from "reca"; 232 | import {ToDoStore} from "../stores/todo.store"; 233 | 234 | export const ToDoComponent = (): JSX.Element => { 235 | const store = useStore(ToDoStore); 236 | 237 | return ( 238 |
239 | ... 240 | 241 |
242 | store.handleCurrentEdit()} 244 | value={store.currentTodo} 245 | /> 246 | 247 | 253 |
254 |
255 | ); 256 | }; 257 | ``` 258 | 259 | ### Advanced Example - Dependency Injection for Enterprise Applications 260 | 261 | This example demonstrates how to build scalable enterprise applications using ReCA with Dependency Injection. It shows the simplicity of business logic organization following Clean Architecture principles. 262 | 263 | The example includes: 264 | 265 | - **Service Layer** - encapsulates business logic and external API calls 266 | - **Model Layer** - defines data structures 267 | - **Store Layer** - manages state and coordinates services 268 | - **Component Layer** - pure view logic 269 | 270 | This architecture makes your code: 271 | 272 | - **Testable** - easily mock services for unit tests 273 | - **Maintainable** - clear separation of concerns 274 | - **Scalable** - add new features without modifying existing code 275 | - **Flexible** - swap implementations through DI (e.g., Repository, Provider, Logger) 276 | 277 | ```typescript 278 | // SpaceXCompanyInfo.ts 279 | export class SpaceXCompanyInfo { 280 | 281 | public name: string = ""; 282 | 283 | public founder: string = ""; 284 | 285 | public employees: number = 0; 286 | 287 | public applyData (json: object): this { 288 | Object.assign(this, json); 289 | return this; 290 | } 291 | 292 | } 293 | 294 | 295 | // SpaceXService.ts 296 | import {reflection} from "first-di"; 297 | import {SpaceXCompanyInfo} from "../models/SpaceXCompanyInfo"; 298 | 299 | @reflection 300 | export class SpaceXService { 301 | 302 | public async getCompanyInfo (): Promise { 303 | const response = await fetch("https://api.spacexdata.com/v3/info"); 304 | const json: unknown = await response.json(); 305 | 306 | // ... and manies manies lines of logics 307 | 308 | if (typeof json === "object" && json !== null) { 309 | return new SpaceXCompanyInfo().applyData(json); 310 | } 311 | throw new Error("SpaceXService.getCompanyInfo: response object is not json"); 312 | } 313 | 314 | } 315 | 316 | 317 | // SpaceXStore.ts 318 | import {reflection} from "first-di"; 319 | import {AutoStore} from "reca"; 320 | import {SpaceXCompanyInfo} from "../models/SpaceXCompanyInfo.js"; 321 | import {SpaceXService} from "../services/SpaceXService.js"; 322 | 323 | @reflection 324 | export class SpaceXStore extends AutoStore { 325 | 326 | public companyInfo: SpaceXCompanyInfo = new SpaceXCompanyInfo(); 327 | 328 | public constructor ( 329 | private readonly spaceXService: SpaceXService, 330 | // private readonly logger: Logger 331 | ) { 332 | super(); 333 | } 334 | 335 | public activate (): void { 336 | this.fetchCompanyInfo(); 337 | } 338 | 339 | private async fetchCompanyInfo (): Promise { 340 | try { 341 | this.companyInfo = await this.spaceXService.getCompanyInfo(); 342 | } catch (error) { 343 | // Process exceptions, ex: this.logger.error(error.message); 344 | } 345 | } 346 | 347 | } 348 | 349 | 350 | // SpaceXComponent.tsx 351 | import {useStore} from "reca"; 352 | import {SpaceXStore} from "../stores/SpaceXStore.js"; 353 | 354 | export const TestStoreComponent = (): JSX.Element => { 355 | const store = useStore(SpaceXStore); 356 | 357 | return ( 358 |
359 |

360 | Company: 361 | {" "} 362 | 363 | {store.companyInfo.name} 364 |

365 | 366 |

367 | Founder: 368 | {" "} 369 | 370 | {store.companyInfo.founder} 371 |

372 |
373 | ); 374 | }; 375 | 376 | ``` 377 | 378 | ## Documentation and Resources 379 | 380 | ### Documentation 381 | 382 | - **[Wiki](https://github.com/LabEG/reca/wiki)** - Comprehensive guides, tutorials, and API reference 383 | - **[API Documentation](https://github.com/LabEG/reca/wiki)** - Detailed API documentation for all features 384 | 385 | ### Community and Support 386 | 387 | - **[Discord Server](https://discordapp.com/channels/974049080454045796/974049142022209566)** - Join our community for real-time help and discussions 388 | - **[GitHub Discussions](https://github.com/LabEG/reca/discussions)** - Ask questions and share ideas 389 | - **[GitHub Issues](https://github.com/LabEG/reca/issues)** - Report bugs or request features 390 | 391 | ### Contributing 392 | 393 | We welcome contributions! See our: 394 | 395 | - **[Contributing Guide](CONTRIBUTING.md)** - Learn how to contribute to the project 396 | - **[Code of Conduct](CODE_OF_CONDUCT.md)** - Our community guidelines 397 | - **[Security Policy](SECURITY.md)** - How to report security vulnerabilities 398 | 399 | ## License 400 | 401 | ReCA is [MIT licensed](https://github.com/LabEG/reca/blob/main/LICENSE). 402 | --------------------------------------------------------------------------------