|  | 
|  | 1 | +/** | 
|  | 2 | + * @license | 
|  | 3 | + * Copyright Google LLC All Rights Reserved. | 
|  | 4 | + * | 
|  | 5 | + * Use of this source code is governed by an MIT-style license that can be | 
|  | 6 | + * found in the LICENSE file at https://angular.dev/license | 
|  | 7 | + */ | 
|  | 8 | + | 
|  | 9 | +/** | 
|  | 10 | + * Forked from https://github.com/aspect-build/rules_esbuild/blob/e4e49d3354cbf7087c47ac9c5f2e6fe7f5e398d3/esbuild/private/plugins/bazel-sandbox.js | 
|  | 11 | + */ | 
|  | 12 | + | 
|  | 13 | +import type { OnResolveResult, Plugin, PluginBuild, ResolveOptions } from 'esbuild'; | 
|  | 14 | +import { stat } from 'node:fs/promises'; | 
|  | 15 | +import { join } from 'node:path'; | 
|  | 16 | + | 
|  | 17 | +export interface CreateBazelSandboxPluginOptions { | 
|  | 18 | +  bindir: string; | 
|  | 19 | +  execroot: string; | 
|  | 20 | +} | 
|  | 21 | + | 
|  | 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. | 
|  | 25 | +export function createBazelSandboxPlugin({ | 
|  | 26 | +  bindir, | 
|  | 27 | +  execroot, | 
|  | 28 | +}: CreateBazelSandboxPluginOptions): Plugin { | 
|  | 29 | +  return { | 
|  | 30 | +    name: 'bazel-sandbox', | 
|  | 31 | +    setup(build) { | 
|  | 32 | +      build.onResolve({ filter: /./ }, async ({ path: importPath, ...otherOptions }) => { | 
|  | 33 | +        // NB: these lines are to prevent infinite recursion when we call `build.resolve`. | 
|  | 34 | +        if (otherOptions.pluginData) { | 
|  | 35 | +          if (otherOptions.pluginData.executedSandboxPlugin) { | 
|  | 36 | +            return; | 
|  | 37 | +          } | 
|  | 38 | +        } else { | 
|  | 39 | +          otherOptions.pluginData = {}; | 
|  | 40 | +        } | 
|  | 41 | +        otherOptions.pluginData.executedSandboxPlugin = true; | 
|  | 42 | + | 
|  | 43 | +        return await resolveInExecroot({ build, bindir, execroot, importPath, otherOptions }); | 
|  | 44 | +      }); | 
|  | 45 | +    }, | 
|  | 46 | +  }; | 
|  | 47 | +} | 
|  | 48 | + | 
|  | 49 | +interface ResolveInExecrootOptions { | 
|  | 50 | +  build: PluginBuild; | 
|  | 51 | +  bindir: string; | 
|  | 52 | +  execroot: string; | 
|  | 53 | +  importPath: string; | 
|  | 54 | +  otherOptions: ResolveOptions; | 
|  | 55 | +} | 
|  | 56 | + | 
|  | 57 | +async function resolveInExecroot({ | 
|  | 58 | +  build, | 
|  | 59 | +  bindir, | 
|  | 60 | +  execroot, | 
|  | 61 | +  importPath, | 
|  | 62 | +  otherOptions, | 
|  | 63 | +}: ResolveInExecrootOptions): Promise<OnResolveResult> { | 
|  | 64 | +  const result = await build.resolve(importPath, otherOptions); | 
|  | 65 | + | 
|  | 66 | +  if (result.errors && result.errors.length) { | 
|  | 67 | +    // There was an error resolving, just return the error as-is. | 
|  | 68 | +    return result; | 
|  | 69 | +  } | 
|  | 70 | + | 
|  | 71 | +  if ( | 
|  | 72 | +    !result.path.startsWith('.') && | 
|  | 73 | +    !result.path.startsWith('/') && | 
|  | 74 | +    !result.path.startsWith('\\') | 
|  | 75 | +  ) { | 
|  | 76 | +    // Not a relative or absolute path. Likely a module resolution that is marked "external" | 
|  | 77 | +    return result; | 
|  | 78 | +  } | 
|  | 79 | + | 
|  | 80 | +  // If esbuild attempts to leave the execroot, map the path back into the execroot. | 
|  | 81 | +  if (!result.path.startsWith(execroot)) { | 
|  | 82 | +    // If it tried to leave bazel-bin, error out completely. | 
|  | 83 | +    if (!result.path.includes(bindir)) { | 
|  | 84 | +      throw new Error( | 
|  | 85 | +        `Error: esbuild resolved a path outside of BAZEL_BINDIR (${bindir}): ${result.path}`, | 
|  | 86 | +      ); | 
|  | 87 | +    } | 
|  | 88 | +    // Otherwise remap the bindir-relative path | 
|  | 89 | +    const correctedPath = join(execroot, result.path.substring(result.path.indexOf(bindir))); | 
|  | 90 | +    if (process.env.JS_BINARY__LOG_DEBUG) { | 
|  | 91 | +      // eslint-disable-next-line no-console | 
|  | 92 | +      console.error( | 
|  | 93 | +        `DEBUG: [bazel-sandbox] correcting resolution ${result.path} that left the sandbox to ${correctedPath}.`, | 
|  | 94 | +      ); | 
|  | 95 | +    } | 
|  | 96 | +    result.path = correctedPath; | 
|  | 97 | + | 
|  | 98 | +    // Fall back to `.js` file if resolved `.ts` file does not exist in the changed path. | 
|  | 99 | +    // | 
|  | 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. | 
|  | 104 | +    if (result.path.endsWith('.ts')) { | 
|  | 105 | +      try { | 
|  | 106 | +        await stat(result.path); | 
|  | 107 | +      } catch (e: unknown) { | 
|  | 108 | +        const jsPath = result.path.slice(0, -3) + '.js'; | 
|  | 109 | +        if (process.env.JS_BINARY__LOG_DEBUG) { | 
|  | 110 | +          // eslint-disable-next-line no-console | 
|  | 111 | +          console.error( | 
|  | 112 | +            `DEBUG: [bazel-sandbox] corrected resolution ${result.path} does not exist in the sandbox, trying ${jsPath}.`, | 
|  | 113 | +          ); | 
|  | 114 | +        } | 
|  | 115 | +        result.path = jsPath; | 
|  | 116 | +      } | 
|  | 117 | +    } | 
|  | 118 | +  } | 
|  | 119 | + | 
|  | 120 | +  return result; | 
|  | 121 | +} | 
0 commit comments