Repository: remix-run/remix (ts)

Issue:
`fetch(...) instanceof Response` fails after installGlobals() 
### What version of Remix are you using?

1.19

### Are all your remix dependencies & dev-dependencies using the same version?

- [X] Yes

### Steps to Reproduce

Many libraries depend `resp instanceof Response` to return true; https://github.com/panva/oauth4webapi/blob/170f44e2f7c7da60feb8a654871c56fc0a741e6b/src/index.ts#L1513 is one of such a library. However, after Remix patches the global `fetch` instance, this no longer holds.

I've  tested this with the following three scenarios:

1. vanilla environment in node 20 (works)
2. remix-run/web-fetch (works)
3. remix-run/node (doesn't work)

```
{
  // correct behavior
  console.log((await fetch("http://example.com")) instanceof Response);
}
{
  // also correct behavior
  const i = await import("@remix-run/web-fetch");
  console.log((await i.fetch("http://example.com")) instanceof i.Response);
}
{
  // incorrect behavior; fails.
  const j = await import("@remix-run/node");
  console.log((await j.fetch("http://example.com")) instanceof j.Response);
}
```

The last one is important, as this is the version of globals that's installed via `installGlobals`.

### Expected Behavior

`(await fetch(...)) instanceof Response` should return `true` for fetch and Response exposed by `remix-run/node`.

### Actual Behavior

as the exposed types from `remix-run/node` is `NodeResponse`, the `instanceof` fails.


### ❓ Question 

What I don't particularly understand is why we have the `NodeRequest` and `NodeResponse`. This is the reason why it's causing this type mismatch issue; these classes seem to be serving very little purpose. We're actually not instantiating these types at all, most of the (non-test) code is using `... as NodeResponse` to coerce types. Can we just eliminate them, and expose `@remix-run/web-fetch` classes and types directly?
fetch() doesn't return correct Response type
### What version of Remix are you using?

1.7.2

### Steps to Reproduce

Inside the remix project try

```javascript
const a = await fetch('https://google.com')
console.log('a instanceof Response: ', a instanceof Response) // -> false
```

This probably has to do with this commit: 
https://github.com/remix-run/remix/commit/25b4c83b10c5bc4d84ea81407e7de1a0e4f43df4#diff-87ec1c3c130fe3481b9f1c542b1d8a53548882d33c73c68b894fd442d606afc0

A current workaround I found thanks to @milamer is to include 
```javascript
import { Response } from '@remix-run/web-fetch'
```
in the file.

### Expected Behavior

instanceof Response should be true

### Actual Behavior

instanceof Response is false

Pull request description:
feat(remix-node): replace extended classes for `NodeRequest` & `NodeResponse` with interface type casts
Retarget to dev of https://github.com/remix-run/remix/pull/7069, credit to @jeeyoungk

Closes #3480
Closes #4148
Closes #4395
Closes #7059

This is a fix for the referenced issue; The fix is by

* Replace `NodeRequest` and `NodeResponse` classes with interfaces.
* However, class is still needed (as people may want to do `new NodeRequest` or `x instanceof NodeRequest`). For this purpose, `WebRequest` and `WebResponse` are exposed as classes.

This code is quite tricky as we're exporting types and values separately. The correct way of doing this is to define a type and value with the same identifier, `export` handles this correctly.

- [ ] Docs
- [x] Tests

Testing Strategy: new integration and unit tests


Patch:
diff --git a/.changeset/global-fetch-instanceof.md b/.changeset/global-fetch-instanceof.md
new file mode 100644
index 000000000..b9dff6755
--- /dev/null
+++ b/.changeset/global-fetch-instanceof.md
@@ -0,0 +1,5 @@
+---
+"@remix-run/node": patch
+---
+
+ensures fetch() return is instanceof global Response by removing extended classes for NodeRequest and NodeResponse in favor of custom interface type cast.
diff --git a/contributors.yml b/contributors.yml
index 91a7e07bb..ae7de85f8 100644
--- a/contributors.yml
+++ b/contributors.yml
@@ -230,6 +230,7 @@
 - jaydiablo
 - jca41
 - jdeniau
+- jeeyoungk
 - JeffBeltran
 - jenseng
 - jeremyjfleming
diff --git a/packages/remix-node/fetch.ts b/packages/remix-node/fetch.ts
index b4182a055..1047e998a 100644
--- a/packages/remix-node/fetch.ts
+++ b/packages/remix-node/fetch.ts
@@ -9,6 +9,7 @@ export { FormData } from "@remix-run/web-fetch";
 export { File, Blob } from "@remix-run/web-file";
 
 type NodeHeadersInit = ConstructorParameters<typeof WebHeaders>[0];
+type NodeResponseInfo = ConstructorParameters<typeof WebResponse>[0];
 type NodeResponseInit = NonNullable<
   ConstructorParameters<typeof WebResponse>[1]
 >;
@@ -31,30 +32,27 @@ export type {
   NodeResponseInit as ResponseInit,
 };
 
-class NodeRequest extends WebRequest {
-  constructor(info: NodeRequestInfo, init?: NodeRequestInit) {
-    super(info, init as RequestInit);
-  }
+interface NodeRequest extends WebRequest {
+  get headers(): WebHeaders;
 
-  public get headers(): WebHeaders {
-    return super.headers as WebHeaders;
-  }
-
-  public clone(): NodeRequest {
-    return new NodeRequest(this);
-  }
+  clone(): NodeRequest;
 }
 
-class NodeResponse extends WebResponse {
-  public get headers(): WebHeaders {
-    return super.headers as WebHeaders;
-  }
+interface NodeResponse extends WebResponse {
+  get headers(): WebHeaders;
 
-  public clone(): NodeResponse {
-    return super.clone() as NodeResponse;
-  }
+  clone(): NodeResponse;
 }
 
+const NodeRequest = WebRequest as new (
+  info: NodeRequestInfo,
+  init?: NodeRequestInit
+) => NodeRequest;
+const NodeResponse = WebResponse as unknown as new (
+  info: NodeResponseInfo,
+  init?: NodeResponseInit
+) => NodeResponse;
+
 export {
   WebHeaders as Headers,
   NodeRequest as Request,


Test patch:
diff --git a/integration/fetch-globals-test.ts b/integration/fetch-globals-test.ts
new file mode 100644
index 000000000..172602c8f
--- /dev/null
+++ b/integration/fetch-globals-test.ts
@@ -0,0 +1,39 @@
+import { test, expect } from "@playwright/test";
+
+import type { Fixture, AppFixture } from "./helpers/create-fixture";
+import { createAppFixture, createFixture, js } from "./helpers/create-fixture";
+
+let fixture: Fixture;
+let appFixture: AppFixture;
+
+test.beforeAll(async () => {
+  fixture = await createFixture({
+    files: {
+      "app/routes/_index.tsx": js`
+        import { json } from "@remix-run/node";
+        import { useLoaderData } from "@remix-run/react";
+        export async function loader() {
+          const resp = await fetch('https://reqres.in/api/users?page=2');
+          return (resp instanceof Response) ? 'is an instance of global Response' : 'is not an instance of global Response';
+        }
+        export default function Index() {
+          let data = useLoaderData();
+          return (
+            <div>
+              {data}
+            </div>
+          )
+        }
+      `,
+    },
+  });
+
+  appFixture = await createAppFixture(fixture);
+});
+
+test.afterAll(async () => appFixture.close());
+
+test("returned variable from fetch() should be instance of global Response", async () => {
+  let response = await fixture.requestDocument("/");
+  expect(await response.text()).toMatch("is an instance of global Response");
+});
diff --git a/packages/remix-node/__tests__/fetch-test.ts b/packages/remix-node/__tests__/fetch-test.ts
index 6f7c84806..e8fac2dff 100644
--- a/packages/remix-node/__tests__/fetch-test.ts
+++ b/packages/remix-node/__tests__/fetch-test.ts
@@ -1,6 +1,10 @@
 import { ReadableStream } from "@remix-run/web-stream";
+import {
+  Request as WebRequest,
+  Response as WebResponse,
+} from "@remix-run/web-fetch";
 
-import { Request } from "../fetch";
+import { Request, Response } from "../fetch";
 
 let test = {
   source: [
@@ -111,6 +115,22 @@ describe("Request", () => {
     expect(file.size).toBe(1023);
 
     expect(cloned instanceof Request).toBeTruthy();
+    expect(cloned instanceof WebRequest).toBeTruthy();
+  });
+
+  it("instanceOf", async () => {
+    let nodeReq = new Request("http://example.com");
+    let webReq = new WebRequest("http://example.com");
+    let nodeRes = new Response("http://example.com");
+    let webRes = new WebResponse("http://example.com");
+    expect(nodeReq instanceof Request).toBeTruthy();
+    expect(nodeReq instanceof WebRequest).toBeTruthy();
+    expect(webReq instanceof Request).toBeTruthy();
+    expect(webReq instanceof WebRequest).toBeTruthy();
+    expect(nodeRes instanceof Response).toBeTruthy();
+    expect(nodeRes instanceof WebResponse).toBeTruthy();
+    expect(webRes instanceof Response).toBeTruthy();
+    expect(webRes instanceof WebResponse).toBeTruthy();
   });
 });