aspect-build / rules_esbuild

Bazel rules for https://esbuild.github.io/ JS bundler
https://docs.aspect.build/rules/aspect_rules_esbuild
Apache License 2.0
27 stars 27 forks source link

fix: remove module cache in sandbox plugin #209

Closed Strum355 closed 5 months ago

Strum355 commented 6 months ago

This module cache came up as problematic with the presence of multiple versions of a package in a given dependency closure. In our case, we have minipass 4.2.8 and 7.0.4 (required by glob 9.3.2 and path-scurry 1.10.1 respectively) in a given closure. These are strictly not compatible with each other, so resolving minipass from glob should yield 4.2.8 and from path-scurry should yield 7.0.4. The module cache however results in everything getting the same version, as we are keying only by name sans version.

Log output I added to illustrate the point:

DEBUG NOAH BEGIN: 
        IMPORTER /home/noah/.cache/bazel/_bazel_noah/8fd1d20666a46767e7f29541678514a0/sandbox/linux-sandbox/10/execroot/__main__/bazel-out/k8-fastbuild/bin/node_modules/.aspect_rules_js/glob@9.3.2/node_modules/glob/dist/mjs/walker.js
DEBUG NOAH RETURN RESOLVE: 
        IMPORTER /home/noah/.cache/bazel/_bazel_noah/8fd1d20666a46767e7f29541678514a0/sandbox/linux-sandbox/10/execroot/__main__/bazel-out/k8-fastbuild/bin/node_modules/.aspect_rules_js/glob@9.3.2/node_modules/glob/dist/mjs/walker.js
        RESOLVED /home/noah/.cache/bazel/_bazel_noah/8fd1d20666a46767e7f29541678514a0/sandbox/linux-sandbox/10/execroot/__main__/bazel-out/k8-fastbuild/bin/node_modules/.aspect_rules_js/minipass@4.2.8/node_modules/minipass/index.mjs
DEBUG NOAH BEGIN: 
        IMPORTER /home/noah/.cache/bazel/_bazel_noah/8fd1d20666a46767e7f29541678514a0/sandbox/linux-sandbox/10/execroot/__main__/bazel-out/k8-fastbuild/bin/node_modules/.aspect_rules_js/path-scurry@1.10.1/node_modules/path-scurry/dist/mjs/index.js
DEBUG NOAH RETURN WOULD-BE CACHE: 
        IMPORTER /home/noah/.cache/bazel/_bazel_noah/8fd1d20666a46767e7f29541678514a0/sandbox/linux-sandbox/10/execroot/__main__/bazel-out/k8-fastbuild/bin/node_modules/.aspect_rules_js/path-scurry@1.10.1/node_modules/path-scurry/dist/mjs/index.js
        RESOLVED /home/noah/.cache/bazel/_bazel_noah/8fd1d20666a46767e7f29541678514a0/sandbox/linux-sandbox/10/execroot/__main__/bazel-out/k8-fastbuild/bin/node_modules/.aspect_rules_js/minipass@4.2.8/node_modules/minipass/index.mjs
DEBUG NOAH RETURN RESOLVE: 
        IMPORTER /home/noah/.cache/bazel/_bazel_noah/8fd1d20666a46767e7f29541678514a0/sandbox/linux-sandbox/10/execroot/__main__/bazel-out/k8-fastbuild/bin/node_modules/.aspect_rules_js/path-scurry@1.10.1/node_modules/path-scurry/dist/mjs/index.js
        RESOLVED /home/noah/.cache/bazel/_bazel_noah/8fd1d20666a46767e7f29541678514a0/sandbox/linux-sandbox/10/execroot/__main__/bazel-out/k8-fastbuild/bin/node_modules/.aspect_rules_js/minipass@7.0.4/node_modules/minipass/dist/esm/index.js

from the following diff to 0.19.0:

diff --git a/esbuild/private/plugins/bazel-sandbox.js b/esbuild/private/plugins/bazel-sandbox.js
index 84f0921..32ccaa7 100644
--- a/esbuild/private/plugins/bazel-sandbox.js
+++ b/esbuild/private/plugins/bazel-sandbox.js
@@ -28,15 +28,27 @@ function bazelSandboxPlugin() {
           }
           otherOptions.pluginData.executedSandboxPlugin = true

+          if (importPath === 'minipass') {
+            console.error(`DEBUG NOAH BEGIN: \n\tIMPORTER ${otherOptions.importer}`)
+          }
+
           // Prevent us from loading different forms of a module (CJS vs ESM).
           if (pkgImport.test(importPath)) {
-            if (!moduleCache.has(importPath)) {
+            const isNotCached = !moduleCache.has(importPath)
+            if (isNotCached) {
               moduleCache.set(
                 importPath,
                 resolveInExecroot(build, importPath, otherOptions)
               )
             }
-            return await moduleCache.get(importPath)
+            const res = await moduleCache.get(importPath)
+            if (importPath === 'minipass' && !isNotCached) {
+              console.error(`DEBUG NOAH RETURN WOULD-BE CACHE: \n\tIMPORTER ${otherOptions.importer}\n\tRESOLVED ${res.path}`)
+            }
+            // only for log output clarity
+            if (isNotCached) {
+              return res
+            }
           }
           return await resolveInExecroot(build, importPath, otherOptions)
         }
@@ -62,6 +74,14 @@ async function resolveInExecroot(build, importPath, otherOptions) {
     return result
   }

+  const res = correctImportPath(result, otherOptions, false)
+  if (importPath ==='minipass') {
+    console.error(`DEBUG NOAH RETURN RESOLVE: \n\tIMPORTER ${otherOptions.importer}\n\tRESOLVED ${res.path}`)
+  }
+  return res
+}
+
+function correctImportPath(result, otherOptions, firstEntry) {
   // If esbuild attempts to leave the execroot, map the path back into the execroot.
   if (!result.path.startsWith(execroot)) {
     // If it tried to leave bazel-bin, error out completely.

The one thing Im unsure about is the comment about avoiding loading both ESM and CJS, Im not sure if theres an easy reproducer somewhere or a test case in this repo that covers it cc @vpanta


Test plan

This issue was being worked on as part of a series of issues that cropped up when upgrading to 0.19.0, including https://github.com/aspect-build/rules_esbuild/pull/201. As such, the manual testing I did was the same as mentioned over there, except for a different commit: github.com/sourcegraph/sourcegraph/commit/51503969643612665485aa8e6e150566b6863d54 with target //client/web/dev:esbuild-config-production_bundle

jbedard commented 5 months ago

Have you been able to reproduce this in a test at all? Or any understandable way in your repo that you can describe here?

jbedard commented 5 months ago

@vpanta can you take a look at this and maybe try it out? I believe you had a reason for this cache originally in https://github.com/aspect-build/rules_esbuild/pull/160

vpanta commented 5 months ago

So just for some context, this plugin was originally something I created internally for Fullstory and decided to upstream. The module cache was added because certain modules were getting dual-loaded by esbuild depending on their import-semantics: if it came from a require, it would load the CJS version of the module, and if it was from an import statement, it would load the ESM version of the module. If I remember correctly, this was at best causing bloat in our bundle but at worst causing full breakages (when something like react would double-load). Generally FWICT it was caused by certain modules having only one form and then, when transitively loading the module A via module B's lone CJS form, module A would be duplicated as CJS.

I don't think I have explicit tests for this case, as it was something inherent to our internal build.

Take all this with a grain of salt, YMMV. I'm not sure of the best way to approach this to be honest, because your case is perfectly valid.

Strum355 commented 5 months ago

Thanks for the reply 🙏

Copying from my conversation with Jason:

on what I believe is the last error Im coming across, Im getting a quite confusing one:


✘ [ERROR] Could not read from file: /home/noah/.cache/bazel/_bazel_noah/8fd1d20666a46767e7f29541678514a0/sandbox/linux-sandbox/16/execroot/__main__/bazel-out/k8-fastbuild/bin/node_modules/.aspect_rules_js/object-inspect@1.13.1/node_modules/object-inspect/util.inspect
 node_modules/.aspect_rules_js/object-inspect@1.13.1/node_modules/object-inspect/index.js:68:26:
   68 │ var utilInspect = require('./util.inspect');
      ╵                           ~~~~~~~~~~~~~~~~
> The weird part is that `<snip>/node_modules/.aspect_rules_js/object-inspect@1.13.1/node_modules/object-inspect/util.inspect.js` (with a .js suffix) does exist, and adding the following to the bazel-sandbox plugin makes the build succeed
```js
           const res = await resolveInExecroot(build, importPath, otherOptions)
           if (importPath.includes('util.inspect')) {
             res.path = res.path + ".js"
           }

No idea how to explain this

Does this sound similar to the issue you came across that you solved with the module cache? @vpanta

vpanta commented 5 months ago

First off, sorry, I saw this question and completely forgot about it. But yeah, that actually looks like some kind of ESM/CJS incompatibility? Because ESM requires file extensions but CJS can appropriately assume them. I dunno why a require call would fail without a file extension :(

Like my only thought is esbuild is applying ESM rules to what should be a CJS package.

jbedard commented 5 months ago

I think I'd like to merge this then. @Strum355 did you have any thoughts on a test for this or is that not worth your time right now?