What happened?
I have a Bazel project that uses rules_js to build a vite app. It builds fine with vite 7, but fails with vite 8. To isolate it I made a repro with two packages that are identical except for the vite major version (and the matching @vitejs/plugin-react major, since plugin-react 6.x requires vite 8).
bazel build //vite8:build fails in vite:build-html:
[plugin vite:build-html]
Error: The "fileName" or "name" properties of emitted chunks and assets must
be strings that are neither absolute nor relative paths, received
"../../../../../../../../../execroot/_main/bazel-out/darwin_arm64-fastbuild/bin/vite8/index.html".
at PluginContextImpl.emitFile (.../rolldown/dist/shared/bindingify-input-options-....mjs:978:23)
at PluginContextImpl.generateBundle (.../vite/dist/node/chunks/node.js:22686:10)
The ../../../... part is the giveaway: the emitted index.html fileName has been computed against a root that lives outside the sandbox working directory.
What I think is going on
As far as I can tell, rules_js launches node with patched fs APIs (lstat, readlink, realpath, readdir, and so on) so that JS code can't resolve the sandbox input symlinks back out to the real output tree. In effect this emulates --preserve-symlinks for everything in the execroot, which is what keeps Node tooling working inside the sandbox.
vite 7 and earlier bundle through rollup plus esbuild's JS pipeline, whose path handling goes through Node's (patched) fs. Paths stay inside the sandbox, the index.html fileName is computed relative to the project root, and the build works.
vite 8 bundles through rolldown, whose resolver and fs access are native Rust. It seems the native code calls the OS realpath directly, which bypasses the rules_js patches. The index.html entry, which is a sandbox symlink, gets canonicalized to its real path under the output tree, while config.root stays the unresolved sandbox working directory. vite:build-html then emits with path.relative(root, id), which produces the ../../../... escape above, and rolldown rejects it.
So from the rules_js side, the surface seems to be this: the fs-patching strategy that emulates --preserve-symlinks doesn't reach a bundler that does its fs access in native code. rolldown is the first mainstream one I've hit that does this, but it probably won't be the last.
I'm not assuming this is a rules_js bug to fix. The canonicalization could also be addressed in rolldown (an option not to canonicalize entry/module ids) or in vite (compute the html fileName against the canonicalized root). I'm filing here because the --preserve-symlinks emulation is a rules_js mechanism, so you'll have the clearest read on whether there's a fix possible on this side.
Versions
| Component |
Version |
| Bazel |
9.1.1 |
| aspect_rules_js |
3.2.2 |
| rules_nodejs / Node |
6.7.4 / 22.22.2 |
| pnpm |
11.5.2 |
| vite (failing) |
8.0.16 (rolldown 1.0.3) |
| vite (passing) |
7.3.5 |
| @vitejs/plugin-react |
6.0.2 (vite8) / 5.2.0 (vite7) |
Development (host) and target OS/architectures
macOS, arm64. Observed under Bazel's darwin-sandbox. The mechanism (action inputs presented as symlinks into the real output tree) should apply to the Linux sandbox as well, but I haven't reproduced it there.
$ bazel --version
bazel 9.1.1
Language(s) and/or frameworks involved
JavaScript / vite / rolldown, with a pnpm node_modules layout.
How to reproduce
Minimal repro (requires bazelisk and pnpm; lockfile is checked in): https://github.com/macourteau/vite8-rolldown-bazel-repro
bazel build //vite7:build # PASSES
bazel build //vite8:build # FAILS
The two packages are identical apps apart from the vite major. The .bazelrc disables disk and remote caches so a previously cached unsandboxed success can't mask the sandboxed failure.
Workarounds I tried (both fail, differently)
resolve: { preserveSymlinks: true }. This fixes the entry canonicalization, but with a pnpm node_modules layout the transitive deps of direct deps live behind symlinks into the pnpm virtual store, and they stop resolving:
Error: [vite]: Rolldown failed to resolve import "ms" from
".../bin/vite8/node_modules/debug/src/common.js".
(debug depends on ms; it's in the repro specifically to show this.)
tags = ["no-sandbox"]. This removes the symlink indirection, so the canonicalization problem disappears and the minimal app builds. But in the larger real project this was extracted from, the unsandboxed build then failed a third way: rolldown wrote dist/index.html carrying the read-only permission bits of the Bazel input file it was derived from, then couldn't overwrite its own output:
[UNHANDLEABLE_ERROR] Something went wrong inside rolldown ...
Failed to write file in .../bin/.../frontend/dist/index.html
Caused by: Permission denied (os error 13)
(Bazel marks action inputs and outputs read-only, so any tool that preserves source permissions on derived files and then re-opens them for writing will break.) I haven't minimized that third failure in the repro since it needed a larger app (CSS, fonts, public/ assets) to trigger, but I'm noting it because it closes off the obvious escape hatch.
Any other information?
The concrete question for rules_js: is there an existing or planned strategy for bundlers that do fs access in native code (rolldown today, probably others as more JS tooling moves to Rust), given that the fs-patch approach to emulating --preserve-symlinks doesn't reach them? Happy to test patches against the repro.
What happened?
I have a Bazel project that uses rules_js to build a vite app. It builds fine with vite 7, but fails with vite 8. To isolate it I made a repro with two packages that are identical except for the vite major version (and the matching
@vitejs/plugin-reactmajor, since plugin-react 6.x requires vite 8).bazel build //vite8:buildfails invite:build-html:The
../../../...part is the giveaway: the emittedindex.htmlfileName has been computed against a root that lives outside the sandbox working directory.What I think is going on
As far as I can tell, rules_js launches node with patched
fsAPIs (lstat,readlink,realpath,readdir, and so on) so that JS code can't resolve the sandbox input symlinks back out to the real output tree. In effect this emulates--preserve-symlinksfor everything in the execroot, which is what keeps Node tooling working inside the sandbox.vite 7 and earlier bundle through rollup plus esbuild's JS pipeline, whose path handling goes through Node's (patched)
fs. Paths stay inside the sandbox, theindex.htmlfileName is computed relative to the project root, and the build works.vite 8 bundles through rolldown, whose resolver and fs access are native Rust. It seems the native code calls the OS
realpathdirectly, which bypasses the rules_js patches. Theindex.htmlentry, which is a sandbox symlink, gets canonicalized to its real path under the output tree, whileconfig.rootstays the unresolved sandbox working directory.vite:build-htmlthen emits withpath.relative(root, id), which produces the../../../...escape above, and rolldown rejects it.So from the rules_js side, the surface seems to be this: the
fs-patching strategy that emulates--preserve-symlinksdoesn't reach a bundler that does its fs access in native code. rolldown is the first mainstream one I've hit that does this, but it probably won't be the last.I'm not assuming this is a rules_js bug to fix. The canonicalization could also be addressed in rolldown (an option not to canonicalize entry/module ids) or in vite (compute the html fileName against the canonicalized root). I'm filing here because the
--preserve-symlinksemulation is a rules_js mechanism, so you'll have the clearest read on whether there's a fix possible on this side.Versions
Development (host) and target OS/architectures
macOS, arm64. Observed under Bazel's
darwin-sandbox. The mechanism (action inputs presented as symlinks into the real output tree) should apply to the Linux sandbox as well, but I haven't reproduced it there.Language(s) and/or frameworks involved
JavaScript / vite / rolldown, with a pnpm
node_moduleslayout.How to reproduce
Minimal repro (requires
bazeliskandpnpm; lockfile is checked in): https://github.com/macourteau/vite8-rolldown-bazel-reprobazel build //vite7:build # PASSES
bazel build //vite8:build # FAILS
The two packages are identical apps apart from the vite major. The
.bazelrcdisables disk and remote caches so a previously cached unsandboxed success can't mask the sandboxed failure.Workarounds I tried (both fail, differently)
resolve: { preserveSymlinks: true }. This fixes the entry canonicalization, but with a pnpmnode_moduleslayout the transitive deps of direct deps live behind symlinks into the pnpm virtual store, and they stop resolving:Error: [vite]: Rolldown failed to resolve import "ms" from
".../bin/vite8/node_modules/debug/src/common.js".
(
debugdepends onms; it's in the repro specifically to show this.)tags = ["no-sandbox"]. This removes the symlink indirection, so the canonicalization problem disappears and the minimal app builds. But in the larger real project this was extracted from, the unsandboxed build then failed a third way: rolldown wrotedist/index.htmlcarrying the read-only permission bits of the Bazel input file it was derived from, then couldn't overwrite its own output:[UNHANDLEABLE_ERROR] Something went wrong inside rolldown ...
Failed to write file in .../bin/.../frontend/dist/index.html
Caused by: Permission denied (os error 13)
(Bazel marks action inputs and outputs read-only, so any tool that preserves source permissions on derived files and then re-opens them for writing will break.) I haven't minimized that third failure in the repro since it needed a larger app (CSS, fonts,
public/assets) to trigger, but I'm noting it because it closes off the obvious escape hatch.Any other information?
The concrete question for rules_js: is there an existing or planned strategy for bundlers that do fs access in native code (rolldown today, probably others as more JS tooling moves to Rust), given that the
fs-patch approach to emulating--preserve-symlinksdoesn't reach them? Happy to test patches against the repro.