numu-theme lint runs 10 static-analysis rules. Each rule is self-contained at numu-theme-cli/src/lint/rules/<id>.ts and exports { id, description, check(ctx) }.manifest-required-fields#
Catches: missing or malformed identity fields in theme.json.Required: id (kebab-case ^[a-z0-9-]+$), name (bilingual object with at least en), version (semver), author (non-empty string).Failures abort marketplace submission.
{ "id": "My Theme", "name": "My Theme" }
{ "id": "my-theme", "name": { "en": "My Theme" }, "version": "1.0.0", "author": "me@example.com" }
schema-registry-sync#
Catches: preset references to sections without a matching schema (and vice versa).If theme.json preset uses section type hero but schemas/sections/hero.json doesn't exist, the customizer 500s at runtime ā the merchant can't add or edit that section. Same in reverse: a schema file with no theme code to render it is dead weight.ā theme.json references "hero" preset, but schemas/sections/hero.json is missing.
ā schemas/sections/banner.json declared, but no preset uses "banner".
locale-parity#
Catches: keys present in locales/en.default.json but missing in locales/ar.json (or any other locale).
{ "cart": { "title": "Cart", "checkout": "Checkout" } }
{ "cart": { "title": "Ų§ŁŲ³ŁŲ©" } }
ā ā locales/ar.json: missing key "cart.checkout"The translation fallback would render the English key in an Arabic-locale store, which is bad UX. Backfill every key explicitly.preset-schema-conformance#
Catches: preset settings that don't conform to the section's schema.If schemas/sections/hero.json declares only headline and alignment, but the preset in theme.json sets background_image, the customizer will silently drop it on first save:ā theme.json: preset "home.hero_1" sets "background_image"
which is not declared in schemas/sections/hero.json
Also catches type mismatches (e.g. setting alignment: "diagonal" when the schema declares only left|center|right).unused-settings#
Catches: declared settings_schema.json entries no theme code reads.Heuristic: grep the source for settings.<id> and setting?.id === "<id>". If neither pattern matches, the setting is unreferenced.ā settings_schema.json: "footer_padding_top" is declared but never read in src/
Warning by default (not an error), since false positives are possible (e.g. settings read indirectly via Object.keys(settings)). Add // numu-lint-ignore unused-settings to suppress per declaration.img-missing-alt#
Catches: raw <img> tags without an alt attribute (including alt="" is fine ā that's "decorative image").// ā
<img src="/hero.jpg" />
// ā
<img src="/hero.jpg" alt="Summer collection hero" />
// ā
ā alt="" means "decorative, screen readers skip"
<img src="/divider.svg" alt="" />
Doesn't flag the SDK's <Image> component (which requires alt at the type level).hardcoded-text#
Catches: 3+-word JSX text nodes that should probably go through t().// ā
<h1>Shop our summer sale today</h1>
// ā
<h1>{t("home.summer_sale_heading")}</h1>
Heuristic: counts whitespace-separated words in JSXText nodes. 2 or fewer is fine (button labels like "Cart", "Buy now" are usually OK; longer copy isn't).Warning by default. Themes targeting a single market may want to silence this rule via --rules exclusion.inline-color-literal#
Catches: hex or rgb() literals in JSX style props.// ā
<div style={{ color: "#FF0000", background: "rgb(15, 23, 42)" }} />
// ā
<div style={{ color: settings.color_text, background: settings.color_bg }} />
// ā
ā CSS variable from a theme color setting
<div style={{ color: "var(--color-text)" }} />
The whole point of the customizer is letting merchants change colors. Hard-coded colors defeat that.forbidden-script-tag#
Catches: <script> tags in theme source components.// ā
<script src="https://cdn.example.com/analytics.js" />
<script dangerouslySetInnerHTML={{ __html: "alert(1)" }} />
Forbidden at the AST level. Any inline script in a theme would bypass our CSP + admin review process. If you need third-party analytics, use useAnalytics() from the SDK ā merchants configure pixels per store, not per theme.use-app-no-availability-check#
Catches: useApp(slug) calls whose return value isn't branched on .available.// ā
const app = useApp("reviews");
return <div>{app.data.average_rating}</div>; // crashes if app not installed
// ā
const app = useApp("reviews");
if (!app.available) return null;
return <div>{app.data?.average_rating}</div>;
Phase 9 apps platform is optional ā themes that integrate with apps must degrade gracefully when the app isn't installed on the merchant's store.Running selectively#
The --rules flag takes a comma-separated id list. Listing none of the warning rules turns them all into a quiet shell ā useful when you're prototyping and not ready to translate yet.Suppressing per-line#
// numu-lint-ignore hardcoded-text
<h1>Internal admin tools</h1>
The comment must appear on the line before the offending construct. Use sparingly ā --rules exclusion is usually cleaner.1.
Create numu-theme-cli/src/lint/rules/<id>.ts:import type { Rule } from "../runner";
export const rule: Rule = {
id: "my-new-rule",
description: "What this catches",
check(ctx) {
const issues = [];
// walk ctx.sources / ctx.manifest / ctx.sectionSchemas / ctx.locales
// push { severity: "error" | "warn", message, file?, line? }
return issues;
},
};
2.
Register it in runner.ts's RULES array.
4.
Add a test (currently informal ā drop a test fixture under tests/).
Modified atĀ 2026-09-19 15:52:50