Skip to content
Twilight React Playground
ثيم رائدaren

bestPracticesPlugin

pluginBeginner

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

app/components/EmptyCart.tsx
// 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 the transform hook, and skips a file whose content has not changed since it last checked it. Each finding prints through console.error as 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: an error never fails the build.

  • no-window-location-reload (error): any line containing window.location.reload(. Fix: router.invalidate() to re-run the loaders, a new key to 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, and ProductCard.tsx itself, are skipped.

  • no-deep-component-import (warn): a static import or export … from whose path climbs with ../ into a folder named common, cart, product, layout, home, modal, drawer, dropdown, collapse, toast or navigation and then goes deeper, stylesheet imports included. Fix: import the folder (../common). It matches folder names, not the engine, so your own ../home/Hero is reported too: give that folder an index.ts. A ../product/ProductCard line is reported by this rule and the one above.

  • no-manual-dom (warn): document.querySelector(, querySelectorAll(, getElementById(, getElementsByClassName(, and .classList.add(, remove( or toggle( 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 (contains CartPage, ProductPage, BlogPage or not-found, ends in cart.tsx, product.tsx or blog.tsx, or is named like ProductListPage.tsx, BrandsPage.tsx, ThankYouPage.tsx or an account page) must render the same HookSlot names as the engine page, or register them with hookRegistry.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, .jsx and .tsx only): an <img …> on one line whose attributes lack width= or width={, or height= or height={. A style size does not count. Or use the engine Image.

  • Options: include and exclude are simple globs tested against the path relative to Vite's root (** is anything, * is anything but /). disabledRules and severityOverrides take the rule names above; a misspelled name is ignored without a warning. browserConsole: true appends a console.error, warn or info call 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.md says the plugin runs in vite dev (apply: serve) with real-time warnings, and lists four rules. The code runs it on vite build only, 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. Only require-page-hooks parses the code. The reverse also holds: a multi-line <img>, location.reload() without window., and a dynamic import(...) are not caught.

  • A glob containing {, }, (, ), [, ], ?, +, |, ^, $ or a backslash never matches: include: ['**/*.{ts,tsx}'] checks nothing, and the same pattern in exclude excludes nothing. **/ also needs a folder, so **/*.ts skips files at the project root such as vite.config.ts. Fix: one plain pattern per extension.

  • twilightReact() has no option to configure or remove its instance, and adding a configured bestPracticesPlugin() 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 render salla-* elements are still checked a second time with default options, because twilight:web-hydration changes them in between.

  • require-page-hooks matches unanchored path fragments: mini-cart.tsx, a CartPageSkeleton.tsx or anything under a not-found folder 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, a custom-cart.tsx that 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

Source and docs