Repository: remix-run/remix
Pull request #3626 (2022-06-30 20:09:21)

fix(remix-express,remix-vercel): fix `request.signal`
This PR fixes the signal we assign to our Fetch `Request` in Express + Vercel.  We used to abort the signal on the `close` event on the Express/Vercel `Request`, but in `POST`'s this happens to be as soon as we've consumed the body, so any async flow in an `action` causes the signal to be aborted:

```js
export async function action({ request }) {
    await new Promise(r => setTimeout(r, 0));
    console.log(request.signal.aborted); // This is true every time
    ...
}
```

Instead we should we aborting our server-side processing of the request as soon as we are no longer able to send a `Response`, so we can listen for the Express/Vercel `Response` `close` event instead.


**Open Questions:** 
1. I added an integration test to assert that the `signal` is not immediately aborted, but curious if we have a way to assert that it's correctly aborted if the `response` closes?  
   * Jacob and I manually tested this by sending a `fetch` request from the browser and manually triggering its `AbortController` - and then checking the console logs on the Remix server.  Unsure if we can get at that level of info via our integration harness?
2. Other runtimes
   * CF + Deno provide a `signal` on the incoming `request` so we rely on them to trigger abort the signal properly on their end
   * Arc/Netlify don't seem to have any `signal` available, and we do not create one so `request.signal == null` inside our Remix code.  This will potentially be problematic when we pull in the new router since it will be relying on the existing signal.  We can either just make our references defensive via `request.signal?.aborted`, but it would be better if we could find some way to wire up a legit `signal`?  
     * I chose to create a `signal` (which will never be aborted) for these and added an `invariant(request.signal)` in `createStaticHandler.query` to better align with the `Request` typings


brophdawg11: Hm - seems to be failing CI on a `yarn install` - will try kicking off a new build tomorrow:

```
error An unexpected error occurred: "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.4.4.tgz: Request failed \"404 Not Found\"".
```

Patch:
diff --git a/.changeset/four-numbers-end.md b/.changeset/four-numbers-end.md
deleted file mode 100644
index 656ca11a2..000000000
--- a/.changeset/four-numbers-end.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@remix-run/server-runtime": patch
----
-
-Improved performance for data serialization at runtime
diff --git a/.changeset/four-poets-stare.md b/.changeset/four-poets-stare.md
deleted file mode 100644
index 87c9541eb..000000000
--- a/.changeset/four-poets-stare.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@remix-run/dev": minor
----
-
-Added support for importing `.gql` and `.graphql` files as plain text
diff --git a/.changeset/gold-mice-pretend.md b/.changeset/gold-mice-pretend.md
deleted file mode 100644
index 9bb2fa361..000000000
--- a/.changeset/gold-mice-pretend.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-"remix": patch
-"@remix-run/dev": patch
----
-
-Removed our compiler's React shim in favor of esbuild's new automatic JSX transform
diff --git a/.changeset/green-crabs-join.md b/.changeset/green-crabs-join.md
deleted file mode 100644
index bd151f063..000000000
--- a/.changeset/green-crabs-join.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@remix-run/dev": minor
----
-
-Added support for importing `.zip` and `.avif` files as resource URLs
diff --git a/.changeset/late-jars-wonder.md b/.changeset/late-jars-wonder.md
new file mode 100644
index 000000000..dbfcaa144
--- /dev/null
+++ b/.changeset/late-jars-wonder.md
@@ -0,0 +1,5 @@
+---
+"@remix-run/react": patch
+---
+
+fix: GET form submissions should replace current search params (#4046)
diff --git a/.changeset/neat-beds-unite.md b/.changeset/neat-beds-unite.md
deleted file mode 100644
index 56e03ea79..000000000
--- a/.changeset/neat-beds-unite.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
-"remix": minor
-"@remix-run/cloudflare": minor
-"@remix-run/deno": minor
-"@remix-run/node": minor
-"@remix-run/react": minor
-"@remix-run/serve": minor
-"@remix-run/server-runtime": minor
----
-
-We've added a new type: `SerializeFrom`. This is used to infer the
-JSON-serialized return type of loaders and actions.
diff --git a/.changeset/red-apples-stew.md b/.changeset/red-apples-stew.md
deleted file mode 100644
index 1e8c25494..000000000
--- a/.changeset/red-apples-stew.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@remix-run/dev": patch
----
-
-Fixed a bug in the `import` to `require` conversion in the CLI's `.ts` to `.js` migration script
diff --git a/.changeset/smooth-buses-approve.md b/.changeset/smooth-buses-approve.md
new file mode 100644
index 000000000..ae7f798c1
--- /dev/null
+++ b/.changeset/smooth-buses-approve.md
@@ -0,0 +1,8 @@
+---
+"@remix-run/architect": patch
+"@remix-run/express": patch
+"@remix-run/netlify": patch
+"@remix-run/vercel": patch
+---
+
+fix: abort requests on response close instead of request close (#3626)
diff --git a/.changeset/stupid-houses-sing.md b/.changeset/stupid-houses-sing.md
deleted file mode 100644
index 8eae52c0c..000000000
--- a/.changeset/stupid-houses-sing.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-"remix": minor
-"@remix-run/serve": minor
-"@remix-run/server-runtime": minor
----
-
-`MetaFunction` type can now infer `data` and `parentsData` types from route loaders
diff --git a/.changeset/wild-meals-beg.md b/.changeset/wild-meals-beg.md
deleted file mode 100644
index f0fd443c4..000000000
--- a/.changeset/wild-meals-beg.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@remix-run/react": patch
----
-
-Unblock hydration via async module scripts.
diff --git a/.changeset/wordle-is-weird.md b/.changeset/wordle-is-weird.md
deleted file mode 100644
index 68b8a95bb..000000000
--- a/.changeset/wordle-is-weird.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@remix-run/node": patch
----
-
-Fixed a bug when destroying `fileStorage` sessions to prevent deleting entire session directories
diff --git a/contributors.yml b/contributors.yml
index 4c632d504..4164fec2a 100644
--- a/contributors.yml
+++ b/contributors.yml
@@ -209,6 +209,7 @@
 - juwiragiye
 - jveldridge
 - jvnm-dev
+- jwnx
 - kalch
 - kanermichael
 - karimsan
diff --git a/packages/create-remix/CHANGELOG.md b/packages/create-remix/CHANGELOG.md
index 1929c9977..79002e8e1 100644
--- a/packages/create-remix/CHANGELOG.md
+++ b/packages/create-remix/CHANGELOG.md
@@ -1,5 +1,12 @@
 # `create-remix`
 
+## 1.7.0
+
+### Patch Changes
+
+- Updated dependencies:
+  - `@remix-run/dev@1.7.0`
+
 ## 1.6.8
 
 ### Patch Changes
diff --git a/packages/create-remix/package.json b/packages/create-remix/package.json
index 459af74de..cbf7e4f07 100644
--- a/packages/create-remix/package.json
+++ b/packages/create-remix/package.json
@@ -1,6 +1,6 @@
 {
   "name": "create-remix",
-  "version": "1.6.8",
+  "version": "1.7.0",
   "description": "Create a new Remix app",
   "homepage": "https://remix.run",
   "bugs": {
@@ -17,7 +17,7 @@
     "create-remix": "dist/cli.js"
   },
   "dependencies": {
-    "@remix-run/dev": "1.6.8"
+    "@remix-run/dev": "1.7.0"
   },
   "engines": {
     "node": ">=14"
diff --git a/packages/remix-architect/CHANGELOG.md b/packages/remix-architect/CHANGELOG.md
index 89ad0b8da..b68b3dca1 100644
--- a/packages/remix-architect/CHANGELOG.md
+++ b/packages/remix-architect/CHANGELOG.md
@@ -1,5 +1,12 @@
 # `@remix-run/architect`
 
+## 1.7.0
+
+### Patch Changes
+
+- Updated dependencies:
+  - `@remix-run/node@1.7.0`
+
 ## 1.6.8
 
 ### Patch Changes
diff --git a/packages/remix-architect/package.json b/packages/remix-architect/package.json
index aa4108e77..3b5294b30 100644
--- a/packages/remix-architect/package.json
+++ b/packages/remix-architect/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/architect",
-  "version": "1.6.8",
+  "version": "1.7.0",
   "description": "Architect server request handler for Remix",
   "bugs": {
     "url": "https://github.com/remix-run/remix/issues"
@@ -15,7 +15,7 @@
   "typings": "dist/index.d.ts",
   "dependencies": {
     "@architect/functions": "^5.0.2",
-    "@remix-run/node": "1.6.8",
+    "@remix-run/node": "1.7.0",
     "@types/aws-lambda": "^8.10.82"
   },
   "devDependencies": {
diff --git a/packages/remix-architect/server.ts b/packages/remix-architect/server.ts
index c71c7c562..0fe02c45d 100644
--- a/packages/remix-architect/server.ts
+++ b/packages/remix-architect/server.ts
@@ -64,10 +64,14 @@ export function createRemixRequest(event: APIGatewayProxyEventV2): NodeRequest {
   let isFormData = event.headers["content-type"]?.includes(
     "multipart/form-data"
   );
+  // Note: No current way to abort these for Architect, but our router expects
+  // requests to contain a signal so it can detect aborted requests
+  let controller = new AbortController();
 
   return new NodeRequest(url.href, {
     method: event.requestContext.http.method,
     headers: createRemixHeaders(event.headers, event.cookies),
+    signal: controller.signal,
     body:
       event.body && event.isBase64Encoded
         ? isFormData
diff --git a/packages/remix-cloudflare-pages/CHANGELOG.md b/packages/remix-cloudflare-pages/CHANGELOG.md
index f0014676e..8f52c8615 100644
--- a/packages/remix-cloudflare-pages/CHANGELOG.md
+++ b/packages/remix-cloudflare-pages/CHANGELOG.md
@@ -1,5 +1,12 @@
 # `@remix-run/cloudflare-pages`
 
+## 1.7.0
+
+### Patch Changes
+
+- Updated dependencies:
+  - `@remix-run/cloudflare@1.7.0`
+
 ## 1.6.8
 
 ### Patch Changes
diff --git a/packages/remix-cloudflare-pages/package.json b/packages/remix-cloudflare-pages/package.json
index bcc84961c..45580dd25 100644
--- a/packages/remix-cloudflare-pages/package.json
+++ b/packages/remix-cloudflare-pages/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/cloudflare-pages",
-  "version": "1.6.8",
+  "version": "1.7.0",
   "description": "Cloudflare Pages request handler for Remix",
   "bugs": {
     "url": "https://github.com/remix-run/remix/issues"
@@ -15,7 +15,7 @@
   "typings": "dist/index.d.ts",
   "module": "dist/esm/index.js",
   "dependencies": {
-    "@remix-run/cloudflare": "1.6.8"
+    "@remix-run/cloudflare": "1.7.0"
   },
   "devDependencies": {
     "@cloudflare/workers-types": "^3.4.0",
diff --git a/packages/remix-cloudflare-workers/CHANGELOG.md b/packages/remix-cloudflare-workers/CHANGELOG.md
index 9ee9fc72b..42eaae2eb 100644
--- a/packages/remix-cloudflare-workers/CHANGELOG.md
+++ b/packages/remix-cloudflare-workers/CHANGELOG.md
@@ -1,5 +1,12 @@
 # `@remix-run/cloudflare-workers`
 
+## 1.7.0
+
+### Patch Changes
+
+- Updated dependencies:
+  - `@remix-run/cloudflare@1.7.0`
+
 ## 1.6.8
 
 ### Patch Changes
diff --git a/packages/remix-cloudflare-workers/package.json b/packages/remix-cloudflare-workers/package.json
index 8bc113a5e..4d59884b6 100644
--- a/packages/remix-cloudflare-workers/package.json
+++ b/packages/remix-cloudflare-workers/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/cloudflare-workers",
-  "version": "1.6.8",
+  "version": "1.7.0",
   "description": "Cloudflare worker request handler for Remix",
   "bugs": {
     "url": "https://github.com/remix-run/remix/issues"
@@ -16,7 +16,7 @@
   "module": "dist/esm/index.js",
   "dependencies": {
     "@cloudflare/kv-asset-handler": "^0.1.3",
-    "@remix-run/cloudflare": "1.6.8"
+    "@remix-run/cloudflare": "1.7.0"
   },
   "devDependencies": {
     "@cloudflare/workers-types": "^3.4.0"
diff --git a/packages/remix-cloudflare/CHANGELOG.md b/packages/remix-cloudflare/CHANGELOG.md
index 1bbcf89fc..a9ff3fbc3 100644
--- a/packages/remix-cloudflare/CHANGELOG.md
+++ b/packages/remix-cloudflare/CHANGELOG.md
@@ -1,5 +1,17 @@
 # `@remix-run/cloudflare`
 
+## 1.7.0
+
+### Minor Changes
+
+- We've added a new type: `SerializeFrom`. This is used to infer the ([#4013](https://github.com/remix-run/remix/pull/4013))
+  JSON-serialized return type of loaders and actions.
+
+### Patch Changes
+
+- Updated dependencies:
+  - `@remix-run/server-runtime@1.7.0`
+
 ## 1.6.8
 
 ### Patch Changes
diff --git a/packages/remix-cloudflare/package.json b/packages/remix-cloudflare/package.json
index 16e4508e8..12cf259b6 100644
--- a/packages/remix-cloudflare/package.json
+++ b/packages/remix-cloudflare/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/cloudflare",
-  "version": "1.6.8",
+  "version": "1.7.0",
   "description": "Cloudflare platform abstractions for Remix",
   "bugs": {
     "url": "https://github.com/remix-run/remix/issues"
@@ -15,7 +15,7 @@
   "typings": "dist/index.d.ts",
   "dependencies": {
     "@cloudflare/kv-asset-handler": "^0.1.3",
-    "@remix-run/server-runtime": "1.6.8"
+    "@remix-run/server-runtime": "1.7.0"
   },
   "devDependencies": {
     "@cloudflare/workers-types": "^3.4.0"
diff --git a/packages/remix-deno/CHANGELOG.md b/packages/remix-deno/CHANGELOG.md
index 0402d119a..1873bfd26 100644
--- a/packages/remix-deno/CHANGELOG.md
+++ b/packages/remix-deno/CHANGELOG.md
@@ -1,5 +1,18 @@
 # `@remix-run/deno`
 
+## 1.7.0
+
+### Minor Changes
+
+- We've added a new type: `SerializeFrom`. This is used to infer the
+  ([#4013](https://github.com/remix-run/remix/pull/4013)) JSON-serialized return
+  type of loaders and actions.
+
+### Patch Changes
+
+- Updated dependencies:
+  - `@remix-run/server-runtime@1.7.0`
+
 ## 1.6.8
 
 ### Patch Changes
diff --git a/packages/remix-deno/package.json b/packages/remix-deno/package.json
index 4982aebe3..8036b12dc 100644
--- a/packages/remix-deno/package.json
+++ b/packages/remix-deno/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/deno",
-  "version": "1.6.8",
+  "version": "1.7.0",
   "description": "Deno platform abstractions for Remix",
   "homepage": "https://remix.run",
   "main": "./index.ts",
@@ -15,7 +15,7 @@
   "license": "MIT",
   "sideEffects": false,
   "dependencies": {
-    "@remix-run/server-runtime": "1.6.8",
+    "@remix-run/server-runtime": "1.7.0",
     "mime": "^3.0.0"
   },
   "engines": {
diff --git a/packages/remix-dev/CHANGELOG.md b/packages/remix-dev/CHANGELOG.md
index 1fff77535..974829216 100644
--- a/packages/remix-dev/CHANGELOG.md
+++ b/packages/remix-dev/CHANGELOG.md
@@ -1,5 +1,19 @@
 # `@remix-run/dev`
 
+## 1.7.0
+
+### Minor Changes
+
+- Added support for importing `.gql` and `.graphql` files as plain text ([#3923](https://github.com/remix-run/remix/pull/3923))
+- Added support for importing `.zip` and `.avif` files as resource URLs ([#3985](https://github.com/remix-run/remix/pull/3985))
+
+### Patch Changes
+
+- Removed our compiler's React shim in favor of esbuild's new automatic JSX transform ([#3860](https://github.com/remix-run/remix/pull/3860))
+- Updated dependencies:
+  - `@remix-run/server-runtime@1.7.0`
+  - `@remix-run/serve@1.7.0`
+
 ## 1.6.8
 
 ### Patch Changes
diff --git a/packages/remix-dev/cli/migrate/migrations/convert-to-javascript/transform/createExportExpressionStatementFromExportDefaultDeclaration.ts b/packages/remix-dev/cli/migrate/migrations/convert-to-javascript/transform/createExportExpressionStatementFromExportDefaultDeclaration.ts
new file mode 100644
index 000000000..32e84e717
--- /dev/null
+++ b/packages/remix-dev/cli/migrate/migrations/convert-to-javascript/transform/createExportExpressionStatementFromExportDefaultDeclaration.ts
@@ -0,0 +1,81 @@
+import type { ExportDefaultDeclaration, JSCodeshift } from "jscodeshift";
+
+/**
+ * export default foo
+ * =>
+ * module.exports = foo
+ */
+export const createExportExpressionStatementFromExportDefaultDeclaration = (
+  j: JSCodeshift,
+  exportDefaultDeclaration: ExportDefaultDeclaration
+) => {
+  /**
+   * HACK: Can't use casts nor type guards in a `jscodeshift` transform
+   * https://github.com/facebook/jscodeshift/issues/467
+   *
+   * So to narrow declaration type, we check it against all possible
+   * `DeclarationKind` values instead.
+   */
+  if (
+    exportDefaultDeclaration.declaration.type === "ClassBody" ||
+    exportDefaultDeclaration.declaration.type === "ClassMethod" ||
+    exportDefaultDeclaration.declaration.type === "ClassPrivateMethod" ||
+    exportDefaultDeclaration.declaration.type === "ClassPrivateProperty" ||
+    exportDefaultDeclaration.declaration.type === "ClassProperty" ||
+    exportDefaultDeclaration.declaration.type === "ClassPropertyDefinition" ||
+    exportDefaultDeclaration.declaration.type === "DeclareClass" ||
+    exportDefaultDeclaration.declaration.type ===
+      "DeclareExportAllDeclaration" ||
+    exportDefaultDeclaration.declaration.type === "DeclareExportDeclaration" ||
+    exportDefaultDeclaration.declaration.type === "DeclareInterface" ||
+    exportDefaultDeclaration.declaration.type === "DeclareOpaqueType" ||
+    exportDefaultDeclaration.declaration.type === "DeclareTypeAlias" ||
+    exportDefaultDeclaration.declaration.type === "EnumDeclaration" ||
+    exportDefaultDeclaration.declaration.type === "ExportAllDeclaration" ||
+    exportDefaultDeclaration.declaration.type === "ExportDeclaration" ||
+    exportDefaultDeclaration.declaration.type === "ExportDefaultDeclaration" ||
+    exportDefaultDeclaration.declaration.type === "ExportNamedDeclaration" ||
+    exportDefaultDeclaration.declaration.type === "FunctionDeclaration" ||
+    exportDefaultDeclaration.declaration.type === "ImportDeclaration" ||
+    exportDefaultDeclaration.declaration.type === "InterfaceDeclaration" ||
+    exportDefaultDeclaration.declaration.type === "MethodDefinition" ||
+    exportDefaultDeclaration.declaration.type === "OpaqueType" ||
+    exportDefaultDeclaration.declaration.type ===
+      "TSCallSignatureDeclaration" ||
+    exportDefaultDeclaration.declaration.type ===
+      "TSConstructSignatureDeclaration" ||
+    exportDefaultDeclaration.declaration.type === "TSDeclareFunction" ||
+    exportDefaultDeclaration.declaration.type === "TSDeclareMethod" ||
+    exportDefaultDeclaration.declaration.type === "TSEnumDeclaration" ||
+    exportDefaultDeclaration.declaration.type === "TSExternalModuleReference" ||
+    exportDefaultDeclaration.declaration.type === "TSImportEqualsDeclaration" ||
+    exportDefaultDeclaration.declaration.type === "TSIndexSignature" ||
+    exportDefaultDeclaration.declaration.type === "TSInterfaceDeclaration" ||
+    exportDefaultDeclaration.declaration.type === "TSMethodSignature" ||
+    exportDefaultDeclaration.declaration.type === "TSModuleDeclaration" ||
+    exportDefaultDeclaration.declaration.type ===
+      "TSNamespaceExportDeclaration" ||
+    exportDefaultDeclaration.declaration.type === "TSPropertySignature" ||
+    exportDefaultDeclaration.declaration.type === "TSTypeAliasDeclaration" ||
+    exportDefaultDeclaration.declaration.type ===
+      "TSTypeParameterDeclaration" ||
+    exportDefaultDeclaration.declaration.type === "TypeAlias" ||
+    exportDefaultDeclaration.declaration.type === "VariableDeclaration"
+  ) {
+    return exportDefaultDeclaration;
+  }
+
+  let expressionKind =
+    exportDefaultDeclaration.declaration.type === "ClassDeclaration"
+      ? j.classExpression.from(exportDefaultDeclaration.declaration)
+      : // : exportDefaultDeclaration.declaration.type === "FunctionDeclaration"
+        // ? j.functionExpression.from(exportDefaultDeclaration.declaration)
+        exportDefaultDeclaration.declaration;
+  return j.expressionStatement(
+    j.assignmentExpression(
+      "=",
+      j.memberExpression(j.identifier("module"), j.identifier("exports")),
+      expressionKind
+    )
+  );
+};
diff --git a/packages/remix-dev/cli/migrate/migrations/convert-to-javascript/transform/createExpressionStatement.ts b/packages/remix-dev/cli/migrate/migrations/convert-to-javascript/transform/createImportExpressionStatement.ts
similarity index 72%
rename from packages/remix-dev/cli/migrate/migrations/convert-to-javascript/transform/createExpressionStatement.ts
rename to packages/remix-dev/cli/migrate/migrations/convert-to-javascript/transform/createImportExpressionStatement.ts
index 9c70f78e1..9e77561d7 100644
--- a/packages/remix-dev/cli/migrate/migrations/convert-to-javascript/transform/createExpressionStatement.ts
+++ b/packages/remix-dev/cli/migrate/migrations/convert-to-javascript/transform/createImportExpressionStatement.ts
@@ -1,6 +1,11 @@
 import type { ImportDeclaration, JSCodeshift } from "jscodeshift";
 
-export const createExpressionStatement = (
+/**
+ * import "foo"
+ * =>
+ * require("foo")
+ */
+export const createImportExpressionStatement = (
   j: JSCodeshift,
   { source }: ImportDeclaration
 ) => {
diff --git a/packages/remix-dev/cli/migrate/migrations/convert-to-javascript/transform/createVariableDeclarationIdentifier.ts b/packages/remix-dev/cli/migrate/migrations/convert-to-javascript/transform/createVariableDeclarationIdentifier.ts
index e6037bb2f..f57aa3b14 100644
--- a/packages/remix-dev/cli/migrate/migrations/convert-to-javascript/transform/createVariableDeclarationIdentifier.ts
+++ b/packages/remix-dev/cli/migrate/migrations/convert-to-javascript/transform/createVariableDeclarationIdentifier.ts
@@ -1,10 +1,20 @@
 import type { ImportDeclaration, JSCodeshift } from "jscodeshift";
 
+/**
+ * import foo from "foo"
+ * import * as foo from "foo"
+ * =>
+ * const foo = require("foo").default
+ * const foo = require("foo")
+ */
 export const createVariableDeclarationIdentifier = (
   j: JSCodeshift,
   { source, specifiers }: ImportDeclaration
 ) => {
   let callExpression = j.callExpression(j.identifier("require"), [source]);
+  let isDefaultImport = (specifiers || []).some(
+    ({ type }) => type === "ImportDefaultSpecifier"
+  );
 
   return j.variableDeclaration("const", [
     j.variableDeclarator(
@@ -24,7 +34,9 @@ export const createVariableDeclarationIdentifier = (
               : []
           )[0].local?.name || ""
       ),
-      callExpression
+      isDefaultImport
+        ? j.memberExpression(callExpression, j.identifier("default"))
+        : callExpression
     ),
   ]);
 };
diff --git a/packages/remix-dev/cli/migrate/migrations/convert-to-javascript/transform/createVariableDeclarationObjectPattern.ts b/packages/remix-dev/cli/migrate/migrations/convert-to-javascript/transform/createVariableDeclarationObjectPattern.ts
index 9f37bfc18..fe86ad7dd 100644
--- a/packages/remix-dev/cli/migrate/migrations/convert-to-javascript/transform/createVariableDeclarationObjectPattern.ts
+++ b/packages/remix-dev/cli/migrate/migrations/convert-to-javascript/transform/createVariableDeclarationObjectPattern.ts
@@ -1,5 +1,12 @@
 import type { ImportDeclaration, JSCodeshift } from "jscodeshift";
 
+/**
+ * import { foo } from "foo"
+ * import { foo as bar } from "foo"
+ * =>
+ * const { foo } = require("foo")
+ * const { foo: bar } = require("foo")
+ */
 export const createVariableDeclarationObjectPattern = (
   j: JSCodeshift,
   { source, specifiers }: ImportDeclaration
diff --git a/packages/remix-dev/cli/migrate/migrations/convert-to-javascript/transform/index.ts b/packages/remix-dev/cli/migrate/migrations/convert-to-javascript/transform/index.ts
index bab5745e4..d2f85d224 100644
--- a/packages/remix-dev/cli/migrate/migrations/convert-to-javascript/transform/index.ts
+++ b/packages/remix-dev/cli/migrate/migrations/convert-to-javascript/transform/index.ts
@@ -1,7 +1,8 @@
 import type { Transform } from "jscodeshift";
 
 import { checkNoDifferentImportTypesCombined } from "./checkNoDifferentImportTypesCombined";
-import { createExpressionStatement } from "./createExpressionStatement";
+import { createExportExpressionStatementFromExportDefaultDeclaration } from "./createExportExpressionStatementFromExportDefaultDeclaration";
+import { createImportExpressionStatement } from "./createImportExpressionStatement";
 import { createVariableDeclarationIdentifier } from "./createVariableDeclarationIdentifier";
 import { createVariableDeclarationObjectPattern } from "./createVariableDeclarationObjectPattern";
 
@@ -9,9 +10,13 @@ const transform: Transform = (file, api, options) => {
   let j = api.jscodeshift;
   let root = j(file.source);
 
-  let allESImportDeclarations = root.find(j.ImportDeclaration);
-  if (allESImportDeclarations.length === 0) {
-    // This transform doesn't need to run if there are no ES imports
+  let allImportDeclarations = root.find(j.ImportDeclaration);
+  let allExportDefaultDeclarations = root.find(j.ExportDefaultDeclaration);
+  if (
+    allImportDeclarations.length === 0 &&
+    allExportDefaultDeclarations.length === 0
+  ) {
+    // This transform doesn't need to run if there are no ES imports/exports
     return null;
   }
 
@@ -19,20 +24,24 @@ const transform: Transform = (file, api, options) => {
   let getFirstNode = () => root.find(j.Program).get("body", 0).node;
   let oldFirstNode = getFirstNode();
 
-  allESImportDeclarations.forEach((importDeclaration) => {
+  allImportDeclarations.forEach((importDeclaration) => {
     if (importDeclaration.node.importKind === "type") {
       return;
     }
 
     let { specifiers } = importDeclaration.node;
+
+    // import "foo"
     if (!specifiers || specifiers.length === 0) {
       return j(importDeclaration).replaceWith(
-        createExpressionStatement(j, importDeclaration.node)
+        createImportExpressionStatement(j, importDeclaration.node)
       );
     }
 
+    // import Foo, { bar } from "foo"
     checkNoDifferentImportTypesCombined(importDeclaration.node);
 
+    // import foo from "foo" || import * as foo from "foo"
     if (
       ["ImportDefaultSpecifier", "ImportNamespaceSpecifier"].includes(
         specifiers[0].type
@@ -43,11 +52,22 @@ const transform: Transform = (file, api, options) => {
       );
     }
 
+    // import { foo } from "foo" || import { foo as bar } from "foo"
     return j(importDeclaration).replaceWith(
       createVariableDeclarationObjectPattern(j, importDeclaration.node)
     );
   });
 
+  allExportDefaultDeclarations.forEach((exportDefaultDeclaration) => {
+    // export default foo
+    j(exportDefaultDeclaration).replaceWith(
+      createExportExpressionStatementFromExportDefaultDeclaration(
+        j,
+        exportDefaultDeclaration.node
+      )
+    );
+  });
+
   // If the first node has been modified or deleted, reattach the comments
   let newFirstNode = getFirstNode();
   if (newFirstNode !== oldFirstNode) {
diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts
index a2425c7ca..b677af3ca 100644
--- a/packages/remix-dev/compiler.ts
+++ b/packages/remix-dev/compiler.ts
@@ -368,6 +368,10 @@ async function createBrowserBuild(
     sourcemap: options.sourcemap,
     metafile: true,
     incremental: options.incremental,
+    // As pointed out by https://github.com/evanw/esbuild/issues/2440, when tsconfig is set to
+    // `undefined`, esbuild will keep looking for a tsconfig.json recursively up. This unwanted
+    // behavior can only be avoided by creating an empty tsconfig file in the root directory.
+    tsconfig: config.tsconfigPath,
     mainFields: ["browser", "module", "main"],
     treeShaking: true,
     minify: options.mode === BuildMode.Production,
@@ -458,6 +462,10 @@ function createServerBuild(
       loader: loaders,
       bundle: true,
       logLevel: "silent",
+      // As pointed out by https://github.com/evanw/esbuild/issues/2440, when tsconfig is set to
+      // `undefined`, esbuild will keep looking for a tsconfig.json recursively up. This unwanted
+      // behavior can only be avoided by creating an empty tsconfig file in the root directory.
+      tsconfig: config.tsconfigPath,
       incremental: options.incremental,
       sourcemap: options.sourcemap, // use linked (true) to fix up .map file
       // The server build needs to know how to generate asset URLs for imports
diff --git a/packages/remix-dev/compiler/plugins/mdx.ts b/packages/remix-dev/compiler/plugins/mdx.ts
index 4ff210ec9..49e137b8a 100644
--- a/packages/remix-dev/compiler/plugins/mdx.ts
+++ b/packages/remix-dev/compiler/plugins/mdx.ts
@@ -17,7 +17,7 @@ export function mdxPlugin(config: RemixConfig): esbuild.Plugin {
       ]);
 
       build.onResolve({ filter: /\.mdx?$/ }, (args) => {
-        let matchPath = createMatchPath();
+        let matchPath = createMatchPath(config.tsconfigPath);
         // Resolve paths according to tsconfig paths property
         function resolvePath(id: string) {
           if (!matchPath) {
diff --git a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts
index 4e9fcffe1..73902695a 100644
--- a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts
+++ b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts
@@ -23,7 +23,9 @@ export function serverBareModulesPlugin(
   let isDenoRuntime = remixConfig.serverBuildTarget === "deno";
 
   // Resolve paths according to tsconfig paths property
-  let matchPath = isDenoRuntime ? undefined : createMatchPath();
+  let matchPath = isDenoRuntime
+    ? undefined
+    : createMatchPath(remixConfig.tsconfigPath);
   function resolvePath(id: string) {
     if (!matchPath) {
       return id;
diff --git a/packages/remix-dev/compiler/utils/tsconfig/index.ts b/packages/remix-dev/compiler/utils/tsconfig/index.ts
index 8836e7a47..559a34e88 100644
--- a/packages/remix-dev/compiler/utils/tsconfig/index.ts
+++ b/packages/remix-dev/compiler/utils/tsconfig/index.ts
@@ -2,8 +2,17 @@ import tsConfigPaths from "tsconfig-paths";
 
 import { writeConfigDefaults } from "./write-config-defaults";
 
-export function createMatchPath() {
-  let configLoaderResult = tsConfigPaths.loadConfig();
+export function createMatchPath(tsconfigPath: string | undefined) {
+  // There is no tsconfig to match paths against.
+  if (!tsconfigPath) {
+    return undefined;
+  }
+
+  // When passing a absolute path, loadConfig assumes that the path contains
+  // a tsconfig file.
+  // Ref.: https://github.com/dividab/tsconfig-paths/blob/v4.0.0/src/__tests__/config-loader.test.ts#L74
+  let configLoaderResult = tsConfigPaths.loadConfig(tsconfigPath);
+
   if (configLoaderResult.resultType === "failed") {
     if (configLoaderResult.message === "Missing baseUrl in compilerOptions") {
       throw new Error(
diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts
index 6c2a110ed..39d87a89f 100644
--- a/packages/remix-dev/config.ts
+++ b/packages/remix-dev/config.ts
@@ -269,6 +269,11 @@ export interface RemixConfig {
    * A list of directories to watch.
    */
   watchPaths: string[];
+
+  /**
+   * The path for the tsconfig file, if present on the root directory.
+   */
+  tsconfigPath: string | undefined;
 }
 
 /**
@@ -450,6 +455,18 @@ export async function readConfig(
 
   let serverDependenciesToBundle = appConfig.serverDependenciesToBundle || [];
 
+  // When tsconfigPath is undefined, the default "tsconfig.json" is not
+  // found in the root directory.
+  let tsconfigPath: string | undefined;
+  let rootTsconfig = path.resolve(rootDirectory, "tsconfig.json");
+  let rootJsConfig = path.resolve(rootDirectory, "jsconfig.json");
+
+  if (fse.existsSync(rootTsconfig)) {
+    tsconfigPath = rootTsconfig;
+  } else if (fse.existsSync(rootJsConfig)) {
+    tsconfigPath = rootJsConfig;
+  }
+
   return {
     appDirectory,
     cacheDirectory,
@@ -472,6 +489,7 @@ export async function readConfig(
     serverDependenciesToBundle,
     mdx,
     watchPaths,
+    tsconfigPath,
   };
 }
 
diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json
index 3c61d22eb..241bdaeca 100644
--- a/packages/remix-dev/package.json
+++ b/packages/remix-dev/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/dev",
-  "version": "1.6.8",
+  "version": "1.7.0",
   "description": "Dev tools and CLI for Remix",
   "homepage": "https://remix.run",
   "bugs": {
@@ -24,7 +24,7 @@
     "@babel/preset-typescript": "^7.18.6",
     "@esbuild-plugins/node-modules-polyfill": "^0.1.4",
     "@npmcli/package-json": "^2.0.0",
-    "@remix-run/server-runtime": "1.6.8",
+    "@remix-run/server-runtime": "1.7.0",
     "@yarnpkg/esbuild-plugin-pnp": "3.0.0-rc.11",
     "arg": "^5.0.1",
     "cacache": "^15.0.5",
@@ -59,7 +59,7 @@
     "xdm": "^2.0.0"
   },
   "devDependencies": {
-    "@remix-run/serve": "1.6.8",
+    "@remix-run/serve": "1.7.0",
     "@types/cacache": "^15.0.0",
     "@types/gunzip-maybe": "^1.4.0",
     "@types/inquirer": "^8.2.0",
@@ -78,7 +78,7 @@
     "type-fest": "^2.16.0"
   },
   "peerDependencies": {
-    "@remix-run/serve": "1.6.8"
+    "@remix-run/serve": "1.7.0"
   },
   "peerDependenciesMeta": {
     "@remix-run/serve": {
diff --git a/packages/remix-eslint-config/CHANGELOG.md b/packages/remix-eslint-config/CHANGELOG.md
index fc64b7d50..b5e5e9dd1 100644
--- a/packages/remix-eslint-config/CHANGELOG.md
+++ b/packages/remix-eslint-config/CHANGELOG.md
@@ -1,7 +1,13 @@
 # `@remix-run/eslint-config`
 
+## 1.7.0
+
+No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.7.0) for an overview of all changes in v1.7.0.
+
 ## 1.6.8
 
+No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.6.8) for an overview of all changes in v1.6.8.
+
 ## 1.6.7
 
 No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.6.7) for an overview of all changes in v1.6.7.
diff --git a/packages/remix-eslint-config/package.json b/packages/remix-eslint-config/package.json
index 46f8db3d8..ec4906cea 100644
--- a/packages/remix-eslint-config/package.json
+++ b/packages/remix-eslint-config/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/eslint-config",
-  "version": "1.6.8",
+  "version": "1.7.0",
   "description": "ESLint configuration for Remix projects",
   "bugs": {
     "url": "https://github.com/remix-run/remix/issues"
diff --git a/packages/remix-express/CHANGELOG.md b/packages/remix-express/CHANGELOG.md
index a4a03a936..8ecfc053d 100644
--- a/packages/remix-express/CHANGELOG.md
+++ b/packages/remix-express/CHANGELOG.md
@@ -1,5 +1,12 @@
 # `@remix-run/express`
 
+## 1.7.0
+
+### Patch Changes
+
+- Updated dependencies:
+  - `@remix-run/node@1.7.0`
+
 ## 1.6.8
 
 ### Patch Changes
diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json
index 8f1d2cf0e..67ad90c7e 100644
--- a/packages/remix-express/package.json
+++ b/packages/remix-express/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/express",
-  "version": "1.6.8",
+  "version": "1.7.0",
   "description": "Express server request handler for Remix",
   "bugs": {
     "url": "https://github.com/remix-run/remix/issues"
@@ -14,7 +14,7 @@
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",
   "dependencies": {
-    "@remix-run/node": "1.6.8"
+    "@remix-run/node": "1.7.0"
   },
   "devDependencies": {
     "@types/express": "^4.17.9",
diff --git a/packages/remix-express/server.ts b/packages/remix-express/server.ts
index 576be2c0d..edbbac623 100644
--- a/packages/remix-express/server.ts
+++ b/packages/remix-express/server.ts
@@ -52,7 +52,7 @@ export function createRequestHandler({
     next: express.NextFunction
   ) => {
     try {
-      let request = createRemixRequest(req);
+      let request = createRemixRequest(req, res);
       let loadContext = getLoadContext?.(req, res);
 
       let response = (await handleRequest(
@@ -89,15 +89,16 @@ export function createRemixHeaders(
   return headers;
 }
 
-export function createRemixRequest(req: express.Request): NodeRequest {
+export function createRemixRequest(
+  req: express.Request,
+  res: express.Response
+): NodeRequest {
   let origin = `${req.protocol}://${req.get("host")}`;
   let url = new URL(req.url, origin);
 
+  // Abort action/loaders once we can no longer write a response
   let controller = new AbortController();
-
-  req.on("close", () => {
-    controller.abort();
-  });
+  res.on("close", () => controller.abort());
 
   let init: NodeRequestInit = {
     method: req.method,
diff --git a/packages/remix-netlify/CHANGELOG.md b/packages/remix-netlify/CHANGELOG.md
index 62a92b686..2833b4a3e 100644
--- a/packages/remix-netlify/CHANGELOG.md
+++ b/packages/remix-netlify/CHANGELOG.md
@@ -1,5 +1,12 @@
 # `@remix-run/netlify`
 
+## 1.7.0
+
+### Patch Changes
+
+- Updated dependencies:
+  - `@remix-run/node@1.7.0`
+
 ## 1.6.8
 
 ### Patch Changes
diff --git a/packages/remix-netlify/package.json b/packages/remix-netlify/package.json
index 0f13318e7..c46d0cc47 100644
--- a/packages/remix-netlify/package.json
+++ b/packages/remix-netlify/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/netlify",
-  "version": "1.6.8",
+  "version": "1.7.0",
   "description": "Netlify server request handler for Remix",
   "bugs": {
     "url": "https://github.com/remix-run/remix/issues"
@@ -14,7 +14,7 @@
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",
   "dependencies": {
-    "@remix-run/node": "1.6.8"
+    "@remix-run/node": "1.7.0"
   },
   "devDependencies": {
     "@netlify/functions": "^1.0.0"
diff --git a/packages/remix-netlify/server.ts b/packages/remix-netlify/server.ts
index bb3afe1cd..f73b02a46 100644
--- a/packages/remix-netlify/server.ts
+++ b/packages/remix-netlify/server.ts
@@ -65,9 +65,14 @@ export function createRemixRequest(event: HandlerEvent): NodeRequest {
     url = new URL(rawPath, `http://${origin}`);
   }
 
+  // Note: No current way to abort these for Netlify, but our router expects
+  // requests to contain a signal so it can detect aborted requests
+  let controller = new AbortController();
+
   let init: NodeRequestInit = {
     method: event.httpMethod,
     headers: createRemixHeaders(event.multiValueHeaders),
+    signal: controller.signal,
   };
 
   if (event.httpMethod !== "GET" && event.httpMethod !== "HEAD" && event.body) {
diff --git a/packages/remix-node/CHANGELOG.md b/packages/remix-node/CHANGELOG.md
index 418eede4b..eb347e54a 100644
--- a/packages/remix-node/CHANGELOG.md
+++ b/packages/remix-node/CHANGELOG.md
@@ -1,5 +1,18 @@
 # `@remix-run/node`
 
+## 1.7.0
+
+### Minor Changes
+
+- We've added a new type: `SerializeFrom`. This is used to infer the ([#4013](https://github.com/remix-run/remix/pull/4013))
+  JSON-serialized return type of loaders and actions.
+
+### Patch Changes
+
+- Fixed a bug when destroying `fileStorage` sessions to prevent deleting entire session directories
+- Updated dependencies:
+  - `@remix-run/server-runtime@1.7.0`
+
 ## 1.6.8
 
 ### Patch Changes
diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json
index 37c365e65..74a6b54f9 100644
--- a/packages/remix-node/package.json
+++ b/packages/remix-node/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/node",
-  "version": "1.6.8",
+  "version": "1.7.0",
   "description": "Node.js platform abstractions for Remix",
   "bugs": {
     "url": "https://github.com/remix-run/remix/issues"
@@ -15,7 +15,7 @@
   "typings": "dist/index.d.ts",
   "sideEffects": false,
   "dependencies": {
-    "@remix-run/server-runtime": "1.6.8",
+    "@remix-run/server-runtime": "1.7.0",
     "@remix-run/web-fetch": "^4.1.3",
     "@remix-run/web-file": "^3.0.2",
     "@remix-run/web-stream": "^1.0.3",
diff --git a/packages/remix-react/CHANGELOG.md b/packages/remix-react/CHANGELOG.md
index 064302c83..5e5ecce67 100644
--- a/packages/remix-react/CHANGELOG.md
+++ b/packages/remix-react/CHANGELOG.md
@@ -1,5 +1,16 @@
 # `@remix-run/react`
 
+## 1.7.0
+
+### Minor Changes
+
+- We've added a new type: `SerializeFrom`. This is used to infer the ([#4013](https://github.com/remix-run/remix/pull/4013))
+  JSON-serialized return type of loaders and actions.
+
+### Patch Changes
+
+- Unblock hydration via async module scripts. ([#3918](https://github.com/remix-run/remix/pull/3918))
+
 ## 1.6.8
 
 ### Patch Changes
diff --git a/packages/remix-react/components.tsx b/packages/remix-react/components.tsx
index 8a59d4c67..16641edf3 100644
--- a/packages/remix-react/components.tsx
+++ b/packages/remix-react/components.tsx
@@ -1221,13 +1221,19 @@ export function useSubmitImpl(key?: string): SubmitFunction {
       let url = new URL(action, `${protocol}//${host}`);
 
       if (method.toLowerCase() === "get") {
+        // Start with a fresh set of params and wipe out the old params to
+        // match default browser behavior
+        let params = new URLSearchParams();
+        let hasParams = false;
         for (let [name, value] of formData) {
           if (typeof value === "string") {
-            url.searchParams.append(name, value);
+            hasParams = true;
+            params.append(name, value);
           } else {
             throw new Error(`Cannot submit binary form data using GET`);
           }
         }
+        url.search = hasParams ? `?${params.toString()}` : "";
       }
 
       let submission: Submission = {
diff --git a/packages/remix-react/package.json b/packages/remix-react/package.json
index d5e9ab039..05887bd10 100644
--- a/packages/remix-react/package.json
+++ b/packages/remix-react/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/react",
-  "version": "1.6.8",
+  "version": "1.7.0",
   "description": "React DOM bindings for Remix",
   "bugs": {
     "url": "https://github.com/remix-run/remix/issues"
@@ -21,7 +21,7 @@
     "type-fest": "^2.17.0"
   },
   "devDependencies": {
-    "@remix-run/server-runtime": "*",
+    "@remix-run/server-runtime": "1.7.0",
     "@testing-library/jest-dom": "^5.16.2",
     "@testing-library/react": "^13.3.0",
     "abort-controller": "^3.0.0",
diff --git a/packages/remix-serve/CHANGELOG.md b/packages/remix-serve/CHANGELOG.md
index 974e24f55..452d83dc6 100644
--- a/packages/remix-serve/CHANGELOG.md
+++ b/packages/remix-serve/CHANGELOG.md
@@ -1,5 +1,18 @@
 # `@remix-run/serve`
 
+## 1.7.0
+
+### Minor Changes
+
+- We've added a new type: `SerializeFrom`. This is used to infer the ([#4013](https://github.com/remix-run/remix/pull/4013))
+  JSON-serialized return type of loaders and actions.
+- `MetaFunction` type can now infer `data` and `parentsData` types from route loaders ([#4022](https://github.com/remix-run/remix/pull/4022))
+
+### Patch Changes
+
+- Updated dependencies:
+  - `@remix-run/express@1.7.0`
+
 ## 1.6.8
 
 ### Patch Changes
diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json
index c22763312..d8e0843cd 100644
--- a/packages/remix-serve/package.json
+++ b/packages/remix-serve/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/serve",
-  "version": "1.6.8",
+  "version": "1.7.0",
   "description": "Production application server for Remix",
   "bugs": {
     "url": "https://github.com/remix-run/remix/issues"
@@ -17,7 +17,7 @@
     "remix-serve": "dist/cli.js"
   },
   "dependencies": {
-    "@remix-run/express": "1.6.8",
+    "@remix-run/express": "1.7.0",
     "compression": "^1.7.4",
     "express": "^4.17.1",
     "morgan": "^1.10.0"
diff --git a/packages/remix-server-runtime/CHANGELOG.md b/packages/remix-server-runtime/CHANGELOG.md
index a38b9875b..09a5967cd 100644
--- a/packages/remix-server-runtime/CHANGELOG.md
+++ b/packages/remix-server-runtime/CHANGELOG.md
@@ -1,5 +1,17 @@
 # `@remix-run/server-runtime`
 
+## 1.7.0
+
+### Minor Changes
+
+- We've added a new type: `SerializeFrom`. This is used to infer the ([#4013](https://github.com/remix-run/remix/pull/4013))
+  JSON-serialized return type of loaders and actions.
+- `MetaFunction` type can now infer `data` and `parentsData` types from route loaders ([#4022](https://github.com/remix-run/remix/pull/4022))
+
+### Patch Changes
+
+- Improved performance for data serialization at runtime ([#3889](https://github.com/remix-run/remix/pull/3889))
+
 ## 1.6.8
 
 ### Patch Changes
diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json
index 15032c81d..309e1a65c 100644
--- a/packages/remix-server-runtime/package.json
+++ b/packages/remix-server-runtime/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/server-runtime",
-  "version": "1.6.8",
+  "version": "1.7.0",
   "description": "Server runtime for Remix",
   "bugs": {
     "url": "https://github.com/remix-run/remix/issues"
diff --git a/packages/remix-vercel/CHANGELOG.md b/packages/remix-vercel/CHANGELOG.md
index 3d16c6488..44d4200ef 100644
--- a/packages/remix-vercel/CHANGELOG.md
+++ b/packages/remix-vercel/CHANGELOG.md
@@ -1,5 +1,12 @@
 # `@remix-run/vercel`
 
+## 1.7.0
+
+### Patch Changes
+
+- Updated dependencies:
+  - `@remix-run/node@1.7.0`
+
 ## 1.6.8
 
 ### Patch Changes
diff --git a/packages/remix-vercel/package.json b/packages/remix-vercel/package.json
index a358f9122..2166ee732 100644
--- a/packages/remix-vercel/package.json
+++ b/packages/remix-vercel/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@remix-run/vercel",
-  "version": "1.6.8",
+  "version": "1.7.0",
   "description": "Vercel server request handler for Remix",
   "bugs": {
     "url": "https://github.com/remix-run/remix/issues"
@@ -14,7 +14,7 @@
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",
   "dependencies": {
-    "@remix-run/node": "1.6.8"
+    "@remix-run/node": "1.7.0"
   },
   "devDependencies": {
     "@types/supertest": "^2.0.10",
diff --git a/packages/remix-vercel/server.ts b/packages/remix-vercel/server.ts
index cd5ccdd9b..44a4f88f4 100644
--- a/packages/remix-vercel/server.ts
+++ b/packages/remix-vercel/server.ts
@@ -46,7 +46,7 @@ export function createRequestHandler({
   let handleRequest = createRemixRequestHandler(build, mode);
 
   return async (req, res) => {
-    let request = createRemixRequest(req);
+    let request = createRemixRequest(req, res);
     let loadContext = getLoadContext?.(req, res);
 
     let response = (await handleRequest(request, loadContext)) as NodeResponse;
@@ -75,17 +75,18 @@ export function createRemixHeaders(
   return headers;
 }
 
-export function createRemixRequest(req: VercelRequest): NodeRequest {
+export function createRemixRequest(
+  req: VercelRequest,
+  res: VercelResponse
+): NodeRequest {
   let host = req.headers["x-forwarded-host"] || req.headers["host"];
   // doesn't seem to be available on their req object!
   let protocol = req.headers["x-forwarded-proto"] || "https";
   let url = new URL(req.url!, `${protocol}://${host}`);
 
+  // Abort action/loaders once we can no longer write a response
   let controller = new AbortController();
-
-  req.on("close", () => {
-    controller.abort();
-  });
+  res.on("close", () => controller.abort());
 
   let init: NodeRequestInit = {
     method: req.method,
diff --git a/packages/remix/package.json b/packages/remix/package.json
index c85bd9a64..504f46f0a 100644
--- a/packages/remix/package.json
+++ b/packages/remix/package.json
@@ -1,6 +1,6 @@
 {
   "name": "remix",
-  "version": "1.6.8",
+  "version": "1.7.0",
   "description": "A framework for building better websites",
   "homepage": "https://remix.run",
   "bugs": {


Test patch:
diff --git a/integration/abort-signal-test.ts b/integration/abort-signal-test.ts
new file mode 100644
index 000000000..cca95a88a
--- /dev/null
+++ b/integration/abort-signal-test.ts
@@ -0,0 +1,61 @@
+import { test, expect } from "@playwright/test";
+
+import { PlaywrightFixture } from "./helpers/playwright-fixture";
+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.jsx": js`
+        import { json } from "@remix-run/node";
+        import { useActionData, useLoaderData, Form } from "@remix-run/react";
+
+        export async function action ({ request }) {
+          console.log('action request.signal', request.signal)
+          // New event loop causes express request to close
+          await new Promise(r => setTimeout(r, 0));
+          return json({ aborted: request.signal.aborted });
+        }
+
+        export function loader({ request }) {
+          console.log('loader request.signal', request.signal)
+          return json({ aborted: request.signal.aborted });
+        }
+
+        export default function Index() {
+          let actionData = useActionData();
+          let data = useLoaderData();
+          return (
+            <div>
+              <p className="action">{actionData ? String(actionData.aborted) : "empty"}</p>
+              <p className="loader">{String(data.aborted)}</p>
+              <Form method="post">
+                <button type="submit">Submit</button>
+              </Form>
+            </div>
+          )
+        }
+      `,
+    },
+  });
+
+  // This creates an interactive app using puppeteer.
+  appFixture = await createAppFixture(fixture);
+});
+
+test.afterAll(async () => appFixture.close());
+
+test("should not abort the request in a new event loop", async ({ page }) => {
+  let app = new PlaywrightFixture(appFixture, page);
+  await app.goto("/");
+  expect(await app.getHtml(".action")).toMatch("empty");
+  expect(await app.getHtml(".loader")).toMatch("false");
+
+  await app.clickElement('button[type="submit"]');
+  expect(await app.getHtml(".action")).toMatch("false");
+  expect(await app.getHtml(".loader")).toMatch("false");
+});
diff --git a/integration/form-test.ts b/integration/form-test.ts
index ea616bc43..68f8f420c 100644
--- a/integration/form-test.ts
+++ b/integration/form-test.ts
@@ -26,6 +26,7 @@ test.describe("Forms", () => {
   let STATIC_ROUTE_PARENT_ACTION = "static-route-parent";
   let STATIC_ROUTE_TOO_MANY_DOTS_ACTION = "static-route-too-many-dots";
   let INDEX_ROUTE_NO_ACTION = "index-route-none";
+  let INDEX_ROUTE_NO_ACTION_POST = "index-route-none-post";
   let INDEX_ROUTE_ABSOLUTE_ACTION = "index-route-abs";
   let INDEX_ROUTE_CURRENT_ACTION = "index-route-cur";
   let INDEX_ROUTE_PARENT_ACTION = "index-route-parent";
@@ -192,6 +193,7 @@ test.describe("Forms", () => {
             return (
               <>
                 <Form id="${INDEX_ROUTE_NO_ACTION}">
+                  <input type="hidden" name="foo" defaultValue="1" />
                   <button>Submit</button>
                 </Form>
                 <Form id="${INDEX_ROUTE_ABSOLUTE_ACTION}" action="/about">
@@ -206,6 +208,11 @@ test.describe("Forms", () => {
                 <Form id="${INDEX_ROUTE_TOO_MANY_DOTS_ACTION}" action="../../../../about">
                   <button>Submit</button>
                 </Form>
+
+                <Form method="post" id="${INDEX_ROUTE_NO_ACTION_POST}">
+                  <input type="hidden" name="bar" defaultValue="2" />
+                  <button>Submit</button>
+                </Form>
               </>
             )
           }
@@ -620,25 +627,53 @@ test.describe("Forms", () => {
         expect(el.attr("action")).toMatch("/");
       });
 
-      test("does not repeatedly add index params on submissions", async ({
+      test("handles search params correctly on GET submissions", async ({
         page,
       }) => {
         let app = new PlaywrightFixture(appFixture, page);
-        await app.goto("/blog");
+
+        // Start with a query param
+        await app.goto("/blog?junk=1");
         let html = await app.getHtml();
         let el = getElement(html, `#${INDEX_ROUTE_NO_ACTION}`);
-        expect(el.attr("action")).toBe("/blog?index");
-        expect(app.page.url()).toMatch(/\/blog$/);
+        expect(el.attr("action")).toBe("/blog?index&junk=1");
+        expect(app.page.url()).toMatch(/\/blog\?junk=1$/);
 
+        // On submission, we replace existing parameters (reflected in the
+        // form action) with the values from the form data.  We also do not
+        // need to preserve the index param in the URL on GET submissions
         await app.clickElement(`#${INDEX_ROUTE_NO_ACTION} button`);
+        html = await app.getHtml();
         el = getElement(html, `#${INDEX_ROUTE_NO_ACTION}`);
-        expect(el.attr("action")).toBe("/blog?index");
-        expect(app.page.url()).toMatch(/\/blog\?index$/);
+        expect(el.attr("action")).toBe("/blog?index&foo=1");
+        expect(app.page.url()).toMatch(/\/blog\?foo=1$/);
 
+        // Does not append duplicate params on re-submissions
         await app.clickElement(`#${INDEX_ROUTE_NO_ACTION} button`);
+        html = await app.getHtml();
         el = getElement(html, `#${INDEX_ROUTE_NO_ACTION}`);
-        expect(el.attr("action")).toBe("/blog?index");
-        expect(app.page.url()).toMatch(/\/blog\?index$/);
+        expect(el.attr("action")).toBe("/blog?index&foo=1");
+        expect(app.page.url()).toMatch(/\/blog\?foo=1$/);
+      });
+
+      test("handles search params correctly on POST submissions", async ({
+        page,
+      }) => {
+        let app = new PlaywrightFixture(appFixture, page);
+
+        // Start with a query param
+        await app.goto("/blog?junk=1");
+        let html = await app.getHtml();
+        let el = getElement(html, `#${INDEX_ROUTE_NO_ACTION_POST}`);
+        expect(el.attr("action")).toBe("/blog?index&junk=1");
+        expect(app.page.url()).toMatch(/\/blog\?junk=1$/);
+
+        // Form action reflects the current params and change them on submission
+        await app.clickElement(`#${INDEX_ROUTE_NO_ACTION_POST} button`);
+        html = await app.getHtml();
+        el = getElement(html, `#${INDEX_ROUTE_NO_ACTION_POST}`);
+        expect(el.attr("action")).toBe("/blog?index&junk=1");
+        expect(app.page.url()).toMatch(/\/blog\?index&junk=1$/);
       });
     });
 
diff --git a/packages/remix-architect/__tests__/server-test.ts b/packages/remix-architect/__tests__/server-test.ts
index 0dc6bca7a..5ac8bb24d 100644
--- a/packages/remix-architect/__tests__/server-test.ts
+++ b/packages/remix-architect/__tests__/server-test.ts
@@ -292,7 +292,13 @@ describe("architect createRemixRequest", () => {
           "method": "GET",
           "parsedURL": "https://localhost:3333/",
           "redirect": "follow",
-          "signal": null,
+          "signal": AbortSignal {
+            Symbol(kEvents): Map {},
+            Symbol(events.maxEventTargetListeners): 10,
+            Symbol(events.maxEventTargetListenersWarned): false,
+            Symbol(kAborted): false,
+            Symbol(kReason): undefined,
+          },
         },
       }
     `);
@@ -302,8 +308,7 @@ describe("architect createRemixRequest", () => {
 describe("sendRemixResponse", () => {
   it("handles regular responses", async () => {
     let response = new NodeResponse("anything");
-    let abortController = new AbortController();
-    let result = await sendRemixResponse(response, abortController);
+    let result = await sendRemixResponse(response);
     expect(result.body).toBe("anything");
   });
 
@@ -316,9 +321,7 @@ describe("sendRemixResponse", () => {
       },
     });
 
-    let abortController = new AbortController();
-
-    let result = await sendRemixResponse(response, abortController);
+    let result = await sendRemixResponse(response);
 
     expect(result.body).toMatch(json);
   });
@@ -333,9 +336,7 @@ describe("sendRemixResponse", () => {
       },
     });
 
-    let abortController = new AbortController();
-
-    let result = await sendRemixResponse(response, abortController);
+    let result = await sendRemixResponse(response);
 
     expect(result.body).toMatch(image.toString("base64"));
   });
diff --git a/packages/remix-dev/__tests__/migrations/convert-to-javascript-test.ts b/packages/remix-dev/__tests__/migrations/convert-to-javascript-test.ts
index 849d39b86..78f506e4f 100644
--- a/packages/remix-dev/__tests__/migrations/convert-to-javascript-test.ts
+++ b/packages/remix-dev/__tests__/migrations/convert-to-javascript-test.ts
@@ -90,10 +90,14 @@ const checkMigrationRanSuccessfully = async (projectDir: string) => {
     cwd: config.rootDirectory,
     ignore: [`./${config.appDirectory}/**/*`],
   });
-  let result = shell.grep("-l", 'from "', JSFiles);
-  expect(result.stdout.trim()).toBe("");
-  expect(result.stderr).toBeNull();
-  expect(result.code).toBe(0);
+  let importResult = shell.grep("-l", 'from "', JSFiles);
+  expect(importResult.stdout.trim()).toBe("");
+  expect(importResult.stderr).toBeNull();
+  expect(importResult.code).toBe(0);
+  let exportDefaultResult = shell.grep("-l", 'export default "', JSFiles);
+  expect(exportDefaultResult.stdout.trim()).toBe("");
+  expect(exportDefaultResult.stderr).toBeNull();
+  expect(exportDefaultResult.code).toBe(0);
 };
 
 const makeApp = () => {
diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts
index ddfed1ba5..5f14c9f4f 100644
--- a/packages/remix-dev/__tests__/readConfig-test.ts
+++ b/packages/remix-dev/__tests__/readConfig-test.ts
@@ -21,6 +21,7 @@ describe("readConfig", () => {
         serverBuildPath: expect.any(String),
         assetsBuildDirectory: expect.any(String),
         relativeAssetsBuildDirectory: expect.any(String),
+        tsconfigPath: expect.any(String),
       },
       `
       Object {
@@ -50,6 +51,7 @@ describe("readConfig", () => {
         "serverMode": "production",
         "serverModuleFormat": "cjs",
         "serverPlatform": "node",
+        "tsconfigPath": Any<String>,
         "watchPaths": Array [],
       }
     `
diff --git a/packages/remix-express/__tests__/server-test.ts b/packages/remix-express/__tests__/server-test.ts
index 3d3b02b9c..e4b6a6afc 100644
--- a/packages/remix-express/__tests__/server-test.ts
+++ b/packages/remix-express/__tests__/server-test.ts
@@ -1,6 +1,6 @@
 import express from "express";
 import supertest from "supertest";
-import { createRequest } from "node-mocks-http";
+import { createRequest, createResponse } from "node-mocks-http";
 import {
   createRequestHandler as createRemixRequestHandler,
   Response as NodeResponse,
@@ -234,8 +234,10 @@ describe("express createRemixRequest", () => {
         Host: "localhost:3000",
       },
     });
+    let expressResponse = createResponse();
 
-    expect(createRemixRequest(expressRequest)).toMatchInlineSnapshot(`
+    expect(createRemixRequest(expressRequest, expressResponse))
+      .toMatchInlineSnapshot(`
       NodeRequest {
         "agent": undefined,
         "compress": true,
diff --git a/packages/remix-netlify/__tests__/server-test.ts b/packages/remix-netlify/__tests__/server-test.ts
index 2f344a77a..783723d24 100644
--- a/packages/remix-netlify/__tests__/server-test.ts
+++ b/packages/remix-netlify/__tests__/server-test.ts
@@ -273,7 +273,13 @@ describe("netlify createRemixRequest", () => {
           "method": "GET",
           "parsedURL": "http://localhost:3000/",
           "redirect": "follow",
-          "signal": null,
+          "signal": AbortSignal {
+            Symbol(kEvents): Map {},
+            Symbol(events.maxEventTargetListeners): 10,
+            Symbol(events.maxEventTargetListenersWarned): false,
+            Symbol(kAborted): false,
+            Symbol(kReason): undefined,
+          },
         },
       }
     `);
@@ -283,8 +289,7 @@ describe("netlify createRemixRequest", () => {
 describe("sendRemixResponse", () => {
   it("handles regular responses", async () => {
     let response = new NodeResponse("anything");
-    let abortController = new AbortController();
-    let result = await sendRemixResponse(response, abortController);
+    let result = await sendRemixResponse(response);
     expect(result.body).toBe("anything");
   });
 
@@ -297,9 +302,7 @@ describe("sendRemixResponse", () => {
       },
     });
 
-    let abortController = new AbortController();
-
-    let result = await sendRemixResponse(response, abortController);
+    let result = await sendRemixResponse(response);
 
     expect(result.body).toMatch(json);
   });
@@ -314,9 +317,7 @@ describe("sendRemixResponse", () => {
       },
     });
 
-    let abortController = new AbortController();
-
-    let result = await sendRemixResponse(response, abortController);
+    let result = await sendRemixResponse(response);
 
     expect(result.body).toMatch(image.toString("base64"));
   });
diff --git a/packages/remix-vercel/__tests__/server-test.ts b/packages/remix-vercel/__tests__/server-test.ts
index 4e4e72c24..a6fdd6c42 100644
--- a/packages/remix-vercel/__tests__/server-test.ts
+++ b/packages/remix-vercel/__tests__/server-test.ts
@@ -1,7 +1,7 @@
 import supertest from "supertest";
-import { createRequest } from "node-mocks-http";
+import { createRequest, createResponse } from "node-mocks-http";
 import { createServerWithHelpers } from "@vercel/node-bridge/helpers";
-import type { VercelRequest } from "@vercel/node";
+import type { VercelRequest, VercelResponse } from "@vercel/node";
 import {
   createRequestHandler as createRemixRequestHandler,
   Response as NodeResponse,
@@ -238,8 +238,9 @@ describe("vercel createRemixRequest", () => {
         "Cache-Control": "max-age=300, s-maxage=3600",
       },
     }) as VercelRequest;
+    let response = createResponse() as unknown as VercelResponse;
 
-    expect(createRemixRequest(request)).toMatchInlineSnapshot(`
+    expect(createRemixRequest(request, response)).toMatchInlineSnapshot(`
       NodeRequest {
         "agent": undefined,
         "compress": true,