Code Standards
The conventions reviewers apply to every change in erxes/erxes, drawn from the repository's rule files and the lint and formatter configuration. For the fork-and-PR workflow, see Contribute to codebase.
How the rules are organized
The root AGENTS.md is the repository-wide rule file, and each directory can carry its own. Nested files may add constraints or pick among approved patterns, but they cannot weaken the non-negotiable rules, the plugin scope boundary, security requirements, or tenant isolation. When several guides apply to a file, all of them apply.
When rules conflict with preference, the working principles set a priority order: existing repository patterns first, then local plugin consistency, minimal changes, clear behavior and maintainability, reusability, performance, and personal preference last. If you are unsure whether a change violates a rule, ask rather than guess.
Non-negotiable rules
These must hold in any new or modified code.
| Rule | Why | How to check |
|---|---|---|
Use erxes-ui and ui-modules components; never import @radix-ui/* directly or hand-roll primitives; prefer composed APIs (Form.Field, RecordTable.Provider, Sheet.Content) | One design system; primitives already wrap Radix | Grep the diff for @radix-ui imports |
After a create/update/delete mutation, update the UI immediately through Apollo cache, refetch, or subscribeToMore | Users must never need a manual refresh | Exercise the mutation in the browser without reloading |
| Completeness: every button has a handler, every form validates, every list has loading and empty states, every mutation reports success/error; no placeholder code | Half-finished pages ship bugs to self-hosters | Click through the whole flow in each state |
| Named exports in application code; a default export only where an external tool (Nx, Rspack, Module Federation loader) requires it | Named exports survive refactors and are what host loaders import | Review exports in the diff |
No any, as any, or unnecessary casts in new or modified code; type all props and hook returns | The ESLint rule @typescript-eslint/no-explicit-any is turned off in eslint.config.js, so type safety is a review rule, not a lint rule | pnpm exec tsc --noEmit on the project; read the diff |
GraphQL operations are named, unique repository-wide, and prefixed with the plugin or module (cmsPageList, salesDealCreate), colocated with the feature | Operation names are public API used by caching, logging, and codegen | Search the repo for the operation name before adding it |
Backend schemas follow their module's established new Schema(...) and schemaWrapper pattern; never change a backend API from a frontend-only change | Tenant models and shared schema behavior are load-bearing | Compare with a sibling module in the same plugin |
| Plugin isolation: a plugin change stays inside that plugin; no cross-plugin imports; no edits to core, shared libraries, or root infrastructure | Plugins must remain independently buildable, testable, deployable, and removable | git diff --stat shows only allowed directories |
No leftover console.log/debugger, commented-out code, unused imports/variables, or untracked TODOs | Dead code rots fast in a monorepo | pnpm nx lint <project> plus a final diff read |
Forbidden changes
- Introducing a new UI system or replacing existing patterns with personal preferences.
- Casually renaming public APIs, GraphQL operation names, routes, or Module Federation exposes.
- Moving Module Federation exposes without updating every host reference.
- Large refactors without an explicit request.
- Changing core, shared libraries, root infrastructure, or another plugin as an implicit part of plugin work; that is a separate, explicitly scoped task.
- New dependencies the change does not require, or ones an existing platform package already covers.
Frontend plugin rules
For work under frontend/plugins/<name>_ui:
| Area | Rule |
|---|---|
| Project shape | Route-level pages live in src/pages or src/modules following the touched plugin's pattern; feature internals stay near the feature in src/modules; src/widgets is only for Module Federation widget exports. Shared primitives go to frontend/libs/erxes-ui, shared product modules to frontend/libs/ui-modules; never to core-ui. |
| Imports | Use local imports or the plugin aliases (~/* for src, @/* for src/modules); no cross-plugin imports; move UI to a shared lib only for clear cross-plugin reuse. |
| UI | Match nearby layout, spacing, loading/empty states, filters, and action placement; reuse erxes-ui/ui-modules tables, forms, sheets, dialogs, and filters; icons come from @tabler/icons-react; preserve responsive behavior; no inline styles unless neighbors use them. |
| React | Functional components and existing hooks; Jotai only for shared sibling/page state or existing atom-backed patterns; keep hooks, constants, types, and GraphQL documents next to the feature. |
| GraphQL | Search existing documents first; prefix names with the plugin/module and keep them unique; reuse the plugin's fragments and Apollo hooks; do not change backend APIs from frontend work unless explicitly requested. |
| Module Federation | Check module-federation.config.ts before exposing modules; the named-export rule applies to every expose; never satisfy a host loader with a default export; fix the expose or the loader instead. |
Lint and formatting
- ESLint uses a flat config rooted at
eslint.config.js: the Nxflat/base,flat/typescript, andflat/javascriptpresets, plus@nx/enforce-module-boundariesset toerror. Its options areenforceBuildableLibDependency,allowCircularSelfDependency, and adepConstraintsentry allowing any source tag to depend on any tagged library. Frontend plugin configs (for examplesales_ui) spread the root config and addnx.configs['flat/react']. @typescript-eslint/no-explicit-anyisoff; the ban onanycomes from code review, not from lint. Do not read a green lint run as proof of type safety.- Prettier (
.prettierrc) enforces single quotes, trailing commas everywhere, andendOfLine: "auto". The repository conventions add two-space indentation, kebab-case config files, PascalCase components, camelCase functions, andUPPER_SNAKE_CASEglobal constants; match the touched project where it differs. - Run
pnpm nx lint <project>andpnpm nx build <project>before pushing; both must pass along withpnpm nx test <project>when the project defines tests. See Testing.