|
| 1 | +"""Root BUILD file for the examples/remix-app workspace. |
| 2 | +
|
| 3 | +Demonstrates: |
| 4 | + - Remix v2 SPA mode with rules_typescript |
| 5 | + - staging_srcs: copies app/ route files into a writable staging dir so |
| 6 | + the Remix vitePlugin can scan routes and write codegen |
| 7 | + - vite_config: injects the Remix Vite plugin while preserving Bazel |
| 8 | + resolve.alias and outDir integration |
| 9 | + - How framework plugins interact with the Bazel sandbox |
| 10 | +
|
| 11 | +Run `bazel run //:gazelle` to regenerate BUILD files. |
| 12 | +Run `bazel build //...` to compile all packages. |
| 13 | +Run `bazel build //:app_plain` to produce a plain Vite SPA bundle (no Remix). |
| 14 | +Run `bazel build //:app_remix` to produce a full Remix SPA bundle with routes. |
| 15 | +
|
| 16 | +── What :app_remix does ─────────────────────────────────────────────────────── |
| 17 | +
|
| 18 | +:app_remix uses ts_bundle in app mode with: |
| 19 | + - staging_srcs = glob(["app/**/*.tsx"]) + ["package.json", "index.html"] |
| 20 | + Copies all app/ source files plus required project files into a writable |
| 21 | + _staging/ directory before Vite runs. VITE_STAGING_ROOT is set to the |
| 22 | + staging dir so the Remix vitePlugin finds app/routes/*.tsx there. |
| 23 | + - vite_config = "remix-vite.config.mjs" |
| 24 | + Injects vitePlugin() from @remix-run/dev in SPA mode (ssr: false) and |
| 25 | + sets buildDirectory = VITE_OUT_DIR so Remix writes its output into the |
| 26 | + Bazel-declared directory (app_remix_bundle/). |
| 27 | +
|
| 28 | +── Remix SPA mode ────────────────────────────────────────────────────────────── |
| 29 | +
|
| 30 | +Remix v2 supports SPA mode (ssr: false) which: |
| 31 | + - Produces only a client bundle (no server bundle). |
| 32 | + - Does not require app/entry.server.tsx or a Node.js adapter. |
| 33 | + - Outputs to buildDirectory/client/ (configurable via vitePlugin options). |
| 34 | +
|
| 35 | +── How the integration works ─────────────────────────────────────────────────── |
| 36 | +
|
| 37 | +The three key pieces that make Remix work in the Bazel sandbox: |
| 38 | +
|
| 39 | + 1. staging_srcs (writable source directory): |
| 40 | + Remix's vitePlugin() must write codegen files (route types, manifests) |
| 41 | + during the build. The Bazel source tree is read-only. staging_srcs copies |
| 42 | + source files into a writable _staging/ dir inside the action sandbox. |
| 43 | + VITE_STAGING_ROOT points to this dir; vite.root is set to it so Remix |
| 44 | + finds app/routes/*.tsx there. |
| 45 | +
|
| 46 | + 2. HTML in staging (fixes Rollup path traversal): |
| 47 | + When staging is the Vite root, the HTML file must also be inside staging |
| 48 | + (not at ../_html_staging/). The wrapper script detects when index.html |
| 49 | + is staged and updates VITE_HTML_PATH to point to the staged copy, |
| 50 | + so rollupOptions.input uses a clean relative path with no '..'. |
| 51 | +
|
| 52 | + 3. buildDirectory = VITE_OUT_DIR (fixes output capture): |
| 53 | + By default, Remix overrides Vite's build.outDir and writes to |
| 54 | + "<viteRoot>/build/client/". Since viteRoot = staging dir, the output |
| 55 | + would land outside the Bazel-declared directory. remix-vite.config.mjs |
| 56 | + passes buildDirectory = process.env["VITE_OUT_DIR"] to the Remix plugin, |
| 57 | + redirecting output into app_remix_bundle/client/ — inside the declared dir. |
| 58 | +
|
| 59 | +── What does NOT work with full SSR Remix in Bazel ──────────────────────────── |
| 60 | +
|
| 61 | +Full SSR Remix (ssr: true, the default) requires: |
| 62 | + 1. A server entry (app/entry.server.tsx). |
| 63 | + 2. A separate server bundle build (outputs to buildDirectory/server/). |
| 64 | + 3. A Node.js adapter (@remix-run/node or @remix-run/express). |
| 65 | +
|
| 66 | +The dual-output model (client + server) is architecturally incompatible with |
| 67 | +ts_bundle's single declared_directory output. Use SPA mode for Bazel-native |
| 68 | +Remix deployments. |
| 69 | +
|
| 70 | +── Route compilation model ────────────────────────────────────────────────────── |
| 71 | +
|
| 72 | +Note: Remix compiles route TypeScript via its own Vite transforms, not via |
| 73 | +Bazel's oxc compiler. The oxc-compiled .js files in bazel-bin are available |
| 74 | +via resolve.alias but Remix uses the staged .tsx files directly for routes. |
| 75 | +This is by design: staging makes source available; Remix handles compilation. |
| 76 | +The ts_compile targets (entry_client, root, routes) are still useful for |
| 77 | +incremental compilation validation and can be depended on by other targets. |
| 78 | +""" |
| 79 | + |
| 80 | +load("@gazelle//:def.bzl", "gazelle") |
| 81 | +load("@rules_typescript//npm:defs.bzl", "node_modules") |
| 82 | +load("@rules_typescript//ts:defs.bzl", "ts_bundle", "ts_compile") |
| 83 | +load("@rules_typescript//vite:bundler.bzl", "vite_bundler") |
| 84 | + |
| 85 | +# Run "bazel run //:gazelle" to auto-generate BUILD files. |
| 86 | +gazelle( |
| 87 | + name = "gazelle", |
| 88 | + gazelle = "@rules_typescript//gazelle:gazelle_ts", |
| 89 | +) |
| 90 | + |
| 91 | +# ── Compilation targets ────────────────────────────────────────────────────── |
| 92 | +# |
| 93 | +# Each ts_compile target covers files in a single directory. This is required |
| 94 | +# by rules_typescript's oxc invocation: when multiple files are compiled |
| 95 | +# together, they must share the same strip-dir-prefix (their common directory). |
| 96 | +# Files in different subdirectories (app/ vs app/routes/) must be in separate |
| 97 | +# targets. |
| 98 | +# |
| 99 | +# The staging_srcs on ts_bundle accepts any label_list, so we can pass all |
| 100 | +# source files to glob the full app/ tree — the wrapper stages each file at |
| 101 | +# its package-relative path. |
| 102 | + |
| 103 | +# Compile the Remix client entry point (single-file target — required by |
| 104 | +# ts_bundle which expects exactly 1 .js output from the entry_point). |
| 105 | +ts_compile( |
| 106 | + name = "entry_client", |
| 107 | + srcs = ["app/entry.client.tsx"], |
| 108 | + visibility = ["//visibility:public"], |
| 109 | + deps = [ |
| 110 | + "@npm//:react", |
| 111 | + "@npm//:react-dom", |
| 112 | + "@npm//:remix-run_react", |
| 113 | + ], |
| 114 | +) |
| 115 | + |
| 116 | +# Compile the root component (single-file). |
| 117 | +ts_compile( |
| 118 | + name = "root", |
| 119 | + srcs = ["app/root.tsx"], |
| 120 | + visibility = ["//visibility:public"], |
| 121 | + deps = [ |
| 122 | + "@npm//:react", |
| 123 | + "@npm//:remix-run_react", |
| 124 | + ], |
| 125 | +) |
| 126 | + |
| 127 | +# Compile the route components (all in app/routes/). |
| 128 | +ts_compile( |
| 129 | + name = "routes", |
| 130 | + srcs = [ |
| 131 | + "app/routes/_index.tsx", |
| 132 | + "app/routes/about.tsx", |
| 133 | + ], |
| 134 | + visibility = ["//visibility:public"], |
| 135 | + deps = [ |
| 136 | + "@npm//:react", |
| 137 | + "@npm//:remix-run_react", |
| 138 | + ], |
| 139 | +) |
| 140 | + |
| 141 | +# node_modules tree with vite, remix, and their transitive deps. |
| 142 | +node_modules( |
| 143 | + name = "node_modules", |
| 144 | + deps = [ |
| 145 | + "@npm//:vite", |
| 146 | + "@npm//:react", |
| 147 | + "@npm//:react-dom", |
| 148 | + "@npm//:remix-run_dev", |
| 149 | + "@npm//:remix-run_react", |
| 150 | + "@npm//:remix-run_node", |
| 151 | + ], |
| 152 | +) |
| 153 | + |
| 154 | +# Vite bundler instance. |
| 155 | +vite_bundler( |
| 156 | + name = "vite", |
| 157 | + vite = "@npm//:vite", |
| 158 | + node_modules = ":node_modules", |
| 159 | +) |
| 160 | + |
| 161 | +# ── Plain SPA bundle (no Remix plugin) ────────────────────────────────────── |
| 162 | +# This always works: plain Vite bundles the entry_client.tsx entry point. |
| 163 | +# No route scanning, no Remix framework features. |
| 164 | +ts_bundle( |
| 165 | + name = "app_plain", |
| 166 | + mode = "app", |
| 167 | + html = "index.html", |
| 168 | + entry_point = ":entry_client", |
| 169 | + bundler = ":vite", |
| 170 | + minify = False, |
| 171 | + sourcemap = False, |
| 172 | +) |
| 173 | + |
| 174 | +# ── Remix SPA bundle (staging_srcs + vite_config) ──────────────────────────── |
| 175 | +# |
| 176 | +# Uses staging_srcs to copy all app/ source files into a writable staging dir. |
| 177 | +# The Remix vitePlugin (via remix-vite.config.mjs) scans routes in the staging |
| 178 | +# dir and generates the route manifest. |
| 179 | +# |
| 180 | +# Output: app_remix_bundle/client/ containing: |
| 181 | +# - index.html (Remix-generated entry with hashed asset references) |
| 182 | +# - assets/*.js (route chunks: entry.client, root, _index, about, + vendors) |
| 183 | +# - .vite/manifest.json (Vite build manifest) |
| 184 | +# |
| 185 | +# Run: bazel build //:app_remix |
| 186 | +ts_bundle( |
| 187 | + name = "app_remix", |
| 188 | + mode = "app", |
| 189 | + html = "index.html", |
| 190 | + entry_point = ":entry_client", |
| 191 | + bundler = ":vite", |
| 192 | + vite_config = "remix-vite.config.mjs", |
| 193 | + # Stage the full project structure that Remix needs: |
| 194 | + # - package.json (required by Remix's config resolver) |
| 195 | + # - app/**/*.tsx (route files for route scanning) |
| 196 | + # - index.html (HTML entry point — staged so Rollup resolves it from |
| 197 | + # the staging root without '../' path traversal) |
| 198 | + # The staging dir becomes vite.root so Remix finds: |
| 199 | + # package.json at <staging>/package.json |
| 200 | + # routes at <staging>/app/routes/*.tsx |
| 201 | + staging_srcs = glob( |
| 202 | + ["app/**/*.tsx"], |
| 203 | + allow_empty = False, |
| 204 | + ) + [ |
| 205 | + "package.json", |
| 206 | + "index.html", |
| 207 | + ], |
| 208 | + minify = False, |
| 209 | + sourcemap = False, |
| 210 | + tags = ["manual"], |
| 211 | +) |
0 commit comments