Code Quality Standards
This guide covers code quality standards, testing practices, and technical requirements for Waldur HomePort.
Technical Standards
Architecture Principles
- Composition over inheritance - Use dependency injection
- Interfaces over singletons - Enable testing and flexibility
- Explicit over implicit - Clear data flow and dependencies
- Test-driven when possible - Never disable tests, fix them
Code Quality Requirements
- Every commit must:
- Compile successfully
- Pass all existing tests
- Include tests for new functionality
-
Follow project formatting/linting
-
Before committing:
- Run formatters/linters
- Self-review changes
- Ensure commit message explains "why"
Error Handling
- Fail fast with descriptive messages
- Include context for debugging
- Handle errors at appropriate level
- Never silently swallow exceptions
Development Guidelines
TypeScript Configuration
- Uses
@/*path mapping for internal imports - Strict TypeScript checking disabled for legacy compatibility
- Module resolution set to "Bundler" for Vite compatibility
Code Style
- ESLint with flat config format enforced with TypeScript, React, and accessibility rules
- Prettier for code formatting (2 spaces, semicolons, single quotes)
- Import ordering enforced with
@waldurimports grouped separately - SCSS/CSS linting with Stylelint
- Husky for git hooks and pre-commit checks
Check Code Style Tool Versions
1 2 3 | |
TypeScript and SDK Types
- Always prefer SDK types over custom types from
waldur-js-clientpackage - Import types using
typekeyword:import { type ComponentUsageCreateRequest } from 'waldur-js-client' - Common SDK types to use instead of custom interfaces:
ResourcePlanPeriod- for plan periods with componentsBaseComponentUsage- for component usage data in periodsComponentUsageCreateRequest- for usage submission request bodiesComponentUserUsageCreateRequest- for user usage submission request bodiesComponentUsage- for general component usage data- All marketplace API request/response types are available in the SDK
- When using React Final Form, use standard pattern:
<Field component={NumberField} /> - Convert between SDK string types and numbers when necessary (e.g.,
parseFloat(component.usage)) - Handle nullable SDK types properly with optional chaining (
period.value?.components)
Tooling
Essential Commands
Code Quality
yarn lint:check- Run ESLint checksyarn lint:fix- Fix ESLint issues automaticallyyarn format:check- Check code formatting with Prettieryarn format:fix- Auto-format code with Prettieryarn style:check- Check SCSS/CSS styles with Stylelintyarn colors:check/yarn rem:check- Ratchets: hard-coded colours and rawremlengths per file may only go down (yarn colors:save/yarn rem:saveafter removing some)yarn deps:unused- Check for unused dependencies with Knipyarn tsgo- Typescript type check
Dependency Management
yarn deps:unused- Find unused dependencies and exports with Knipyarn deps:circular- Check for import cycles with oxlintimport/no-cycle(config:.oxlintrc.cycles.json;@/imports are resolved). Every static import counts, includingimport type/export typeand a plain import of a type; only a dynamicimport()does not, since lazy pages and dialogs are split off that way. oxlint reports each import that closes a cycle rather than the whole chain: follow the reported import lines from file to file to see it.
Tooling Standards
- Use project's existing build system
- Use project's test framework
- Use project's formatter/linter settings
- Don't introduce new tools without strong justification
Quality Assurance
Code Quality & Analysis
- Knip for unused dependency detection
- oxlint
import/no-cyclefor circular dependency analysis - Lint-staged for pre-commit code formatting
- CSS: SCSS compiled by Sass, Tailwind CSS v4 through
@tailwindcss/vite(which adds vendor prefixes itself), and Vite's minification in production builds. There is no PostCSS configuration.
Modern Development Practices
- ESM (ES Modules) throughout the codebase
- TypeScript with comprehensive typing
- Flat ESLint config format
- Husky git hooks for automated quality checks
- Yarn package management with lockfile integrity
UI-Router state name guard
src/state-names.test.ts is a Vitest smoke test that asserts every literal UI-Router state name referenced from source (<Link state="...">, <UISref state="...">, router.stateService.go('...'), and breadcrumb to: fields) resolves to a state registered in src/states.ts. It catches the regression class where a route is renamed in a routes.ts module but call sites elsewhere keep the old name and silently 404.
Dynamic references (state={variable}, router.stateService.go(name), computed names) are skipped — the literal-string regexes do not match expression syntax.
Opt-outs
- Per-line: append
// state-check: ignoreto the line containing the literal. Use this for fixture data or constants where the literal value is intentionally not a real state name. - Global allowlist (in the test file): for values that are intentionally not in the registry but legitimately appear in the codebase (e.g., the
'404'fallback used insideuseSref(state || '404', ...)insrc/core/Link.tsx). Keep this set small and add a comment explaining each entry. - Known-broken set (in the test file): for pre-existing broken references that require product knowledge to fix. Do not add new entries — fix the reference instead. When the last call site for an entry is gone, remove the entry.
Typed property guard
waldur-custom/prefer-typed-property reports a PropertyItem that formats its value by hand where a typed formatter fits (propVal.date, propVal.currency, propVal.boolean, …), and renderFieldOrDash inside one. Existing rows are counted per file and kind in typed-property.baseline.json; the counts only shrink, so rewrite them with yarn lint:properties:baseline after moving rows, never to let new ones in. See property-fields.md.
Hand-rolled table guard
waldur-custom/no-hand-rolled-table reports raw <table> markup, which should be @/table/Table or SimpleTable (see table/migration-guide.md). Existing tables are counted per file in hand-rolled-table.baseline.json; the counts only shrink, so rewrite them with yarn lint:tables:baseline after migrating some, never to let new ones in. A table that is deliberately not a data table (a <td> under test, a story's layout grid) takes // eslint-disable-next-line waldur-custom/no-hand-rolled-table -- <reason> and is not counted.
rem length guard
waldur-custom/no-rem-length (ESLint: arbitrary classes and inline styles in TS/TSX) and waldur/no-rem-length (stylelint) report a raw rem length once a file has more than its count in scripts/rem-literals.baseline.json, naming the px it is at HomePort's 13px root and the spacing step or --text-* token that equals it. yarn rem:check runs the same detector over the repository in CI and fails only when a file gains one; after removing some, lower the counts with yarn rem:save in the same merge request. See "Root font size" in design-tokens.md.
Unknown class guard
Tailwind and Bootstrap both ignore a class they do not know, so a typo (felx, text-teritary, md:hiden) or a class whose stylesheet was retired renders as nothing, with no warning from the build, the type checker or the browser. waldur-custom/no-unknown-class reports every class that styles nothing.
A class is known when either:
- Tailwind generates CSS for it. The rule asks the same Tailwind design system the build loads from
src/tailwind.css, so variants, arbitrary values,!, opacity modifiers,@themetokens and@utilitydefinitions are judged exactly as the build judges them. There is no hand-kept list of utilities. - A stylesheet defines it. Every
.scss(compiled with Sass) and.cssfile undersrc/,packages/andapps/, plus thenode_modulesstylesheets the source imports, contributes its class selectors. A class counts as defined if it appears in any selector, including one that only applies inside a parent (.team-chat .small). - An arbitrary variant styles it. A class selected inside a Tailwind arbitrary variant in any source file (
[&_.x]:,has-[.x]:,group-[.x]:) counts as defined, since that variant styles it. These are read from the source on every recheck, not cached; the lint plugin's own files are skipped.
The rule reads class positions only:
classNameand any*ClassNameattribute or property;- the arguments of
cn/clsx/classNames/cx/twMerge; cvabases and variant values;- variables named
*class,*classesor*className, in any case (activeClass,BASE_CLASSNAME).
A const used in one of those positions is followed to its value in the same file: className={BASE} checks BASE, cn(SIZES[size]) checks every value of SIZES, TONE.danger checks that entry. Imports, parameters and let are not followed. A token glued to a ${…} in a template literal is a fragment and is skipped. The message names the nearest real class when there is one (Did you mean "flex"?).
Implementation: packages/eslint-plugin-waldur/lib/class-oracle.js, reached from the synchronous rule through a synckit worker. The rule finds the repository root from its own location, so ESLint can run from any directory. Compiled stylesheet classes are cached in node_modules/.cache/eslint-plugin-waldur/, keyed by every stylesheet's path, size and mtime and by yarn.lock; a long-lived ESLint server (the editor's) picks up stylesheet changes within a few seconds.
Existing violations
Existing violations are listed per file in unknown-classes.baseline.json. A listed class is not reported in its file; any other unknown class is, whatever else the change does. A listed class the file no longer has (fixed, removed, or now defined by a stylesheet) is reported as stale, so the list only shrinks.
- After fixing listed classes, rewrite the list:
yarn lint:classes:baseline. - Never add entries for new code. Fix the class instead.
Classes that deliberately style nothing
A class used only as a hook (for a test or a third-party script) should be a data-* attribute instead (data-slot, data-testid); class-names-and-data-attributes.md says which one. Where it cannot be, list a pattern in the rule's allow option in eslint.config.js, so each exception is a visible decision. UNKNOWN_CLASS_HOOKS there lists the classes the end-to-end suite in waldur-integration-testing selects on, now only react-select's metronic-select__*; remove one only together with its selector there. Test files (*.test.ts(x)) are exempt because they pass made-up classes to check that a prop is forwarded.