Skip to content

Commit 4b0aaa8

Browse files
miknclaude
andauthored
fix(vite): serve declared directory members that link out of their tree (#363)
When bazel-bin is mirrored with one link per file, Vite resolves a member of a declared directory output to its link target, outside the tree, and the plugin rejected it as "resolved outside declared directory". Map the target back to the in-tree member whose realpath it is and serve that path; anything else still throws. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent baddb2b commit 4b0aaa8

3 files changed

Lines changed: 44 additions & 3 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
### Fixed
2+
3+
- **The dev server serves a declared directory output whose files are
4+
symlinks to paths outside it.** When bazel-bin is mirrored with one link per
5+
file, Vite resolves a directory member to its link target, and the plugin
6+
rejected it as "resolved outside declared directory". In serve mode the
7+
plugin now maps the target back to the member that links to it and serves
8+
it at the member's path.

‎vite/src/plugin.ts‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,9 +291,13 @@ export function bazelPlugin(options: BazelPluginOptions = {}): Plugin {
291291
return result?.filePath ?? null;
292292
}
293293
if (directory !== undefined && resolver.declaredDirectory(resolved.id) !== directory) {
294-
throw new Error(
295-
`Bazel module ${file} resolved outside declared directory: ${resolved.id}`,
296-
);
294+
const member = linkedMember(file, resolved.id);
295+
if (member === undefined || resolver.declaredDirectory(member) !== directory) {
296+
throw new Error(
297+
`Bazel module ${file} resolved outside declared directory: ${resolved.id}`,
298+
);
299+
}
300+
return { ...resolved, id: member + suffix };
297301
}
298302
if (
299303
result !== null &&
@@ -493,6 +497,19 @@ function splitSuffix(request: string): [string, string] {
493497
return cut < 0 ? [request, ''] : [request.slice(0, cut + 1), request.slice(cut + 1)];
494498
}
495499

500+
/** A tree member that links out of its tree, as in a bazel-bin mirrored file by file, resolves to its link target. */
501+
function linkedMember(request: string, resolvedId: string): string | undefined {
502+
const target = path.normalize(resolvedId);
503+
const name = path.basename(target);
504+
return [request, path.join(path.dirname(request), name), path.join(request, name)].find((candidate) => {
505+
try {
506+
return fs.realpathSync(candidate) === target;
507+
} catch {
508+
return false;
509+
}
510+
});
511+
}
512+
496513
function canonicalPath(file: string): string {
497514
for (let ancestor = file; ; ancestor = path.dirname(ancestor)) {
498515
try {

‎vite/tests/resolution_parity_test.mjs‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,22 @@ test('a declared generated tree cannot be replaced by checkout files', async ()
866866
'a native resolution miss must retain the declared producer path');
867867
});
868868

869+
test('a declared tree whose members link out of it still resolves to the member', async () => {
870+
const tree = path.join(bazelBin, 'app/linked');
871+
const target = path.join(root, 'linked-runfiles/runtime.js');
872+
write(target, 'export const runtime = "linked member";\n');
873+
fs.mkdirSync(tree, { recursive: true });
874+
fs.symlinkSync(target, path.join(tree, 'runtime.js'));
875+
const plugin = bazelPlugin({
876+
workspaceRoot, bazelBin, mode: 'serve',
877+
declaredFiles: { 'app/linked': { path: tree, context: 'source', directory: true } },
878+
});
879+
plugin.configResolved({ root: workspaceRoot, logger: { info() {} } });
880+
const context = { resolve: async () => ({ id: fs.realpathSync(target) }) };
881+
assert.deepEqual(await plugin.resolveId.handler.call(context, './linked/runtime', importers.serve, {}),
882+
{ id: path.join(tree, 'runtime.js') });
883+
});
884+
869885
let failed = 0;
870886
for (const [name, fn] of tests) {
871887
try {

0 commit comments

Comments
 (0)