1212
1313import  type  {  OnResolveResult ,  Plugin ,  PluginBuild ,  ResolveOptions  }  from  'esbuild' ; 
1414import  {  stat  }  from  'node:fs/promises' ; 
15- import  {  join  }  from  'node:path' ; 
15+ import  path ,   {  join  }  from  'node:path' ; 
1616
1717export  interface  CreateBazelSandboxPluginOptions  { 
1818  bindir : string ; 
1919  execroot : string ; 
20+   runfiles ?: string ; 
2021} 
2122
2223// Under Bazel, esbuild will follow symlinks out of the sandbox when the sandbox is enabled. See https://github.com/aspect-build/rules_esbuild/issues/58. 
@@ -25,6 +26,7 @@ export interface CreateBazelSandboxPluginOptions {
2526export  function  createBazelSandboxPlugin ( { 
2627  bindir, 
2728  execroot, 
29+   runfiles, 
2830} : CreateBazelSandboxPluginOptions ) : Plugin  { 
2931  return  { 
3032    name : 'bazel-sandbox' , 
@@ -40,7 +42,14 @@ export function createBazelSandboxPlugin({
4042        } 
4143        otherOptions . pluginData . executedSandboxPlugin  =  true ; 
4244
43-         return  await  resolveInExecroot ( {  build,  bindir,  execroot,  importPath,  otherOptions } ) ; 
45+         return  await  resolveInExecroot ( { 
46+           build, 
47+           bindir, 
48+           execroot, 
49+           runfiles, 
50+           importPath, 
51+           otherOptions, 
52+         } ) ; 
4453      } ) ; 
4554    } , 
4655  } ; 
@@ -50,14 +59,30 @@ interface ResolveInExecrootOptions {
5059  build : PluginBuild ; 
5160  bindir : string ; 
5261  execroot : string ; 
62+   runfiles ?: string ; 
5363  importPath : string ; 
5464  otherOptions : ResolveOptions ; 
5565} 
5666
67+ const  EXTERNAL_PREFIX  =  'external/' ; 
68+ 
69+ function  removeExternalPathPrefix ( filePath : string ) : string  { 
70+   // Normalize to relative path without leading slash. 
71+   if  ( filePath . startsWith ( '/' ) )  { 
72+     filePath  =  filePath . substring ( 1 ) ; 
73+   } 
74+   // Remove the EXTERNAL_PREFIX if present. 
75+   if  ( filePath . startsWith ( EXTERNAL_PREFIX ) )  { 
76+     filePath  =  filePath . substring ( EXTERNAL_PREFIX . length ) ; 
77+   } 
78+   return  filePath ; 
79+ } 
80+ 
5781async  function  resolveInExecroot ( { 
5882  build, 
5983  bindir, 
6084  execroot, 
85+   runfiles, 
6186  importPath, 
6287  otherOptions, 
6388} : ResolveInExecrootOptions ) : Promise < OnResolveResult >  { 
@@ -85,8 +110,39 @@ async function resolveInExecroot({
85110        `Error: esbuild resolved a path outside of BAZEL_BINDIR (${ bindir } ${ result . path }  , 
86111      ) ; 
87112    } 
88-     // Otherwise remap the bindir-relative path 
89-     const  correctedPath  =  join ( execroot ,  result . path . substring ( result . path . indexOf ( bindir ) ) ) ; 
113+     // Get the path under the bindir for the file. This allows us to map into 
114+     // the execroot or the runfiles directory (if present). 
115+     // Example: 
116+     //   bindir             = bazel-out/<arch>/bin 
117+     //   result.path        = <base>/execroot/bazel-out/<arch>/bin/external/repo+/path/file.ts 
118+     //   binDirRelativePath = external/repo+/path/file.ts 
119+     const  binDirRelativePath  =  result . path . substring ( 
120+       result . path . indexOf ( bindir )  +  bindir . length  +  1 , 
121+     ) ; 
122+     // We usually remap into the bindir. However, when sources are provided 
123+     // as `data` (runfiles), they will be in the runfiles root instead. The 
124+     // runfiles path is absolute and under the bindir, so we don't need to 
125+     // join anything to it. The execroot does not include the bindir, so there 
126+     // we add it again after previously removing it from the result path. 
127+     const  remapBase  =  runfiles  ??  path . join ( execroot ,  bindir ) ; 
128+     // The path relative to the remapBase also differs between runfiles and 
129+     // bindir, but only if the file is in an external repository. External 
130+     // repositories appear under `external/repo+` in the bindir, whereas they 
131+     // are directly under `repo+` in the runfiles tree. This difference needs 
132+     // to be accounted for by removing a potential `external/` prefix when 
133+     // mapping into runfiles. 
134+     const  remapBaseRelativePath  =  runfiles 
135+       ? removeExternalPathPrefix ( binDirRelativePath ) 
136+       : binDirRelativePath ; 
137+     // Join the paths back together. The results will look slightly different 
138+     // between runfiles and bindir, but this is intentional. 
139+     // Source path: 
140+     //   <bin>/external/repo+/path/file.ts 
141+     // Example in bindir: 
142+     //   <sandbox-bin>/external/repo+/path/file.ts 
143+     // Example in runfiles: 
144+     //   <sandbox-bin>/path/bin.runfiles/repo+/path/file.ts 
145+     const  correctedPath  =  join ( remapBase ,  remapBaseRelativePath ) ; 
90146    if  ( process . env . JS_BINARY__LOG_DEBUG )  { 
91147      // eslint-disable-next-line no-console 
92148      console . error ( 
0 commit comments