docs: add documentation on how to enable path mapping (#2966)

---

### Changes are visible to end-users: no

### Test plan
diff --git a/docs/faq.md b/docs/faq.md
index 0662801..5230b42 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -130,3 +130,7 @@
 Note that when following option 2, it might require updating some configuration files which refer to the original output locations. For example, your `tsconfig.json` file might have a `paths` section which points to the `../../dist` folder.
 
 To keep your legacy build system working during the migration, you might want to avoid changing those configuration files in-place. For this purpose, you can use [the `jq` rule](https://registry.bazel.build/docs/jq.bzl#function-jq) in place of `copy_to_bin`, using a `filter` expression so the copy of the configuration file in `bazel-bin` that's used by the Bazel build can have a different path than the configuration file in the source tree.
+
+## Does rules_js support path mapping?
+
+Yes, see the [path mapping guide](./path_mapping.md).
diff --git a/docs/path_mapping.md b/docs/path_mapping.md
new file mode 100644
index 0000000..42d69e2
--- /dev/null
+++ b/docs/path_mapping.md
@@ -0,0 +1,57 @@
+# Path mapping
+
+It is not unusual for Bazel build actions for different compilation modes to be
+substantively identical, and yet unable to share a cached result due to
+superficial differences in output paths. For example, a `fastbuild` result
+might land in `bazel-out/k8-fastbuild/bin` whereas an `opt` result lands in
+`bazel-out/k8-opt/bin`. This can result in a lot of wasted work, with Bazel
+needlessly repeating the same action for each compilation mode even when there
+is no real difference in the work being done.
+
+Fortunately, Bazel supports a feature called [path
+mapping](https://github.com/bazelbuild/bazel/discussions/22658) that addresses
+this issue. When path mapping is active, Bazel temporarily collapses all bin
+directories into a single one called `bazel-out/cfg/bin` during the action.
+This way, build actions for different compilation modes or even CPU platforms
+can share a cached result, provided that the actions are otherwise identical.
+
+## Enabling path mapping
+
+Getting path mapping set up requires both enabling it via command-line flags at
+the Bazel level and then ensuring that specific targets are compatible with it.
+
+At the Bazel level:
+ - Pass the `--experimental_output_paths=strip` flag to Bazel
+ - Use either remote caching or `--disk_cache`. Counterintuitively, Bazel's
+   default caching will otherwise not benefit from path mapping.
+
+The `js_run_binary` macro will automatically enable path mapping if it
+determines it is safe to do so, but you may need to explicitly opt in (see
+below). To set up a `js_run_binary` target for path mapping:
+ - Pass `set_legacy_environment_variables = False`. This will disable the
+   setting of environment variables such as `BAZEL_COMPILATION_MODE` that would
+   otherwise be problematic.
+ - Avoid using [Make
+   variables](https://bazel.build/reference/be/make-variables) or expressions
+   such as `$(execpath :target)`. Such variables and expressions are not
+   path-mapping-aware and may be unsafe.
+ - If you need to use a Make variable or an expression such as `$(rootpath
+   :target)` but you know that it is safe with respect to path mapping, you can
+   explicitly opt in with `execution_requirements = {"supports-path-mapping":
+   "1"}`. This is fine as long as you know the expression will not include
+   `bazel-out/<platform dir>`, which would be incorrect when path mapping is
+   active.
+
+For custom rules that invoke a `js_binary`:
+ - Use the helper function `js_binary_lib.run_binary_action()` from
+   `@aspect_rules_js//js:libs.bzl`. This is a thin wrapper around
+   `ctx.actions.run()` that handles some internal implementation details in a
+   path-mapping-friendly way.
+ - Set `execution_requirements = {"supports-path-mapping": "1"}` to opt into
+   path mapping.
+ - Make sure that any paths involving `bazel-out/` are mapped correctly. The
+   only way to do this is to call `args.add()` or `args.add_all()` on an
+   [Args](https://bazel.build/rules/lib/builtins/Args) object from
+   `ctx.actions.args()`. If you need to do any additional munging of the path,
+   it must be done in a `map_each` callback passed to
+   [args.add_all()](https://bazel.build/rules/lib/builtins/Args#add_all).
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index 3d49211..813fc0d 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -197,6 +197,13 @@
 )
 ```
 
+### Path mapping
+
+Some build actions do essentially the same thing regardless of the compilation
+mode (e.g. `opt` or `dbg`), but for technical reasons Bazel will not allow
+cached results to be shared across different modes by default. See the [path
+mapping guide](./path_mapping.md) for how you can address this problem.
+
 #### Jest
 
 See [rules_jest](https://github.com/aspect-build/rules_jest) for troubleshooting.