Skip to content

[Bug]: vite 8 (rolldown) fails under the sandbox because native-fs path canonicalization bypasses the rules_js fs patches #2888

Description

@macourteau

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)

  1. 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.)

  1. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions