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

twilightReact

pluginBeginnerlive demo

The one Vite plugin call in a theme's vite.config.ts: server rendering, a route for every storefront page, translations and build checks.

import { twilightReact, TwilightReactOptions } from '@salla.sa/twilight-theme-engine/vite';

In plain words

Vite is the tool that turns your theme's files into code a browser and a server can run. Its settings live in vite.config.ts, and a plugin is a piece you add to those settings to teach Vite something new.

twilightReact() is the one plugin call a theme makes. It sets up everything a Salla theme needs: rendering pages on the server, a route (a URL and the file that draws it) for every storefront page such as home, cart and product, your translation files, and checks of your code while it builds.

It returns a Promise (a value that arrives later), so you await it inside an async config function and spread the list of plugins it gives you into plugins.

Signature

function twilightReact(options?: TwilightReactOptions): Promise<PluginOption[]>
// also the default export of @salla.sa/twilight-theme-engine/vite

interface TwilightReactOptions {
  framework?: 'tanstack' | 'nextjs';  // default 'tanstack'; 'nextjs' throws (not implemented)
  srcDirectory?: string;              // default 'app'; keep it (see Gotchas)
  localesDir?: string;                // no default: without it the translations module is {}
  twilightJson?: string;              // default 'twilight.json'; read by the dev server only
  worker?: boolean | {                // default true
    name?: string;                    // default '<package name without scope>-dev'
    compatibilityDate?: string;       // default '2026-03-03'
    compatibilityFlags?: string[];    // default ['nodejs_compat']; replaced, not merged
  };
  baseRoutesImport?: string;          // default '@salla.sa/twilight-theme-engine/routes'
  react?: ReactPluginOptions | PluginOption;  // accepted, ignored
  router?: RouterOptions | PluginOption;      // accepted, ignored
}

Try it live

Every route twilightReact() generated for this playground: the engine's storefront pages plus the four /playground routes from its own app/routes.ts.Try this: type /cart or /account/wishlist (built-in pages), then /lookbook, and watch the code tab write the entry and the file.
Storefront canvas · en · LTR

/{-$locale}/lookbook is free: this playground has no route there.

  • /{-$locale}
  • /{-$locale}/
  • /{-$locale}/$slug/brand-{$id}
  • /{-$locale}/$slug/c{$id}
  • /{-$locale}/$slug/page-{$id}
  • /{-$locale}/$slug/p{$id}
  • /{-$locale}/$slug/tag-{$id}
  • /{-$locale}/account
  • /{-$locale}/account/notifications
  • /{-$locale}/account/orders
  • /{-$locale}/account/orders/$id
  • /{-$locale}/account/profile
  • /{-$locale}/account/settings
  • /{-$locale}/account/wallet
  • /{-$locale}/account/wishlist
  • /{-$locale}/blog
  • /{-$locale}/blog/$slug/a-{$id}
  • /{-$locale}/blog/$slug/c-{$id}
  • /{-$locale}/blog/$slug/tag-{$id}
  • /{-$locale}/blog/author/$id
  • /{-$locale}/brands
  • /{-$locale}/brands/$id
  • /{-$locale}/cart
  • /{-$locale}/latest-products
  • /{-$locale}/loyalty
  • /{-$locale}/most-sales-products
  • /{-$locale}/offers
  • /{-$locale}/pending-orders
  • /{-$locale}/playground
  • /{-$locale}/playground/$section
  • /{-$locale}/playground/$section/$slug
  • /{-$locale}/playground/$section/$slug/$entry
  • /{-$locale}/redirect/$type/$id
  • /{-$locale}/search
  • /{-$locale}/tags/$id
  • /{-$locale}/testimonials
  • /{-$locale}/thankyou/$orderId
37 routes, read from useRouter().routesById
Controls
A path without the locale, as you would pass it to route().
What a theme writes
// app/routes.ts
import { route } from '@tanstack/virtual-file-routes';

export const routes = [route('/lookbook', 'lookbook.tsx')];

// app/routes/lookbook.tsx: create this file BEFORE adding the entry above
import { createFileRoute } from '@tanstack/react-router';

export const Route = createFileRoute('/{-$locale}/lookbook')({
  component: Page,
});

function Page() {
  return <h1>{'/lookbook'}</h1>;
}

Example

vite.config.ts
import { defineConfig } from 'vite';
import { twilightReact } from '@salla.sa/twilight-theme-engine/vite';

export default defineConfig(async () => ({
  // Async: await it, then spread the plugins it returns.
  plugins: [...(await twilightReact({ localesDir: './locales' }))],
  resolve: {
    // One copy of each library whose React context the engine shares with your code.
    dedupe: [
      'react',
      'react-dom',
      'react/jsx-runtime',
      '@tanstack/react-router',
      '@tanstack/react-query',
      'react-i18next',
      'i18next',
    ],
  },
  environments: {
    ssr: {
      // The server runtime cannot run CommonJS: pre-bundle what reaches it.
      optimizeDeps: {
        include: [
          '@tanstack/react-query',
          '@salla.sa/twilight-theme-engine > react-i18next',
          '@salla.sa/twilight-theme-engine > i18next',
          '@salla.sa/twilight-theme-engine > react-i18next > html-parse-stringify',
          '@salla.sa/twilight-theme-engine > react-i18next > html-parse-stringify > void-elements',
          '@salla.sa/twilight-theme-engine > react-i18next > use-sync-external-store',
        ],
      },
    },
  },
}));

How it behaves

  • It runs in Node when Vite starts (vite dev or vite build); nothing from it reaches your pages. It returns, in order: twilight:assets-base (used by Salla platform builds only), the server runtime plugin (unless worker: false), TanStack Start, @vitejs/plugin-react, twilight:package-aliases (meant for the engine repository itself), twilight:react (route generation), twilight:web-hydration, twilight:translations, twilight:schema and twilight:best-practices.

  • Routes: the engine's built-in pages are merged with the routes array exported by app/routes.ts. A path you list replaces the built-in route with the same path; a new path adds a page. Every page nests under the engine's {-$locale} route, so route('/lookbook', 'lookbook.tsx') becomes the route id /{-$locale}/lookbook, and its file must call createFileRoute('/{-$locale}/lookbook').

  • It writes a file into app/routes/ for every built-in page, once when the config loads and again in its config hook. A file is rewritten only while its first line is // @auto-generated: delete that line and the file is yours. The {-$locale} wrapper, the account layout and redirect/$type/$id live inside the engine package and are never written into your theme.

  • app/routes.ts is compiled with esbuild and evaluated in Node, outside Vite, so import only packages in it (@tanstack/virtual-file-routes): a relative import resolves from the folder Vite runs in, not from app/, and fails. Only top-level route(path, file) and index(file) entries are read. If the file fails to compile or run, the plugin logs Failed to evaluate routes.ts and generates no custom routes.

  • worker (default true) registers the server runtime plugin, so pages render on the server while you develop. A server config file already in the theme folder is used as it is, and the worker object is then ignored. Without one the engine writes its own into node_modules/.twilight/ (name, compatibility_date, compatibility_flags, main: @tanstack/react-start/server-entry), rewriting it only when it changes. Publishing a theme reads neither file.

  • localesDir bundles every *.json directly inside that folder into virtual:twilight/theme-translations; see Theme translations. twilightJson feeds virtual:twilight/schema, the list DevSettingsWidget edits, and the dev server's type check. Both paths resolve against Vite's root (the folder you run Vite in, unless you set root), not against vite.config.ts as the option comments say.

  • @tanstack/react-start and @vitejs/plugin-react are loaded from node_modules/<package> of the folder Vite runs in, and @tanstack/virtual-file-routes is required from there too: all three must be direct dependencies of the theme, and Vite must run from the theme folder, as the package scripts do. Without @tanstack/virtual-file-routes it only warns not found. Please install it. and passes no route tree.

  • Its route plugin adds client optimizeDeps.include entries for the engine's i18n packages. It adds no resolve.dedupe and no environments.ssr.optimizeDeps.include: write those by hand, as in the example (copied from packages/theme-custom/vite.config.ts).

Gotchas

  • plugins: [...twilightReact()], as packages/theme-engine/src/vite/README.md shows, spreads a Promise: TypeScript rejects it and Vite throws a TypeError because a Promise is not iterable. Fix: defineConfig(async () => ({ plugins: [...(await twilightReact())] })).

  • The JSDoc example on twilightReact (what your editor shows on hover) adds a second server runtime plugin next to it. With the default worker: true that registers the runtime plugin twice, which the worker option's own documentation calls a hard error. Fix: drop the extra plugin, or keep it and pass worker: false.

  • A file in app/routes/ is not a route on its own: the router tree is built only from the built-in list plus app/routes.ts (the engine passes TanStack a virtual route config, which replaces file discovery). docs/03-routing-system.md ("Adding Custom Routes") leaves out the app/routes.ts entry. Fix: add route(path, file) for every page you create.

  • Listing a path in app/routes.ts before its file exists makes the generator write a placeholder that imports createCustomRoute_<file> from the engine, which does not exist, so the build fails. Fix: create the file first; if the placeholder is already there, replace its content, starting with its // @auto-generated line.

  • The children of route(path, file, [children]) and every layout() entry in app/routes.ts are skipped without a warning, because only top-level route and index nodes are read. Fix: keep the array flat and write full paths (/lookbook/$id).

  • react and router are typed but ignored: the TanStack adapter reads only srcDirectory, baseRoutesImport, localesDir, twilightJson and worker. The README also documents routesDir, baseRoutes, rootRoute and routesFile, which do not exist. There is no supported way to pass options to @vitejs/plugin-react; router behaviour is set with createRouter in app/router.tsx.

  • Changing srcDirectory breaks routing: route files are still generated into ./app/routes and custom routes still read from ./app/routes.ts (hard-coded in tanstack.adapter.ts), while TanStack Start looks in the new folder. Fix: keep app.

  • worker: { compatibilityFlags: [...] } replaces the default list instead of adding to it. The engine needs nodejs_compat for its server code (the i18n setup and the per-request context, per worker-config.ts); when node:async_hooks cannot load, src/twilight/context.ts falls back to one context object shared by every request. Fix: always include 'nodejs_compat'.

Related

Source and docs