feat(builtin): expose a concrete toolchain for rules that don't under… (#2960)

diff --git a/e2e/core/BUILD.bazel b/e2e/core/BUILD.bazel
index 7703257..6b991ce 100644
--- a/e2e/core/BUILD.bazel
+++ b/e2e/core/BUILD.bazel
@@ -9,6 +9,12 @@
     content = ["require('fs').writeFileSync(process.argv[2], 'stuff')"],
 )
 
+write_file(
+    name = "write_expected",
+    out = "expected",
+    content = ["stuff"],
+)
+
 # This technique can be used to directly grab a node binary as a label, however it has the
 # downside that analysis phase on this select() statement will cause an eager fetch of all
 # the platforms and therefore download a bunch of node binaries.
@@ -34,37 +40,23 @@
 #     tools = [":node_bin"],
 # )
 
-# In theory, you can use the node toolchain together with a genrule().
-# However the genrule implementation doesn't perform toolchain resolution.
-# See this comment from Jay Conrod about a similar question for rules_go
-# https://github.com/bazelbuild/rules_go/issues/2255#issuecomment-545478712
-# That means you must resolve the toolchain yourself, with a select()
-# and that falls down the same deoptimization described above:
-# it will eager-fetch node for all platforms.
-# So instead we recommend always writing a custom rule to access the node binary.
-# alias(
-#     name = "node_toolchain",
-#     actual = select({
-#         "@bazel_tools//src/conditions:darwin_arm64": "@node16_darwin_arm64//:node_toolchain",
-#         "@bazel_tools//src/conditions:darwin_x86_64": "@node16_darwin_amd64//:node_toolchain",
-#         "@bazel_tools//src/conditions:linux_aarch64": "@node16_linux_arm64//:node_toolchain",
-#         "@bazel_tools//src/conditions:linux_s390x": "@node16_linux_s390x//:node_toolchain",
-#         "@bazel_tools//src/conditions:linux_x86_64": "@node16_linux_amd64//:node_toolchain",
-#         "@bazel_tools//src/conditions:linux_ppc64le": "@node16_linux_ppc64le//:node_toolchain",
-#         "@bazel_tools//src/conditions:windows": "@node16_windows_amd64//:node_toolchain",
-#         "//conditions:default": "@node16_linux_amd64//:node_toolchain",
-#     }),
-# )
-# genrule(
-#     name = "use_node_toolchain",
-#     srcs = ["some.js"],
-#     outs = ["thing2"],
-#     cmd = "$(NODE_PATH) $(execpath some.js) $@",
-#     toolchains = [":node_toolchain"],
-#     # It will also fail to include the files from the node_toolchain, so you're
-#     # forced to repeat the label of the node binary as an explicit input.
-#     tools = ["@node16_host//:node_bin"],
-# )
+# You can use the node toolchain together with a genrule().
+# This gives you complete control over starting the interpreter, but you also have to
+# manually handle module resolution.
+genrule(
+    name = "use_node_toolchain",
+    srcs = ["some.js"],
+    outs = ["genrule_out"],
+    cmd = "$(NODE_PATH) $(execpath some.js) $@",
+    toolchains = ["@node16_toolchains//:resolved_toolchain"],
+    tools = ["@node16_toolchains//:resolved_toolchain"],
+)
+
+diff_test(
+    name = "test_genrule",
+    file1 = "expected",
+    file2 = "genrule_out",
+)
 
 # Here, my_nodejs is a fake for something like nodejs_binary or
 # some other custom rule that runs node.
@@ -75,15 +67,8 @@
 )
 
 # Assert that the node program wrote the file we expect
-write_file(
-    name = "write_expected",
-    out = "expected",
-    content = ["stuff"],
-)
-
 diff_test(
-    name = "test",
+    name = "test_custom_rule",
     file1 = "expected",
     file2 = "thing",
 )
-# end Assert
diff --git a/nodejs/private/toolchains_repo.bzl b/nodejs/private/toolchains_repo.bzl
index 3e731f5..9dcdc60 100644
--- a/nodejs/private/toolchains_repo.bzl
+++ b/nodejs/private/toolchains_repo.bzl
@@ -62,12 +62,42 @@
     ),
 }
 
-def _impl(repository_ctx):
+def _toolchains_repo_impl(repository_ctx):
+    # Expose a concrete toolchain which is the result of Bazel resolving the toolchain
+    # for the execution or target platform.
+    # Workaround for https://github.com/bazelbuild/bazel/issues/14009
+    starlark_content = """# Generated by toolchains_repo.bzl
+
+# Forward all the providers
+def _resolved_toolchain_impl(ctx):
+    toolchain_info = ctx.toolchains["@rules_nodejs//nodejs:toolchain_type"]
+    return [
+        toolchain_info,
+        toolchain_info.default,
+        toolchain_info.nodeinfo,
+        toolchain_info.template_variables,
+    ]
+
+# Copied from java_toolchain_alias
+# https://cs.opensource.google/bazel/bazel/+/master:tools/jdk/java_toolchain_alias.bzl
+resolved_toolchain = rule(
+    implementation = _resolved_toolchain_impl,
+    toolchains = ["@rules_nodejs//nodejs:toolchain_type"],
+    incompatible_use_toolchain_transition = True,
+)
+"""
+    repository_ctx.file("defs.bzl", starlark_content)
+
     build_content = """# Generated by toolchains_repo.bzl
 #
 # These can be registered in the workspace file or passed to --extra_toolchains flag.
 # By default all these toolchains are registered by the nodejs_register_toolchains macro
 # so you don't normally need to interact with these targets.
+
+load(":defs.bzl", "resolved_toolchain")
+
+resolved_toolchain(name = "resolved_toolchain", visibility = ["//visibility:public"])
+
 """
 
     for [platform, meta] in PLATFORMS.items():
@@ -90,7 +120,7 @@
     repository_ctx.file("BUILD.bazel", build_content)
 
 toolchains_repo = repository_rule(
-    _impl,
+    _toolchains_repo_impl,
     doc = """Creates a repository with toolchain definitions for all known platforms
      which can be registered or selected.""",
     attrs = {
diff --git a/nodejs/toolchain.bzl b/nodejs/toolchain.bzl
index b48f432..f8beb6d 100644
--- a/nodejs/toolchain.bzl
+++ b/nodejs/toolchain.bzl
@@ -45,23 +45,31 @@
         tool_files = ctx.attr.target_tool.files.to_list()
         target_tool_path = _to_manifest_path(ctx, tool_files[0])
 
+    # Make the $(NODE_PATH) variable available in places like genrules.
+    # See https://docs.bazel.build/versions/main/be/make-variables.html#custom_variables
+    template_variables = platform_common.TemplateVariableInfo({
+        "NODE_PATH": target_tool_path,
+    })
+    default = DefaultInfo(
+        files = depset(tool_files),
+        runfiles = ctx.runfiles(files = tool_files),
+    )
+    nodeinfo = NodeInfo(
+        target_tool_path = target_tool_path,
+        tool_files = tool_files,
+    )
+
+    # Export all the providers inside our ToolchainInfo
+    # so the resolved_toolchain rule can grab and re-export them.
+    toolchain_info = platform_common.ToolchainInfo(
+        nodeinfo = nodeinfo,
+        template_variables = template_variables,
+        default = default,
+    )
     return [
-        DefaultInfo(
-            runfiles = ctx.runfiles(files = tool_files),
-        ),
-        platform_common.ToolchainInfo(
-            nodeinfo = NodeInfo(
-                target_tool_path = target_tool_path,
-                tool_files = tool_files,
-            ),
-        ),
-        # Make the $(NODE_PATH) variable available in places like genrules.
-        # See https://docs.bazel.build/versions/main/be/make-variables.html#custom_variables
-        # Note that genrule seems to have a bug: this only works if the node_toolchain target
-        # itself is used by the toolchains attribute of genrule, not the toolchain_type.
-        platform_common.TemplateVariableInfo({
-            "NODE_PATH": target_tool_path,
-        }),
+        default,
+        toolchain_info,
+        template_variables,
     ]
 
 node_toolchain = rule(