Theme customization audit rules
Audit rules for the theme layer of a Front-Commerce project — override surface, extension points, error boundaries, and design system usage.
Theme customization is the most frequent source of findings in real
Front-Commerce audits. An oversized or incorrectly wired override surface is
invisible day to day, but it turns every version upgrade into a manual merge and
silently disables extension points. These rules check that your app/theme
folder (and any extension theme folder) stays minimal, correctly structured,
and resilient.
AUDIT-THEME-01 — Minimize the override surface
Severity: important — Detection: static
Rule: No file in app/theme (or an extension theme folder) is an
identical or near-identical copy of its original in the reference theme.
Why: Every overridden file is frozen at the version it was copied from: it stops receiving upstream bug fixes, accessibility improvements, and security patches, and it must be manually re-diffed on every Front-Commerce upgrade. Real audits regularly find 10+ files overridden in pure duplication — upgrade debt with zero functional value. Deleting an identical copy is a free win.
How to check: This check needs the installed packages — run pnpm install
(or the project's package manager) first if node_modules is absent. Then diff
every overridden file against its original in
node_modules/@front-commerce/theme-chocolatine/:
cd app/theme
find . -type f | while read -r f; do
orig=$(find ../../node_modules/@front-commerce/theme-chocolatine \
-path "*/theme/${f#./}" 2>/dev/null | head -1)
if [ -n "$orig" ]; then
if diff -q "$f" "$orig" >/dev/null 2>&1; then
echo "IDENTICAL: $f"
else
echo "CHANGED ($(diff "$f" "$orig" | grep -c '^[<>]') lines): $f"
fi
else
echo "NEW: $f"
fi
done | sort
Any IDENTICAL file is a violation: delete it. Files reported as CHANGED with
fewer than ~5 changed lines are near-duplicates: review whether the change can
be achieved by wrapping the original component (import it from
@front-commerce/theme/...) or through an existing extension point instead.
Report the total override count and the identical/near-identical count in the
audit.
AUDIT-THEME-02 — Extend core logic instead of replacing it
Severity: important — Detection: manual
Rule: No core loader, resolver map, or GraphQL module is copied and overridden in full when an additional GraphQL module with a targeted resolver (or a loader decorating the original) achieves the same result.
Why: A wholesale copy of a core loader or resolver silently drops internal
behavior the rest of the framework relies on. Real audit case: a fully
overridden CartLoader lost the __originalData bookkeeping, which broke
dynamic routes. A full copy also freezes hundreds of lines of core logic at one
version, compounding the upgrade debt of AUDIT-THEME-01 at the data layer, where
regressions are hardest to notice.
How to check:
-
List custom GraphQL modules and their intent:
grep -rn "createGraphQLModule\|createGraphQLRuntime" extensions/ app/ --include="*.ts" -
In each
runtime.ts, inspectcontextEnhancer: a returned loader whose name shadows a core loader (Cart,Product,Customer, ...) and whose implementation is a copied class rather than a wrapper aroundloaders.<Name>is a violation. -
Search for copied core sources: pick distinctive lines from suspect files and grep for them in
node_modules/@front-commerce/*/. A file that is mostly identical to a core loader/resolver is a violation. -
For each violation, verify the lighter alternative was possible: a module declaring the owning core module in
dependenciesand redefining only the target fields inresolvers(see the "Change a resolver behavior" guide), or a loader that delegates to the original instance.
AUDIT-THEME-03 — Import theme files through the theme/ alias
Severity: important — Detection: static
Rule: Files under a theme folder import other theme files through the
theme/ alias, never through relative paths that cross component directories
(../OtherComponent/...).
Why: The override mechanism resolves the theme/ alias against a chain that
puts app/theme ahead of every package's theme. A relative import bypasses that
chain and resolves directly on disk, so any override of the imported file is
silently ignored. The breach is invisible in the importing project and typically
surfaces later as "my override doesn't apply in some places".
How to check:
# Relative imports climbing out of the current component directory
grep -rn --include="*.tsx" --include="*.ts" --include="*.jsx" \
-E "from ['\"]\.\./" app/theme extensions/*/theme
Each match that resolves to another component, hook, or stylesheet under a
theme tree is a violation: rewrite it as theme/<path from theme root>.
Same-directory imports of a component's own private files (./Foo.scss) are
acceptable in an integrator project; imports reaching a sibling component
directory are not. Also check .scss files for relative @use/@import of
another component's stylesheet.
AUDIT-THEME-04 — Keep overrides namespaced and one component per directory
Severity: minor — Detection: static
Rule: Custom theme code lives in feature-namespaced PascalCase directories
(theme/modules/StoreLocator/), with one component per file and its .scss,
.gql, stories, and tests colocated in the component's own subdirectory.
Why: Files dropped at the root of theme/components/ or theme/modules/
collide with files from other extensions overriding the same path. Multiple
components packed into one file cannot be overridden (or reused) individually,
and scattered .scss/.gql siblings make it impossible to tell which files
belong to which component when upgrading.
How to check:
-
Flag loose files at shared roots:
find app/theme/components app/theme/modules -maxdepth 1 -type f -
Flag directories mixing several components with their assets side by side:
# More than one .tsx/.jsx per directory alongside .scss/.stories filesfind app/theme -type d | while read -r d; don=$(find "$d" -maxdepth 1 \( -name "*.tsx" -o -name "*.jsx" \) | wc -l)[ "$n" -gt 1 ] && echo "$n components: $d"done -
Flag files exporting more than one React component (
grep -c "^export const [A-Z]\|^export function [A-Z]"per file). -
Flag kebab-case or lowercase module directories under
theme/modules/.
AUDIT-THEME-05 — Export an ErrorBoundary from the root and every overridden layout
Severity: important — Detection: static
Rule: app/root.tsx exports Front-Commerce's RootErrorBoundary (or a
project-specific equivalent), and every overridden layout route re-exports an
ErrorBoundary that preserves the layout (typically wrapping
LayoutErrorBoundary).
Why: Without a root boundary, any unhandled error renders the framework's
bare fallback instead of a branded, internationalized error page. When an
overridden layout (for example _main.tsx) drops the ErrorBoundary export,
errors in any nested route escape to the root boundary and the user loses the
header, footer, and navigation — a real audit finding on an overridden
_main.tsx.
How to check:
# Root boundary present?
grep -n "RootErrorBoundary\|export const ErrorBoundary\|export function ErrorBoundary" app/root.tsx
# Every overridden layout re-exports one
for f in app/routes/_*.tsx; do
grep -L "ErrorBoundary" "$f"
done
A layout route file (an app/routes/ file that renders Outlet and wraps it in
a layout) without an ErrorBoundary export is a violation. Also verify that
custom error pages use internationalized titles rather than hardcoded strings
(grep -rn "appErrorPages" app/theme).
AUDIT-THEME-06 — Keep the PWA manifest valid and branded
Severity: minor — Detection: static
Rule: The PWA configuration (app/config/pwa.ts or the pwa key of
front-commerce.config.ts) declares icons that exist on disk, and its
appName, themeColor, and description reflect the brand instead of the
skeleton defaults.
Why: A manifest icon pointing to a missing file (real audit case:
icon: "assets/icon.png" with no such file) produces a broken install prompt
and a 404 fetched on every page load. Skeleton defaults ("Front-Commerce",
#fbb03b) shipped to production look unfinished on the user's home screen.
How to check:
-
Locate the config:
grep -rn "pwa" front-commerce.config.ts app/config/. -
For each
icon,maskableIcon, and offline fallback path, verify the file exists (paths resolve from the project root, for examplepublic/favicon.svg):node -e "const c = require('./app/config/pwa'); /* or read the file */"ls -l public/favicon.svg public/images/Logo.svg # adapt to declared paths -
Flag skeleton leftovers:
grep -n "Front-Commerce\|fbb03b\|My e-commerce application" app/config/pwa.ts -
Verify icons are at least 512x512 (or SVG), as required for install prompts.
AUDIT-THEME-07 — Reuse design system components instead of recoding them
Severity: minor — Detection: static
Rule: Custom components reuse the theme's design system —
theme/components/atoms/Icon for icons, PriceVariant for prices, existing
form atoms (Input, Select, Checkbox, Button, ...) for controls — instead
of inline SVG, hand-rolled price formatting, or raw form elements.
Why: The theme atoms are internationalization-aware, accessible (labels, focus, error rendering), and consistent with the rest of the store. Hand-rolled duplicates drift visually, lose tax/currency formatting rules for prices, and duplicate accessibility work. Inline SVG bypasses the icon registry, so icons can no longer be swapped centrally.
How to check:
# Inline SVG in custom components
grep -rn "<svg" app/theme extensions/*/theme --include="*.tsx" --include="*.jsx"
# Hand-rolled price display (rendering amounts without PriceVariant/Price)
grep -rn "toFixed(2)\|Intl.NumberFormat" app/theme --include="*.tsx"
# Raw form controls where an atom exists
grep -rn "<input\|<select\|<textarea" app/theme --include="*.tsx" --include="*.jsx"
Each match is a candidate violation; confirm no theme atom covers the need
(ls node_modules/@front-commerce/theme-chocolatine/*/theme/components/atoms)
before reporting. A raw control inside an overridden copy of a reference-theme
file that already used a raw control is not a finding.
AUDIT-THEME-08 — Colocate GraphQL fragments and type components from generated types
Severity: important — Detection: static
Rule: Each theme component that consumes GraphQL data owns a colocated
<ComponentName>Fragment.gql containing only the fields it renders, and its
props are typed with the generated types from ~/graphql/graphql — never a
hand-written TypeScript shape duplicating a GraphQL type.
Why: The fragment is the component's data contract: when it lives elsewhere (or fields are stuffed into a page-level query), overriding the component no longer controls what is fetched, and unused fields accumulate in every response. Hand-written prop types silently drift from the schema; codegen types fail the build instead when the schema changes.
How to check:
# Components rendering GraphQL data without a sibling fragment
find app/theme -name "*Fragment.gql" | sed 's/Fragment.gql//' > /tmp/frags
grep -rln "Fragment" app/theme --include="*.tsx" | while read -r f; do
ls "${f%.tsx}Fragment.gql" >/dev/null 2>&1 || echo "no colocated fragment: $f"
done
# Hand-written GraphQL shapes instead of generated types
grep -rn "interface.*Props" app/theme --include="*.tsx" -A 5 | grep -E ": \{ (id|sku|name|price)"
grep -rLn "~/graphql/graphql" $(grep -rl "Fragment.gql" app/theme --include="*.tsx" 2>/dev/null)
Violations: a fragment declared in a shared/pages-level file while a single
component consumes it; a fragment with fields no component in its subtree
renders; a component with a colocated fragment whose props are typed manually
instead of with <Name>FragmentFragment from ~/graphql/graphql.