twilightReact
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
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./{-$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
// 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
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 devorvite build); nothing from it reaches your pages. It returns, in order:twilight:assets-base(used by Salla platform builds only), the server runtime plugin (unlessworker: 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:schemaand twilight:best-practices.Routes: the engine's built-in pages are merged with the
routesarray exported byapp/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, soroute('/lookbook', 'lookbook.tsx')becomes the route id/{-$locale}/lookbook, and its file must callcreateFileRoute('/{-$locale}/lookbook').It writes a file into
app/routes/for every built-in page, once when the config loads and again in itsconfighook. 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 andredirect/$type/$idlive inside the engine package and are never written into your theme.app/routes.tsis 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 fromapp/, and fails. Only top-levelroute(path, file)andindex(file)entries are read. If the file fails to compile or run, the plugin logsFailed to evaluate routes.tsand generates no custom routes.worker(defaulttrue) 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 theworkerobject is then ignored. Without one the engine writes its own intonode_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.localesDirbundles every*.jsondirectly inside that folder intovirtual:twilight/theme-translations; see Theme translations.twilightJsonfeedsvirtual:twilight/schema, the list DevSettingsWidget edits, and the dev server's type check. Both paths resolve against Vite'sroot(the folder you run Vite in, unless you setroot), not againstvite.config.tsas the option comments say.@tanstack/react-startand@vitejs/plugin-reactare loaded fromnode_modules/<package>of the folder Vite runs in, and@tanstack/virtual-file-routesis 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-routesit only warnsnot found. Please install it.and passes no route tree.Its route plugin adds client
optimizeDeps.includeentries for the engine's i18n packages. It adds noresolve.dedupeand noenvironments.ssr.optimizeDeps.include: write those by hand, as in the example (copied frompackages/theme-custom/vite.config.ts).
Gotchas
plugins: [...twilightReact()], aspackages/theme-engine/src/vite/README.mdshows, 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 defaultworker: truethat registers the runtime plugin twice, which theworkeroption's own documentation calls a hard error. Fix: drop the extra plugin, or keep it and passworker: false.A file in
app/routes/is not a route on its own: the router tree is built only from the built-in list plusapp/routes.ts(the engine passes TanStack a virtual route config, which replaces file discovery).docs/03-routing-system.md("Adding Custom Routes") leaves out theapp/routes.tsentry. Fix: addroute(path, file)for every page you create.Listing a path in
app/routes.tsbefore its file exists makes the generator write a placeholder that importscreateCustomRoute_<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-generatedline.The children of
route(path, file, [children])and everylayout()entry inapp/routes.tsare skipped without a warning, because only top-levelrouteandindexnodes are read. Fix: keep the array flat and write full paths (/lookbook/$id).reactandrouterare typed but ignored: the TanStack adapter reads onlysrcDirectory,baseRoutesImport,localesDir,twilightJsonandworker. The README also documentsroutesDir,baseRoutes,rootRouteandroutesFile, which do not exist. There is no supported way to pass options to@vitejs/plugin-react; router behaviour is set withcreateRouterinapp/router.tsx.Changing
srcDirectorybreaks routing: route files are still generated into./app/routesand custom routes still read from./app/routes.ts(hard-coded intanstack.adapter.ts), while TanStack Start looks in the new folder. Fix: keepapp.worker: { compatibilityFlags: [...] }replaces the default list instead of adding to it. The engine needsnodejs_compatfor its server code (the i18n setup and the per-request context, perworker-config.ts); whennode:async_hookscannot load,src/twilight/context.tsfalls back to one context object shared by every request. Fix: always include'nodejs_compat'.
Related
The build-time code check twilightReact() already runs: six rules print colored findings in the terminal during vite build, never failing it.
DevSettingsWidgetA development-only floating panel that lists your twilight.json settings and home-component fields with defaults, and previews edited values in vite dev.
getFrameworkAdapter, tanstack & nextjsThe adapter objects behind twilightReact(): tanstack generates route files and returns the Vite plugins; nextjs is a placeholder whose methods throw.
Theme translationsHow a theme's locales/*.json files reach t(): the Vite plugin bundles them, TwilightProvider receives them, and Salla's messages are checked first.
createRouterBuilds the theme's TanStack router with the engine's defaults: a data cache, SSR hydration, a loading skeleton, and the error and 404 pages.