Development and production
Warlock keeps one page graph for development SSR, browser hydration, and the production build. The same discovery rules decide which pages and layouts exist in every environment.
Component edits and Fast Refresh
Section titled “Component edits and Fast Refresh”A component-body-only edit is left to React Fast Refresh. It updates the component without a document reload and preserves local state.
Anything outside the component body changes the module’s server-visible skeleton and forces a full reload. That includes:
- imports and module-level declarations;
route,middleware,validation,loader,metadata, orprefix;- a module-level helper even when only the component calls it.
Reloading is deliberately conservative: it re-runs SSR and refreshes the document head instead of risking stale metadata.
Live route-table changes
Section titled “Live route-table changes”Adding, deleting, or renaming a page, or changing its route identity, updates the live route table without restarting the development server. Warlock:
- atomically replaces the page-owned routes, restoring the previous table if registration fails;
- invalidates the browser page registry;
- sends a full document reload so the new server route and hydrated client graph agree.
When checking this manually, verify hydration with an interactive state change
or intercepted <Link> navigation. A visible 200 proves only the server half.
If a request 404s while a matching *.page.tsx exists under src/web/ but is
not registered, the development server logs a one-time warning naming that
file.
Global and page-local CSS
Section titled “Global and page-local CSS”Import global CSS directly from src/web/root.tsx:
import "./app.css";A page or matched layout may import its own stylesheet. Warlock creates one ordered chain for the response: root, ancestor layouts, then page. Only the matched chain contributes render-blocking CSS.
In development, Warlock emits a source stylesheet link ending in ?direct, so
Vite serves text/css instead of a JavaScript CSS module. The rendered root
must contain a closing </head> because stylesheet links are inserted
immediately before it.
Production build
Section titled “Production build”warlock build uses the same discoverPages() scan and:
- emits
page-routes.manifest.jsonwith each page’s method, path, name, and source file; - rejects page graphs that violate the one-rendering-layout rule;
- builds the hydration client and a Vite manifest;
- resolves root, matched-layout, and page CSS into ordered, deduplicated, render-blocking asset links;
- stores the client asset directory in the page manifest so
warlock startdoes not need to load the TypeScript build config.
At production boot, the Web connector refuses to start without the page manifest instead of reporting healthy while serving only 404s.
Use warlock routes:diff to compare the current diagnostic route table with
the last successful build:
warlock routes:diffThe manifest records derived page names. A clean diff does not prove that no two routes collided during registration; discovery itself remains the source of that failure.
Run warlock start from the app root
Section titled “Run warlock start from the app root”The page manifest stores clientDir as an app-root-relative path such as
dist/client. At boot the Web connector resolves it against the process
working directory (process.cwd()), not against the location of dist. A
built dist is therefore not relocatable: run warlock start with the app
root as the working directory. Copying dist elsewhere without the app root
means the client assets, Vite manifest, and copied public files are not found.
Dev: app modules can load twice
Section titled “Dev: app modules can load twice”In warlock dev, page loaders import app modules through Vite SSR, while HTTP
handlers load them natively. A module-level singleton — an AsyncLocalStorage,
a cache, or a registry — can therefore exist twice, and a value set through one
copy is invisible to the other. Until a single module graph lands (planned for
5.26), keep such singletons on globalThis under a Symbol.for key:
import { AsyncLocalStorage } from "node:async_hooks";
const key = Symbol.for("my-app.requestContext");const store = globalThis as typeof globalThis & { [key]?: AsyncLocalStorage<{ userId: string }>;};
export const requestContext = (store[key] ??= new AsyncLocalStorage());Both copies of the module now read the same instance. warlock start has one
module graph and is unaffected.
Dev SSR shares the app’s framework package instances
Section titled “Dev SSR shares the app’s framework package instances”warlock dev runs SSR inside Vite’s module graph. Before 5.13, an app
resolveAlias entry naming a framework package (@warlock.js/cache,
core, logger, context, cascade) made Vite bypass ssr.external and
load a second, never-booted copy of that package — a route with
cache: { serverCache: true } then failed with
CacheDriverNotInitializedError because the module graph’s copy never saw
the connector’s initialization. Dev SSR now always imports those packages
from the instance the app booted, as production does, and drops an app alias
that would re-inline an SSR-external package.
Failed page requests are logged
Section titled “Failed page requests are logged”A page request that fails outside the page pipeline itself — a cache
failure, a module that fails to load — is now logged to stderr as
[warlock:web] page request <METHOD> <path> failed: <error>. Before, the
error reached only the app’s error.page.tsx, and the server log stayed
empty.
Diagnose missing CSS
Section titled “Diagnose missing CSS”- Confirm the root, current layout, or page directly imports the stylesheet.
- In development, inspect the rendered document for a link ending in
?directand confirm it returnstext/css. - In production, confirm the matching source appears in
client/.vite/manifest.jsonwith CSS assets beneath the configured client prefix. - Do not hand-author links to hashed production assets; the manifest owns their filenames.
See warlock routes:diff and
connector lifecycle for
the surrounding Core APIs.