Add GN build for dawn_node

Example:
```
gn gen out/Default --args='dawn_build_node_bindings=true'
autoninja -C out/Default dawn_node
```

Bug: chromium:404208023
Change-Id: I20f745406b83605bf7d0c5a62ea3cf4524cd5198
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/231214
Reviewed-by: James Price <jrprice@google.com>
Commit-Queue: Antonio Maiorano <amaiorano@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index 197d981..1af41fe 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -29,6 +29,8 @@
 import("scripts/dawn_overrides_with_defaults.gni")
 import("scripts/tint_overrides_with_defaults.gni")
 
+import("${dawn_root}/scripts/dawn_features.gni")
+
 if (!is_clang && is_win) {
   # libc++ cannot currently build with MSVC. See https://anglebug.com/376074941
   assert(!use_custom_libcxx, "MSVC build requires use_custom_libcxx=false")
@@ -52,6 +54,12 @@
     "src/dawn/native:webgpu_dawn",
     "src/tint:libs",
   ]
+  if (dawn_build_node_bindings) {
+    assert(
+        !defined(use_libfuzzer) || !use_libfuzzer,
+        "Building dawn node bindings with use_libfuzzer currently not supported")
+    deps += [ "src/dawn/node:dawn_node" ]
+  }
 }
 
 group("tests") {
diff --git a/scripts/dawn_features.gni b/scripts/dawn_features.gni
index 11d25c1..8f0bd1f 100644
--- a/scripts/dawn_features.gni
+++ b/scripts/dawn_features.gni
@@ -58,6 +58,9 @@
   # DXC requires SM6.0+ which is blocklisted on x86.
   # See crbug.com/tint/1753.
   dawn_use_built_dxc = is_win && target_cpu != "x86"
+
+  # Whether to build Dawn's NodeJS bindings
+  dawn_build_node_bindings = false
 }
 
 declare_args() {
diff --git a/src/dawn/node/BUILD.gn b/src/dawn/node/BUILD.gn
new file mode 100644
index 0000000..270a258
--- /dev/null
+++ b/src/dawn/node/BUILD.gn
@@ -0,0 +1,309 @@
+# Copyright 2025 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import("../../../scripts/dawn_overrides_with_defaults.gni")
+
+if (is_win) {
+  _exe = ".exe"
+} else {
+  _exe = ""
+}
+
+# Directory in which to find node-addon-api
+dawn_node_addon_api_dir = "$dawn_root/third_party/node-addon-api"
+
+# Directory in which to find node-api-headers
+dawn_node_api_headers_dir = "$dawn_root/third_party/node-api-headers"
+
+# Path to the webgpu.idl definition file
+dawn_webgpu_idl_path = "$dawn_root/third_party/gpuweb/webgpu.idl"
+
+# Golang executable for running the IDL generator
+dawn_go_executable = "$dawn_root/tools/golang/bin/go${_exe}"
+
+# TODO(amaiorano): Figure out why path_exists doesn't work
+# assert(path_exists(dawn_node_addon_api_dir) && path_exists(dawn_node_api_headers_dir) && path_exists(dawn_webgpu_idl_path), "dawn_build_node_bindings missing dependencies. Please follow the 'Fetch dependencies' instructions at: ./src/dawn/node/README.md")
+
+dawn_node_gen_dir = target_gen_dir
+interop_gen_dir = "$dawn_node_gen_dir/src/dawn/node/interop"
+
+# TODO(amaiorano): Move run_built_binary.py to a common dir
+run_built_binary_script =
+    "$dawn_root/third_party/gn/dxc/build/run_built_binary.py"
+
+config("common_config") {
+  warning_flags = []
+  if (is_clang) {
+    warning_flags += [
+      "-Wno-error",  # TODO(amaiorano): For now, allow warnings
+      "-Wno-extra-semi",
+      "-Wno-shadow",
+      "-Wno-unused-variable",
+      "-Wno-implicit-fallthrough",
+      "-Wno-unused-but-set-variable",
+    ]
+  } else {
+    # MSVC
+    warning_flags += [
+      "/W0",  # Suppress most warnings
+    ]
+  }
+
+  cflags_c = warning_flags
+  cflags_cc = warning_flags
+}
+
+template("idlgen") {
+  assert(defined(invoker.template), "must set 'template'")
+  assert(defined(invoker.output), "must set 'output'")
+  assert(defined(invoker.idls), "must set 'idls'")
+
+  action(target_name) {
+    idlgen_tool_dir = "$dawn_root/tools/src/cmd/idlgen"
+    inputs = [
+               "$idlgen_tool_dir/main.go",
+               invoker.template,
+             ] + invoker.idls
+    if (defined(invoker.depends)) {
+      inputs += invoker.depends
+    }
+    outputs = [ invoker.output ]
+
+    idls = []
+    foreach(i, invoker.idls) {
+      idls += [ rebase_path(i, root_build_dir) ]
+    }
+
+    script = run_built_binary_script
+    args = [
+             "",
+             rebase_path(dawn_go_executable, root_build_dir),
+             "run",
+             rebase_path("$idlgen_tool_dir/main.go", root_build_dir),
+             "--template",
+             rebase_path(invoker.template, root_build_dir),
+             "--output",
+             rebase_path(invoker.output, root_build_dir),
+           ] + idls
+  }
+}
+
+idlgen("idlgen_webgpu_h") {
+  template = "interop/WebGPU.h.tmpl"
+  idls = [
+    "interop/Browser.idl",
+    dawn_webgpu_idl_path,
+    "interop/DawnExtensions.idl",
+  ]
+  depends = [ "interop/WebGPUCommon.tmpl" ]
+  output = "$interop_gen_dir/WebGPU.h"
+}
+idlgen("idlgen_webgpu_cpp") {
+  template = "interop/WebGPU.cpp.tmpl"
+  idls = [
+    "interop/Browser.idl",
+    dawn_webgpu_idl_path,
+    "interop/DawnExtensions.idl",
+  ]
+  depends = [ "interop/WebGPUCommon.tmpl" ]
+  output = "$interop_gen_dir/WebGPU.cpp"
+}
+
+static_library("dawn_node_interop") {
+  public_configs = [ ":common_config" ]
+  sources = [
+    "$interop_gen_dir/WebGPU.cpp",
+    "$interop_gen_dir/WebGPU.h",
+    "interop/Core.cpp",
+    "interop/Core.h",
+  ]
+  include_dirs = [
+    "${dawn_node_addon_api_dir}",
+    "${dawn_node_api_headers_dir}/include",
+    "${dawn_node_gen_dir}",
+  ]
+  deps = [
+    ":idlgen_webgpu_cpp",
+    ":idlgen_webgpu_h",
+    "$dawn_root/include/dawn:cpp_headers",
+  ]
+}
+
+static_library("dawn_node_binding") {
+  public_configs = [ ":common_config" ]
+  if (is_win) {
+    # Use multibyte character set so that Win32 functions map the ANSI ones, not the UNICODE ones.
+    configs -= [ "//build/config/win:unicode" ]
+  }
+  sources = [
+    "binding/AsyncRunner.cpp",
+    "binding/AsyncRunner.h",
+    "binding/Converter.cpp",
+    "binding/Converter.h",
+    "binding/Errors.cpp",
+    "binding/Errors.h",
+    "binding/Flags.cpp",
+    "binding/Flags.h",
+    "binding/GPU.cpp",
+    "binding/GPU.h",
+    "binding/GPUAdapter.cpp",
+    "binding/GPUAdapter.h",
+    "binding/GPUAdapterInfo.cpp",
+    "binding/GPUAdapterInfo.h",
+    "binding/GPUBindGroup.cpp",
+    "binding/GPUBindGroup.h",
+    "binding/GPUBindGroupLayout.cpp",
+    "binding/GPUBindGroupLayout.h",
+    "binding/GPUBuffer.cpp",
+    "binding/GPUBuffer.h",
+    "binding/GPUCommandBuffer.cpp",
+    "binding/GPUCommandBuffer.h",
+    "binding/GPUCommandEncoder.cpp",
+    "binding/GPUCommandEncoder.h",
+    "binding/GPUComputePassEncoder.cpp",
+    "binding/GPUComputePassEncoder.h",
+    "binding/GPUComputePipeline.cpp",
+    "binding/GPUComputePipeline.h",
+    "binding/GPUDevice.cpp",
+    "binding/GPUDevice.h",
+    "binding/GPUPipelineLayout.cpp",
+    "binding/GPUPipelineLayout.h",
+    "binding/GPUQuerySet.cpp",
+    "binding/GPUQuerySet.h",
+    "binding/GPUQueue.cpp",
+    "binding/GPUQueue.h",
+    "binding/GPURenderBundle.cpp",
+    "binding/GPURenderBundle.h",
+    "binding/GPURenderBundleEncoder.cpp",
+    "binding/GPURenderBundleEncoder.h",
+    "binding/GPURenderPassEncoder.cpp",
+    "binding/GPURenderPassEncoder.h",
+    "binding/GPURenderPipeline.cpp",
+    "binding/GPURenderPipeline.h",
+    "binding/GPUSampler.cpp",
+    "binding/GPUSampler.h",
+    "binding/GPUShaderModule.cpp",
+    "binding/GPUShaderModule.h",
+    "binding/GPUSupportedFeatures.cpp",
+    "binding/GPUSupportedFeatures.h",
+    "binding/GPUSupportedLimits.cpp",
+    "binding/GPUSupportedLimits.h",
+    "binding/GPUTexture.cpp",
+    "binding/GPUTexture.h",
+    "binding/GPUTextureView.cpp",
+    "binding/GPUTextureView.h",
+    "binding/IteratorHelper.h",
+    "binding/Split.cpp",
+    "binding/Split.h",
+    "binding/TogglesLoader.cpp",
+    "binding/TogglesLoader.h",
+  ]
+  include_dirs = [
+    "${dawn_node_addon_api_dir}",
+    "${dawn_node_api_headers_dir}/include",
+    "${dawn_node_gen_dir}",
+  ]
+  deps = [
+    ":dawn_node_interop",
+    "$dawn_root/src/dawn/native:native",
+  ]
+}
+
+action("gen_napi_symbols") {
+  script = "gen_napi_symbols.py"
+  args = [ rebase_path("$dawn_root/third_party/node-api-headers/symbols.js",
+                       root_build_dir) ]
+  if (is_win) {
+    args +=
+        [ rebase_path("$dawn_node_gen_dir/NapiSymbols.def", root_build_dir) ]
+    outputs = [ "$dawn_node_gen_dir/NapiSymbols.def" ]
+  } else {
+    args += [ rebase_path("$dawn_node_gen_dir/NapiSymbols.h", root_build_dir) ]
+    outputs = [ "$dawn_node_gen_dir/NapiSymbols.h" ]
+  }
+}
+
+if (is_win) {
+  import("//build/config/clang/clang.gni")
+  action("napi_symbols_lib") {
+    inputs = [ "$dawn_node_gen_dir/NapiSymbols.def" ]
+    outputs = [ "$dawn_node_gen_dir/NapiSymbols.lib" ]
+    script = run_built_binary_script
+    linker = rebase_path("$clang_base_path/bin/lld-link${_exe}", root_build_dir)
+    args = [
+      "",
+      linker,
+      "/DEF:" + rebase_path(inputs[0], root_build_dir),
+      "/OUT:" + rebase_path(outputs[0], root_build_dir),
+    ]
+    deps = [ ":gen_napi_symbols" ]
+  }
+}
+
+copy("copy_index_js") {
+  sources = [ "$dawn_root/src/dawn/node/index.js" ]
+  outputs = [ "$root_build_dir/index.js" ]
+}
+copy("copy_cts_js") {
+  sources = [ "$dawn_root/src/dawn/node/cts.js" ]
+  outputs = [ "$root_build_dir/cts.js" ]
+}
+
+shared_library("dawn_node") {
+  output_name = "dawn"
+  output_extension = "node"
+  output_prefix_override = true  # dawn.node, not libdawn.node
+  sources = [ "Module.cpp" ]
+  deps = [
+    ":copy_cts_js",
+    ":copy_index_js",
+    ":dawn_node_binding",
+    ":gen_napi_symbols",
+    "$dawn_root/src/dawn:proc",
+  ]
+  if (is_win) {
+    deps += [ ":napi_symbols_lib" ]
+    ldflags =
+        [ rebase_path("$dawn_node_gen_dir/NapiSymbols.lib", root_build_dir) ]
+  } else {
+    # Don't pass '-z,defs' linker arg when compiling weak symbols
+    configs -= [ "//build/config/compiler:no_unresolved_symbols" ]
+
+    # TODO(crbug.com/404499678): GN build doesn't like when weak functions have a definition
+    defines = [ "NAPI_SYMBOL_WEAK_DECL_ONLY" ]
+    if (is_mac) {
+      # On mac, make undefined weak functions dynamic to bind with node functions at runtime
+      ldflags = [ "-Wl,-undefined,dynamic_lookup" ]
+    }
+    sources += [ "NapiSymbols.cpp" ]
+  }
+  include_dirs = [
+    "${dawn_node_addon_api_dir}",
+    "${dawn_node_api_headers_dir}/include",
+    "${dawn_node_gen_dir}",
+  ]
+}
diff --git a/src/dawn/node/NapiSymbols.cpp b/src/dawn/node/NapiSymbols.cpp
index a92ea2d..c5593e4 100644
--- a/src/dawn/node/NapiSymbols.cpp
+++ b/src/dawn/node/NapiSymbols.cpp
@@ -39,12 +39,17 @@
 #endif
 
 #ifdef __clang__
+// TODO(crbug.com/404499678): GN build doesn't like when weak functions have a definition
+#ifdef NAPI_SYMBOL_WEAK_DECL_ONLY
+#define NAPI_SYMBOL(NAME) __attribute__((weak)) void NAME();
+#else
 #define NAPI_SYMBOL(NAME)                                                    \
     __attribute__((weak)) void NAME() {                                      \
         UNREACHABLE(#NAME                                                    \
                     " is a weak stub, and should have been runtime replaced" \
                     " by the node implementation");                          \
     }
+#endif
 #else
 #define NAPI_SYMBOL(NAME)
 #endif
diff --git a/src/dawn/node/README.md b/src/dawn/node/README.md
index 0d6aa39..a0d3a58 100644
--- a/src/dawn/node/README.md
+++ b/src/dawn/node/README.md
@@ -6,7 +6,8 @@
 
 ### System requirements
 
-- [CMake 3.16](https://cmake.org/download/) or greater.
+- [GN](https://gn.googlesource.com/gn/) if using the GN build. This is installed as part of depot_tools (see below).
+- [CMake 3.16](https://cmake.org/download/) or greater, if using the CMake build.
   (Check `cmake_minimum_required` in the [CMakeLists.txt](../../../CMakeLists.txt)
   file in the project root.)
 - [Go 1.18](https://golang.org/dl/) or greater.
@@ -38,7 +39,15 @@
 
 ### Build
 
-Currently, the node bindings can only be built with CMake:
+#### With GN
+
+Set `dawn_build_node_bindings = true` in `args.gn` and build the `dawn_node` target:
+```sh
+gn gen out/Default --args='dawn_build_node_bindings=true'
+autoninja -C out/Default dawn_node
+```
+
+#### With CMake
 
 ```sh
 mkdir <build-output-path>
diff --git a/src/dawn/node/gen_napi_symbols.py b/src/dawn/node/gen_napi_symbols.py
new file mode 100644
index 0000000..29f16c8
--- /dev/null
+++ b/src/dawn/node/gen_napi_symbols.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python3
+
+# Copyright 2025 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import sys
+import re
+from pathlib import Path
+
+symbols_js_file = sys.argv[1]
+output_file = Path(sys.argv[2])
+
+with open(symbols_js_file, "r") as f:
+    matches = re.findall(r"napi_[a-z0-9_]*", f.read())
+
+if os.name == 'nt':
+    # Generate the NapiSymbols.def file from the Napi symbol list
+    assert (output_file.suffix == ".def")
+    with open(output_file, "w") as f:
+        f.write("LIBRARY node.exe\n")
+        f.write("EXPORTS\n")
+        for symbol in matches:
+            f.write(f"  {symbol}\n")
+else:
+    # Generate the NapiSymbols.h file from the Napi symbol list
+    assert (output_file.suffix == ".h")
+    with open(output_file, "w") as f:
+        matches2 = [f"NAPI_SYMBOL({symbol})" for symbol in matches]
+        f.write("\n".join(matches2))