12
12
13
13
import type { OnResolveResult , Plugin , PluginBuild , ResolveOptions } from 'esbuild' ;
14
14
import { stat } from 'node:fs/promises' ;
15
- import { join } from 'node:path' ;
15
+ import path , { join } from 'node:path' ;
16
16
17
17
export interface CreateBazelSandboxPluginOptions {
18
18
bindir : string ;
19
19
execroot : string ;
20
+ runfiles ?: string ;
20
21
}
21
22
22
- // 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.
23
- // This plugin using a separate resolver to detect if the the resolution has left the execroot (which is the root of the sandbox
24
- // when sandboxing is enabled) and patches the resolution back into the sandbox.
23
+ // Under Bazel, esbuild will follow symlinks out of the sandbox when the sandbox
24
+ // is enabled. See https://github.com/aspect-build/rules_esbuild/issues/58. This
25
+ // plugin using a separate resolver to detect if the the resolution has left the
26
+ // execroot (which is the root of the sandbox when sandboxing is enabled) and
27
+ // patches the resolution back into the sandbox.
25
28
export function createBazelSandboxPlugin ( {
26
29
bindir,
27
30
execroot,
31
+ runfiles,
28
32
} : CreateBazelSandboxPluginOptions ) : Plugin {
29
33
return {
30
34
name : 'bazel-sandbox' ,
@@ -40,7 +44,14 @@ export function createBazelSandboxPlugin({
40
44
}
41
45
otherOptions . pluginData . executedSandboxPlugin = true ;
42
46
43
- return await resolveInExecroot ( { build, bindir, execroot, importPath, otherOptions } ) ;
47
+ return await resolveInExecroot ( {
48
+ build,
49
+ bindir,
50
+ execroot,
51
+ runfiles,
52
+ importPath,
53
+ otherOptions,
54
+ } ) ;
44
55
} ) ;
45
56
} ,
46
57
} ;
@@ -50,14 +61,31 @@ interface ResolveInExecrootOptions {
50
61
build : PluginBuild ;
51
62
bindir : string ;
52
63
execroot : string ;
64
+ runfiles ?: string ;
53
65
importPath : string ;
54
66
otherOptions : ResolveOptions ;
55
67
}
56
68
69
+ const EXTERNAL_PREFIX = 'external/' ;
70
+
71
+ function removeExternalPathPrefix ( filePath : string ) : string {
72
+ // Normalize to relative path without leading slash.
73
+ if ( filePath . startsWith ( '/' ) ) {
74
+ filePath = filePath . substring ( 1 ) ;
75
+ }
76
+ // Remove the EXTERNAL_PREFIX if present.
77
+ if ( filePath . startsWith ( EXTERNAL_PREFIX ) ) {
78
+ filePath = filePath . substring ( EXTERNAL_PREFIX . length ) ;
79
+ }
80
+
81
+ return filePath ;
82
+ }
83
+
57
84
async function resolveInExecroot ( {
58
85
build,
59
86
bindir,
60
87
execroot,
88
+ runfiles,
61
89
importPath,
62
90
otherOptions,
63
91
} : ResolveInExecrootOptions ) : Promise < OnResolveResult > {
@@ -73,20 +101,53 @@ async function resolveInExecroot({
73
101
! result . path . startsWith ( '/' ) &&
74
102
! result . path . startsWith ( '\\' )
75
103
) {
76
- // Not a relative or absolute path. Likely a module resolution that is marked "external"
104
+ // Not a relative or absolute path. Likely a module resolution that is
105
+ // marked "external"
77
106
return result ;
78
107
}
79
108
80
- // If esbuild attempts to leave the execroot, map the path back into the execroot.
109
+ // If esbuild attempts to leave the execroot, map the path back into the
110
+ // execroot.
81
111
if ( ! result . path . startsWith ( execroot ) ) {
82
112
// If it tried to leave bazel-bin, error out completely.
83
113
if ( ! result . path . includes ( bindir ) ) {
84
114
throw new Error (
85
115
`Error: esbuild resolved a path outside of BAZEL_BINDIR (${ bindir } ): ${ result . path } ` ,
86
116
) ;
87
117
}
88
- // Otherwise remap the bindir-relative path
89
- const correctedPath = join ( execroot , result . path . substring ( result . path . indexOf ( bindir ) ) ) ;
118
+ // Get the path under the bindir for the file. This allows us to map into
119
+ // the execroot or the runfiles directory (if present).
120
+ // Example:
121
+ // bindir = bazel-out/<arch>/bin
122
+ // result.path = <base>/execroot/bazel-out/<arch>/bin/external/repo+/path/file.ts
123
+ // binDirRelativePath = external/repo+/path/file.ts
124
+ const binDirRelativePath = result . path . substring (
125
+ result . path . indexOf ( bindir ) + bindir . length + 1 ,
126
+ ) ;
127
+ // We usually remap into the bindir. However, when sources are provided
128
+ // as `data` (runfiles), they will be in the runfiles root instead. The
129
+ // runfiles path is absolute and under the bindir, so we don't need to
130
+ // join anything to it. The execroot does not include the bindir, so there
131
+ // we add it again after previously removing it from the result path.
132
+ const remapBase = runfiles ?? path . join ( execroot , bindir ) ;
133
+ // The path relative to the remapBase also differs between runfiles and
134
+ // bindir, but only if the file is in an external repository. External
135
+ // repositories appear under `external/repo+` in the bindir, whereas they
136
+ // are directly under `repo+` in the runfiles tree. This difference needs
137
+ // to be accounted for by removing a potential `external/` prefix when
138
+ // mapping into runfiles.
139
+ const remapBaseRelativePath = runfiles
140
+ ? removeExternalPathPrefix ( binDirRelativePath )
141
+ : binDirRelativePath ;
142
+ // Join the paths back together. The results will look slightly different
143
+ // between runfiles and bindir, but this is intentional.
144
+ // Source path:
145
+ // <bin>/external/repo+/path/file.ts
146
+ // Example in bindir:
147
+ // <sandbox-bin>/external/repo+/path/file.ts
148
+ // Example in runfiles:
149
+ // <sandbox-bin>/path/bin.runfiles/repo+/path/file.ts
150
+ const correctedPath = join ( remapBase , remapBaseRelativePath ) ;
90
151
if ( process . env . JS_BINARY__LOG_DEBUG ) {
91
152
// eslint-disable-next-line no-console
92
153
console . error (
@@ -95,12 +156,14 @@ async function resolveInExecroot({
95
156
}
96
157
result . path = correctedPath ;
97
158
98
- // Fall back to `.js` file if resolved `.ts` file does not exist in the changed path.
159
+ // Fall back to `.js` file if resolved `.ts` file does not exist in the
160
+ // changed path.
99
161
//
100
- // It's possible that a `.ts` file exists outside the sandbox and esbuild resolves it. It's not
101
- // guaranteed that the sandbox also contains the same file. One example might be that the build
102
- // depend on a compiled version of the file and the sandbox will only contain the corresponding
103
- // `.js` and `.d.ts` files.
162
+ // It's possible that a `.ts` file exists outside the sandbox and esbuild
163
+ // resolves it. It is not guaranteed that the sandbox also contains the same
164
+ // file. One example might be that the build depends on a compiled version
165
+ // of the file and the sandbox will only contain the corresponding `.js` and
166
+ // `.d.ts` files.
104
167
if ( result . path . endsWith ( '.ts' ) ) {
105
168
try {
106
169
await stat ( result . path ) ;
0 commit comments