remix_build¶
Wraps remix vite:build as a single Bazel action and returns both halves of a
Remix v2 application: the browser bundle under client/ and the request handler
under server/.
The action stages a Remix project directory from declared inputs alone — the
app/ sources, a node_modules tree, the Vite config carrying the Remix
plugin, an optional tsconfig.json — runs the build in it, and moves Remix's
build directory into one declared output artifact.
Usage¶
load("@rules_typescript//npm:defs.bzl", "node_modules")
load("@rules_typescript//ts:defs.bzl", "remix_build")
node_modules(
name = "node_modules",
deps = [
"@npm//:react",
"@npm//:react-dom",
"@npm//:remix-run_dev",
"@npm//:remix-run_node",
"@npm//:remix-run_react",
"@npm//:vite",
],
)
remix_build(
name = "app",
srcs = glob([
"app/**/*.ts",
"app/**/*.tsx",
]),
config = "remix-vite.config.mjs",
node_modules = ":node_modules",
)
The config is a plain Remix config with no Bazel-specific lines: the rule builds inside a staging root it owns and moves the result out afterwards.
import { vitePlugin as remix } from "@remix-run/dev";
export default {
plugins: [remix({ manifest: true })],
build: { manifest: true },
};
Attributes¶
| Attribute | Type | Default | Description |
|---|---|---|---|
srcs |
label_list |
required | Every project file Remix reads: app/root.tsx, app/entry.client.tsx, app/entry.server.tsx, app/routes/**, and anything they import from inside this package |
node_modules |
label |
required | A node_modules target holding @remix-run/dev and the app's dependencies |
config |
label |
required | The Vite config whose default export carries the Remix plugin |
config_srcs |
label_list |
[] |
Local modules config imports, staged at their paths relative to this package |
staging_srcs |
label_list |
[] |
Files from other packages, staged at their workspace-relative paths |
tsconfig |
label |
None |
A tsconfig.json staged at the project root |
env |
string_dict |
{} |
Extra environment variables for the build |
Each srcs file lands at its path relative to the target's package. Remix loads
config itself, so every Vite option in it reaches the build; it must not set
the plugin's buildDirectory, which the rule owns. config_srcs is what
makes a ./plugins/foo import from the config resolve. staging_srcs has the
same contract as next_build's.
Output¶
One directory artifact, <name>_remix_out:
client/index.html the SPA fallback, when the app has one
client/assets/*.js route chunks, the client entry, shared chunks
client/assets/manifest-<hash>.js window.__remixManifest
server/index.js the request handler build
.remix/manifest.json route id → file / path / parentId (plugin manifest: true)
.vite/client-manifest.json (config build.manifest: true)
.vite/server-manifest.json
Why this is a rule and not a ts_bundle¶
remix vite:build is not one vite build. It loads the Vite config to read the
Remix plugin's own options, then runs vite build twice against that same
config — once with build.ssr = false, once with build.ssr = true — and the
plugin's config hook replaces build.outDir and build.rollupOptions.input
differently for each half.
The order is load-bearing. The SSR half's writeBundle reads the SSR Vite
manifest and moves the SSR-emitted assets into the client directory, and the
browser asset manifest client/assets/manifest-<hash>.js — the
window.__remixManifest a hydrating client reads — is emitted by the server
build. The two halves are not independently cacheable, and a build that declares
only client/ as its output produces a bundle that cannot boot.
ts_bundle's Vite wrapper hardcodes exactly one
vite build --config, and the config it generates reads only plugins and
root out of a user vite_config, throwing on any other key. A real framework
config exceeds that, so config here is the file Remix itself loads.
SPA Mode¶
A config with ssr: false produces something that cannot answer a request:
- a
loaderexport becomes a hard build error (SPA Mode: N invalid route export(s)), so no route can load data; - a resource route compiles to an empty client chunk, because a route that only
exports a
loaderhas nothing to ship to a browser; - the server directory is deleted after
index.htmlis prerendered.
remix_build fails and names the missing half. For a client-only bundle, use
the ts_bundle path with vite_config.
Testing the Server Half¶
The server bundle imports @remix-run/react, react-dom/server and
react/jsx-runtime as bare specifiers and is ESM in a plain .js, so running
it needs the node_modules tree above it and a package.json saying
"type": "module".
//tests/integration:remix_ssr_test copies server/ next to the tree, imports
it through createRequestHandler from @remix-run/node, and asserts on real
responses: loader data in the HTML and in window.__remixContext.loaderData, a
nested layout and its child each running their own loader, a POST whose action
echoes the submitted form field, and a resource route answering text/plain
with no HTML shell.
Route Conventions¶
Remix derives the route tree from filenames in app/routes, one directory level
deep. Gazelle's Remix plugin reads the same conventions and states the tree in
the generated BUILD file:
# Remix route routes/_index → / (index, parent root)
# Remix route routes/dash.settings → /dash/settings (parent routes/dash)
# Remix route routes/users.$userId → /users/:userId (parent root)
ts_compile(
name = "routes",
...
)
See Gazelle overview.