bestPracticesPlugin
The build-time code check twilightReact() already runs: six rules print colored findings in the terminal during vite build, never failing it.
import { bestPracticesPlugin, BestPracticesOptions } from '@salla.sa/twilight-theme-engine/vite';In plain words
When you run vite build (pnpm build), the engine reads each of your .ts and .tsx files and looks for patterns known to hurt a Salla theme, such as reloading the whole page or leaving out the places where apps add content. Each finding is printed in the terminal with the file, the line, what is wrong and what to do instead. The build still finishes.
You do not add this plugin: twilightReact() includes it. Import bestPracticesPlugin yourself only for a Vite project that does not use twilightReact().
Signature
function bestPracticesPlugin(options?: BestPracticesOptions): Plugin
// name 'twilight:best-practices', apply: 'build', enforce: 'pre'
interface BestPracticesOptions {
include?: string[]; // default ['**/*.ts', '**/*.tsx']
exclude?: string[]; // default ['**/node_modules/**', '**/dist/**', '**/*.d.ts', '**/.twilight/**']
disabledRules?: string[]; // default []
severityOverrides?: Record<string, 'error' | 'warn' | 'info'>; // default {}
browserConsole?: boolean; // default false
}
// Rule Severity Flags
// no-window-location-reload error window.location.reload(
// no-direct-product-card-import error an import from '../product/ProductCard'
// no-deep-component-import warn an import from '../common/Link' (11 folder names)
// no-manual-dom warn document.querySelector( and 3 more; .classList.add|remove|toggle(
// require-page-hooks warn a page file missing the hook slots of the engine's page
// no-img-without-dimensions info a one-line <img> without width and height (.jsx/.tsx)Example
// Written the way the build checks expect.
import { useRouter } from '@tanstack/react-router';
import { Link } from '@salla.sa/twilight-theme-engine/components/common';
export function EmptyCart() {
const router = useRouter();
return (
<div className="empty-cart">
{/* no-img-without-dimensions: width and height on the tag */}
<img src="/assets/empty-cart.svg" alt="" width={160} height={120} />
{/* no-window-location-reload: re-run the page loaders instead of reloading */}
<button type="button" className="btn btn--primary" onClick={() => router.invalidate()}>
Try again
</button>
{/* no-deep-component-import: a package subpath, not a file inside a component folder */}
<Link to="/">Continue shopping</Link>
</div>
);
}
How it behaves
It runs only in
vite build(apply: build), in thetransformhook, and skips a file whose content has not changed since it last checked it. Each finding prints throughconsole.erroras a block (icon, severity and rule;file:line; the message;→the fix), followed by a summary such as[twilight:best-practices] 1 error(s), 2 warning(s). Severity is only a label: anerrornever fails the build.no-window-location-reload(error): any line containingwindow.location.reload(. Fix:router.invalidate()to re-run the loaders, a newkeyto remount a component, or state.no-direct-product-card-import(error): a relative import ending in../product/ProductCard, which bypasses the registry that lets a theme swap the product card. Files with.test.or.spec.in the path, andProductCard.tsxitself, are skipped.no-deep-component-import(warn): a staticimportorexport … fromwhose path climbs with../into a folder namedcommon,cart,product,layout,home,modal,drawer,dropdown,collapse,toastornavigationand then goes deeper, stylesheet imports included. Fix: import the folder (../common). It matches folder names, not the engine, so your own../home/Herois reported too: give that folder anindex.ts. A../product/ProductCardline is reported by this rule and the one above.no-manual-dom(warn):document.querySelector(,querySelectorAll(,getElementById(,getElementsByClassName(, and.classList.add(,remove(ortoggle(on anything. Fix: a ref and state; for classes on<html>or<body>, useDocumentClass.require-page-hooks(warn): a file whose path looks like an engine page (containsCartPage,ProductPage,BlogPageornot-found, ends incart.tsx,product.tsxorblog.tsx, or is named likeProductListPage.tsx,BrandsPage.tsx,ThankYouPage.tsxor an account page) must render the same HookSlot names as the engine page, or register them withhookRegistry.register. It parses the file and the local files it imports. A file with a one-line import from an engine/routes/<page>module is skipped, which covers every generated route file.no-img-without-dimensions(info,.jsxand.tsxonly): an<img …>on one line whose attributes lackwidth=orwidth={, orheight=orheight={. Astylesize does not count. Or use the engine Image.Options:
includeandexcludeare simple globs tested against the path relative to Vite's root (**is anything,*is anything but/).disabledRulesandseverityOverridestake the rule names above; a misspelled name is ignored without a warning.browserConsole: trueappends aconsole.error,warnorinfocall per finding to the file, so the message also shows in the browser console.Standalone, in a Vite project without
twilightReact():plugins: [bestPracticesPlugin({ disabledRules: ['no-img-without-dimensions'], severityOverrides: { 'no-manual-dom': 'error' } })].
Gotchas
docs/22-best-practices-enforcement.mdsays the plugin runs invite dev(apply: serve) with real-time warnings, and lists four rules. The code runs it onvite buildonly, with six. Fix: run a build to see the report.Five rules read the file line by line with regular expressions, so a match inside a comment or a string counts: a commented-out
window.location.reload()is still reported as an error. Onlyrequire-page-hooksparses the code. The reverse also holds: a multi-line<img>,location.reload()withoutwindow., and a dynamicimport(...)are not caught.A glob containing
{,},(,),[,],?,+,|,^,$or a backslash never matches:include: ['**/*.{ts,tsx}']checks nothing, and the same pattern inexcludeexcludes nothing.**/also needs a folder, so**/*.tsskips files at the project root such asvite.config.ts. Fix: one plain pattern per extension.twilightReact()has no option to configure or remove its instance, and adding a configuredbestPracticesPlugin()next to it does not replace it. Both share one module-level cache of file hashes, so for each file whichever instance runs first reports and the other stays silent. Fix: put yours before...(await twilightReact()); files that rendersalla-*elements are still checked a second time with default options, becausetwilight:web-hydrationchanges them in between.require-page-hooksmatches unanchored path fragments:mini-cart.tsx, aCartPageSkeleton.tsxor anything under anot-foundfolder is expected to carry every hook slot of that page, one warning per missing slot. Fix: rename files that are not pages, or keep the slots.A fork of an engine page is skipped only while its import from
@salla.sa/twilight-theme-engine/routes/<page>fits on one line: the check (importsThemeEngineRoutes) is a single-line pattern. Once Prettier wraps that import, acustom-cart.tsxthat renders the engine's cart gets eight warnings, one per cart hook slot. Fix: import the module alone on one line (import { Cart } from '…/routes/cart') and its types in a separate statement.
Related
A shared ESLint config that warns on engine barrel imports, deep imports into component folders, window.location and document queries.
twilightReactThe one Vite plugin call in a theme's vite.config.ts: server rendering, a route for every storefront page, translations and build checks.
HookSlotA named empty place in the page that renders every handler registered under its name, plus a spot where Salla apps inject content.
registryThe shared name-to-component table the engine consults for a few swappable parts: the product card, the product gallery and home blocks.
ImageAn img that loads lazily by default and asks the Salla CDN for a resized copy, with optional srcset, aspect ratio and mobile source.