Skip to main content
Version: next

Code quality audit rules

Audit rules for the code health of a Front-Commerce project — tooling, dependencies, GraphQL conventions, error handling, and upgrade readiness.

Code quality findings rarely break a store today; they break the next upgrade, the next incident investigation, or the next developer's week. These rules check the signals that real audits use to estimate a project's maintenance cost: green tooling, a lean dependency tree, framework-aligned GraphQL code, and error handling that surfaces problems instead of hiding them.

AUDIT-CODE-01 — Keep lint and typecheck at zero errors and warnings

Severity: important — Detection: static

Rule: lint and TypeScript checking run without any error or warning on the project's main branch.

Why: A red or noisy baseline makes new problems invisible: nobody notices warning 51 appearing among 50 tolerated ones. It also blocks safe upgrades — you cannot tell framework-induced breakage from pre-existing debt.

How to check:

pnpm lint 2>&1 | tee lint-output.txt
npx tsc --noEmit 2>&1 | tee typecheck-output.txt

(Adapt to the project's package manager and scripts; check package.json for lint and typecheck scripts first.) Any error or warning is a violation. Attach both outputs to the audit report. Also flag suppression debt: count eslint-disable, @ts-ignore, and @ts-expect-error occurrences (grep -rn "eslint-disable\|@ts-ignore\|@ts-expect-error" app extensions --include="*.ts*" | wc -l) and report the trend.

AUDIT-CODE-02 — Remove unused and obsolete dependencies

Severity: minor — Detection: static

Rule: Every entry in package.json dependencies is imported by application code; server-only or build-only packages are not in dependencies when they belong in devDependencies.

Why: Unused dependencies (a real audit found 13, including express, lodash, and compression) enlarge the install and attack surface, slow CI, trigger pointless security-update work, and mislead developers about what the project actually uses.

How to check:

npx knip # or: npx depcheck

Cross-check each reported package manually before flagging (config-file-only usage, CLI usage in scripts, and Vite plugins produce false positives):

grep -rn "from ['\"]lodash" app extensions --include="*.ts*" | head

A package with zero import sites, zero scripts usage, and zero config references is a violation. Also flag duplicated utilities: lodash alongside native equivalents, multiple date libraries, multiple HTTP clients.

AUDIT-CODE-03 — Stay close to the current Front-Commerce version

Severity: important — Detection: static

Rule: The project runs a Front-Commerce version at most 2–3 minor releases behind the latest, or has a documented, estimated upgrade plan.

Why: Each skipped minor accumulates migration steps, deprecations, and security fixes. Past 2–3 minors, upgrades stop being routine chores and become projects of their own — and critical fixes can no longer be applied by a simple bump. Minor upgrades are designed to be cheap (pnpm update "@front-commerce/*@X.Y.Z" plus the migration guide).

How to check:

# Installed version
node -p "require('@front-commerce/core/package.json').version"

# Latest published version
npm view @front-commerce/core version

Compare minor versions. A gap greater than 3 minors with no written upgrade plan (ticket, estimate, target date) is a violation; 2–3 minors behind is a warning. Also verify all @front-commerce/* packages are on the same version:

grep -h '"@front-commerce/' package.json

Mixed versions across @front-commerce/* packages are always a violation.

AUDIT-CODE-04 — Follow the GraphQL schema conventions

Severity: minor — Detection: static

Rule: Custom mutations return a dedicated <Action>MutationSuccess type implementing MutationSuccessInterface, and the schema is designed as a graph: fields return objects (product: Product) and ID identifiers, not raw scalar ids (productId: Int).

Why: Generic success payloads and raw ids push loader logic into the client (extra round-trips to resolve the id) and break when id formats change. The MutationSuccess naming is what Front-Commerce codemods and tooling rely on during upgrades — deviating types are skipped by automated migrations.

How to check:

# Custom typeDefs
grep -rn "typeDefs" extensions/ app/ --include="*.ts" -l

# Mutations not following the convention
grep -rn "extend type Mutation" extensions/ -A 10 --include="*.ts" \
| grep -E "\): (Boolean|String|Int|ID|MutationSuccess)\b"

# Raw id fields in the schema
grep -rnE "[a-z]Id(s)?: (Int|String)!?" extensions/ --include="*.ts"

Violations: a custom mutation returning Boolean, a bare scalar, or the shared generic MutationSuccess when it mutates a domain entity (return <Action>MutationSuccess implements MutationSuccessInterface exposing the mutated entity); a field named xxxId typed Int/String where the entity type exists in the schema.

AUDIT-CODE-05 — Structure GraphQL modules as definition plus lazy runtime

Severity: important — Detection: static

Rule: Every custom GraphQL module keeps its index.ts synchronous and light (namespace, dependencies, typeDefs, loadRuntime) and loads resolvers and contextEnhancer lazily through loadRuntime: () => import("./runtime").

Why: The module definition is evaluated at build time in contexts that must not pull server dependencies (schema generation, codegen, client bundles). Inlining resolvers in index.ts drags loaders, HTTP clients, and their transitive dependencies into those contexts, slowing startup and — in the worst case — breaking the build when a server-only import reaches a client bundle.

How to check:

# Modules declaring resolvers or contextEnhancer in the definition
grep -rn "createGraphQLModule" extensions/ app/ --include="*.ts" -l \
| xargs grep -ln "resolvers:\|contextEnhancer:"

# Definitions importing runtime code statically
grep -rn "createGraphQLModule" extensions/ --include="*.ts" -l \
| xargs grep -n "^import .*loader\|^import axios\|^import .*client" -i

A createGraphQLModule call containing resolvers or contextEnhancer directly (instead of loadRuntime: () => import("./runtime")) is a violation. So is an index.ts module definition with static value imports of loaders, HTTP clients, or SDKs.

AUDIT-CODE-06 — Build cross-request services once, not per request

Severity: important — Detection: static

Rule: Services that are not tied to a single request — HTTP clients, SDK instances, registries, providers — are constructed once and registered through dependency injection in the extension's onServerServicesInit, then read from services.DI.get(...) in contextEnhancer. The contextEnhancer only forwards or builds per-request objects (loaders bound to the current user).

Why: Instantiating a client in contextEnhancer rebuilds it on every GraphQL request: connection pools, caches, and warmup are thrown away each time, degrading latency under load. The inverse mistake is worse: mutating a process-wide singleton with per-request state (an authenticated client, a user token) leaks one user's state into another's concurrent request.

How to check:

# Instantiation inside contextEnhancer bodies
grep -rn "contextEnhancer" extensions/ --include="*.ts" -A 15 \
| grep -nE "new [A-Z][A-Za-z]*(Client|Registry|Provider|Sdk|Store|Api)"

# DI usage (the expected pattern)
grep -rn "onServerServicesInit\|services.DI.register\|services.DI.get" extensions/ --include="*.ts"

A new <Client|Registry|Provider> inside contextEnhancer whose constructor takes only configuration (no request/user data) is a violation: move it to a DI factory registered in onServerServicesInit. Conversely, flag any DI-held singleton whose methods are called with per-request credentials to mutate its internal state (setAuthenticatedClient(...)-style APIs) — per-request state belongs in per-request loaders.

AUDIT-CODE-07 — Handle errors loudly with typed domain errors

Severity: important — Detection: static

Rule: Custom code never swallows an error silently (a catch returning []/null/undefined without logging), types caught errors as unknown narrowed with instanceof, and throws named domain error classes instead of generic new Error("...") for recurring conditions.

Why: A silent catch turns an API outage into "the page shows an empty list and nothing else in production" — undiagnosable without redeploying. catch (error: any) plus error.message crashes when a non-Error value is thrown. Generic errors force every caller to parse message strings instead of instanceof-narrowing, and rewrapping without { cause } destroys the stack trace that production debugging needs.

How to check:

# Silent catches
grep -rn "catch" extensions/ app/ --include="*.ts*" -A 3 \
| grep -B 2 "return \[\]\|return null\|return undefined\|^\s*}\s*$"

# any-typed catches
grep -rnE "catch \((e|err|error): any\)" extensions/ app/ --include="*.ts*"

# Generic errors in loaders/services
grep -rnE "throw new Error\(" extensions/ --include="*.ts" | grep -v spec

Violations: a catch block with neither a log call (logger.) nor a throw; catch (error: any); error.message accessed without an instanceof Error guard; repeated throw new Error("X not found") where a XNotFoundError class should exist; rewrapping without { cause: error }. A debug(...) call alone does not count as logging — debug flags are off in production.

AUDIT-CODE-08 — Remove deprecated APIs and 2.x leftovers

Severity: important — Detection: static

Rule: The project uses no deprecated Front-Commerce API and carries no 2.x compatibility code, unless an in-progress, documented migration explains it.

Why: Deprecated APIs are removed at the next major: every remaining call site is a guaranteed upgrade blocker, discovered under time pressure. 2.x idioms (withProps HOCs, config/ module overrides, v2 import paths) also mislead new developers into extending the project the wrong way.

How to check:

# Deprecation warnings at build/boot (also see runtime logs)
pnpm build 2>&1 | grep -i "deprecat"

# Known v2 leftovers
grep -rn "front-commerce/src\|web/theme\|withProps\|makeCommandDispatcher" \
app extensions --include="*.ts*" --include="*.js*"

# Project usage of symbols the installed packages mark @deprecated
grep -rln "@deprecated" node_modules/@front-commerce/*/dist 2>/dev/null \
| head # then grep the project for the deprecated symbols found

Cross-reference findings with the migration guides for the versions between the project's version and the latest. Any deprecated call site without a linked migration ticket is a violation. Projects still migrating from 2.x are covered by the dedicated AUDIT-MIG-* rules instead.

AUDIT-CODE-09 — Read configuration through Front-Commerce, not process.env

Severity: important — Detection: static

Rule: Application code reads configuration through Front-Commerce's configuration system (configuration providers, config in GraphQL context, usePublicConfig client-side), not through direct process.env access. Client-exposed variables use the FRONT_COMMERCE_WEB_* prefix exclusively.

Why: Direct process.env reads bypass validation, defaults, and per-store scoping, and fail silently when a variable is missing (an undefined propagating deep into a request). Worse, a server variable interpolated into client code leaks secrets into the public bundle — only FRONT_COMMERCE_WEB_* variables are designed to be shipped to the browser.

How to check:

# Direct env access in application code
grep -rn "process.env\|import.meta.env" app extensions --include="*.ts*" \
| grep -v "FRONT_COMMERCE_WEB_" | grep -v ".spec."

# Server env vars reaching client-side files (theme components, entry.client)
grep -rn "process.env" app/theme app/entry.client.tsx --include="*.ts*"

Each process.env read outside a configuration provider definition and outside build tooling (vite.config, scripts) is a violation: move it into a configuration provider with a schema and defaults. Any non-FRONT_COMMERCE_WEB_ variable referenced from client-side code is a critical finding — check whether a secret already shipped in the public bundle (grep -rn "API_KEY\|SECRET\|TOKEN" .front-commerce/dist/client 2>/dev/null).

AUDIT-CODE-10 — Test custom business logic

Severity: important — Detection: static

Rule: Custom loaders, mappers, and other business logic in extensions/ have automated tests (*.spec.ts colocated with the source).

Why: Loaders and mappers encode the project's actual business rules (pricing, mapping backend payloads, eligibility) and are exactly the code that breaks on backend API changes and Front-Commerce upgrades. Front-Commerce loaders are designed as plain modules to be testable in isolation — audited projects that skip this routinely ship regressions that a 10-line spec would have caught.

How to check:

# Inventory business logic vs. specs
find extensions -name "*.ts" | grep -iE "loader|mapper|adapter|service" | grep -v spec
find extensions app -name "*.spec.ts" -o -name "*.spec.tsx" | wc -l

# Does a test script even exist and pass?
grep -n '"test"' package.json && pnpm test -- --run

Zero spec files in a project with custom loaders is a violation. Otherwise, list each loader/mapper/adapter without a sibling spec and report the ratio. Prioritize by risk: money-related mappers and authentication-adjacent loaders untested are individual findings; a missing spec on a trivial pass-through is not.

AUDIT-CODE-11 — Keep debug routes and one-shot scripts out of the repository

Severity: minor — Detection: static

Rule: The repository contains no committed debug routes, test endpoints, commented-out experiments, or one-shot scripts outside a dedicated, documented scripts/ folder.

Why: A forgotten app/routes/test.tsx or api.debug.tsx is a public endpoint in production — at best noise for crawlers, at worst an information leak or an unguarded mutation. Dead experiment files mislead maintenance work and inflate the upgrade surface for free.

How to check:

# Suspicious route names
ls app/routes | grep -iE "test|debug|tmp|poc|demo|old|copy|bak"

# One-shot scripts and dead files elsewhere
git ls-files | grep -iE "\.(bak|old|orig)$|/(tmp|poc|sandbox)/"

# Leftover console debugging in app code
grep -rn "console.log\|console.debug" app extensions --include="*.ts*" | grep -v spec

Each match is a violation unless the file is intentionally shipped (a documented demo route behind an environment guard, for example). For any debug route found, also check whether it performs mutations or exposes internal data — if so, escalate it to the security section of the report.