diff --git a/.changeset/big-papayas-jam.md b/.changeset/big-papayas-jam.md
deleted file mode 100644
index 8ff38038eff..00000000000
--- a/.changeset/big-papayas-jam.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@remix-run/vercel": major
----
-
-Drop `@vercel/node` v1 support
diff --git a/.changeset/brown-gifts-chew.md b/.changeset/brown-gifts-chew.md
deleted file mode 100644
index 047f95b2f38..00000000000
--- a/.changeset/brown-gifts-chew.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@remix-run/netlify": major
----
-
-Drop `@netlify/functions` v0.x support
diff --git a/.changeset/config.json b/.changeset/config.json
index e4cb7009969..12d246d66c0 100644
--- a/.changeset/config.json
+++ b/.changeset/config.json
@@ -15,13 +15,11 @@
"@remix-run/dev",
"@remix-run/eslint-config",
"@remix-run/express",
- "@remix-run/netlify",
"@remix-run/node",
"@remix-run/react",
"@remix-run/serve",
"@remix-run/server-runtime",
- "@remix-run/testing",
- "@remix-run/vercel"
+ "@remix-run/testing"
]
],
"linked": [],
diff --git a/.changeset/disabled-link-preload.md b/.changeset/disabled-link-preload.md
new file mode 100644
index 00000000000..574b34302a3
--- /dev/null
+++ b/.changeset/disabled-link-preload.md
@@ -0,0 +1,5 @@
+---
+"@remix-run/react": patch
+---
+
+Add `` timeout counter and disabling logic in case preloading is disabled by the user in Firefox. This prevents us from hanging on client-side navigations when we try to preload stylesheets and never receive a `load`/`error` event on the `link` tag.
diff --git a/.changeset/fair-falcons-wink.md b/.changeset/fair-falcons-wink.md
new file mode 100644
index 00000000000..ec27dfa13ae
--- /dev/null
+++ b/.changeset/fair-falcons-wink.md
@@ -0,0 +1,13 @@
+---
+"@remix-run/vercel": major
+---
+
+The `@remix-run/vercel` runtime adapter has been removed in favor of out of the box Vercel functionality. Please update
+your code by removing `@remix-run/vercel` & `@vercel/node` from your `package.json`, removing your
+`server.ts`/`server.js` file, and removing the `server` & `serverBuildPath` options from your `remix.config.js`.
+
+Due to the removal of this adapter, we also removed our [Vercel template][vercel-template] in favor of the
+[official Vercel template][official-vercel-template].
+
+[vercel-template]: https://github.com/remix-run/remix/tree/main/templates/vercel
+[official-vercel-template]: https://github.com/vercel/vercel/tree/main/examples/remix
diff --git a/.changeset/olive-lemons-marry.md b/.changeset/olive-lemons-marry.md
new file mode 100644
index 00000000000..e178e0b3401
--- /dev/null
+++ b/.changeset/olive-lemons-marry.md
@@ -0,0 +1,5 @@
+---
+"@remix-run/react": patch
+---
+
+Use unique key for `script:ld+json` meta descriptors
diff --git a/.changeset/purple-zoos-refuse.md b/.changeset/purple-zoos-refuse.md
index afd122d699a..2ec4b7364b3 100644
--- a/.changeset/purple-zoos-refuse.md
+++ b/.changeset/purple-zoos-refuse.md
@@ -9,13 +9,11 @@
"@remix-run/deno": major
"@remix-run/dev": major
"@remix-run/express": major
-"@remix-run/netlify": major
"@remix-run/node": major
"@remix-run/react": major
"@remix-run/serve": major
"@remix-run/server-runtime": major
"@remix-run/testing": major
-"@remix-run/vercel": major
---
Require Node >=18.0.0
diff --git a/.changeset/strong-rice-applaud.md b/.changeset/strong-rice-applaud.md
new file mode 100644
index 00000000000..2b431971d71
--- /dev/null
+++ b/.changeset/strong-rice-applaud.md
@@ -0,0 +1,17 @@
+---
+"@remix-run/netlify": major
+---
+
+The `@remix-run/netlify` runtime adapter has removed in favor of [`@netlify/remix-adapter`][official-netlify-adapter]
+& [`@netlify/remix-edge-adapter`][official-netlify-edge-adapter]. Please update your code by changing all
+`@remix-run/netlify` imports to `@netlify/remix-adapter`.
+Keep in mind that `@netlify/remix-adapter` requires `@netlify/functions@^1.0.0`, which is a breaking change compared
+to the previous supported `@netlify/functions` versions in `@remix-run/netlify`.
+
+Due to the removal of this adapter, we also removed our [Netlify template][netlify-template] in favor of the
+[official Netlify template][official-netlify-template].
+
+[official-netlify-adapter]: https://github.com/netlify/remix-compute/tree/main/packages/remix-adapter
+[official-netlify-edge-adapter]: https://github.com/netlify/remix-compute/tree/main/packages/remix-edge-adapter
+[netlify-template]: https://github.com/remix-run/remix/tree/main/templates/netlify
+[official-netlify-template]: https://github.com/netlify/remix-template
diff --git a/.changeset/tasty-apricots-doubt.md b/.changeset/tasty-apricots-doubt.md
new file mode 100644
index 00000000000..52bc8133ebb
--- /dev/null
+++ b/.changeset/tasty-apricots-doubt.md
@@ -0,0 +1,5 @@
+---
+"@remix-run/server-runtime": patch
+---
+
+correctly infer deferred types for top-level promises
diff --git a/.changeset/v2-remove-auto-globals-install.md b/.changeset/v2-remove-auto-globals-install.md
index 4bd8f9bdb4c..9fdda072048 100644
--- a/.changeset/v2-remove-auto-globals-install.md
+++ b/.changeset/v2-remove-auto-globals-install.md
@@ -1,10 +1,8 @@
---
"@remix-run/architect": major
"@remix-run/express": major
-"@remix-run/netlify": major
"@remix-run/node": major
"@remix-run/serve": major
-"@remix-run/vercel": major
---
For preparation of using Node's built in fetch implementation, installing the fetch globals is now a responsibility of the app server. If you are using `remix-serve`, nothing is required. If you are using your own app server, you will need to install the globals yourself.
diff --git a/.github/workflows/deduplicate-yarn.yml b/.github/workflows/deduplicate-yarn.yml
new file mode 100644
index 00000000000..3528fbb797a
--- /dev/null
+++ b/.github/workflows/deduplicate-yarn.yml
@@ -0,0 +1,44 @@
+name: ⚙️ Deduplicate yarn.lock
+
+on:
+ push:
+ branches:
+ - dev
+ paths:
+ - ./yarn.lock
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ format:
+ if: github.repository == 'remix-run/remix'
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: ⬇️ Checkout repo
+ uses: actions/checkout@v3
+
+ - name: ⎔ Setup node
+ uses: actions/setup-node@v3
+ with:
+ node-version-file: ".nvmrc"
+ cache: "yarn"
+
+ - name: ️️⚙️ Dedupe yarn.lock
+ run: npx yarn-deduplicate && rm -rf ./node_modules && yarn
+
+ - name: 💪 Commit
+ run: |
+ git config --local user.email "github-actions[bot]@users.noreply.github.com"
+ git config --local user.name "github-actions[bot]"
+
+ git add .
+ if [ -z "$(git status --porcelain)" ]; then
+ echo "💿 no deduplication needed"
+ exit 0
+ fi
+ git commit -m "chore: deduplicate `yarn.lock`"
+ git push
+ echo "💿 https://github.com/$GITHUB_REPOSITORY/commit/$(git rev-parse HEAD)"
diff --git a/.github/workflows/deployments.yml b/.github/workflows/deployments.yml
index 78aaada9c40..d53ec737fbb 100644
--- a/.github/workflows/deployments.yml
+++ b/.github/workflows/deployments.yml
@@ -21,12 +21,6 @@ on:
required: true
TEST_FLY_TOKEN:
required: true
- TEST_NETLIFY_TOKEN:
- required: true
- TEST_VERCEL_TOKEN:
- required: true
- TEST_VERCEL_USER_ID:
- required: true
jobs:
arc_deploy:
@@ -222,74 +216,3 @@ jobs:
working-directory: ./scripts/deployment-test
env:
FLY_API_TOKEN: ${{ secrets.TEST_FLY_TOKEN }}
-
- netlify_deploy:
- name: "Netlify Deploy"
- if: github.repository == 'remix-run/remix'
- runs-on: ubuntu-latest
- steps:
- - name: 🛑 Cancel Previous Runs
- uses: styfle/cancel-workflow-action@0.11.0
-
- - name: ⬇️ Checkout repo
- uses: actions/checkout@v3
-
- - name: ⎔ Setup node
- uses: actions/setup-node@v3
- with:
- node-version-file: ".nvmrc"
- cache: npm
- cache-dependency-path: ./scripts/deployment-test/package.json # no lockfile, key caching off package.json
-
- # some deployment targets require the latest version of npm
- # TODO: remove this eventually when the default version we get
- # is "latest" enough.
- - name: 📦 Install latest version of npm
- run: npm install -g npm@latest
- working-directory: ./scripts/deployment-test
-
- - name: 📥 Install deployment-test deps
- run: npm install
- working-directory: ./scripts/deployment-test
-
- - name: 🚀 Deploy to Netlify
- run: node ./netlify.mjs
- working-directory: ./scripts/deployment-test
- env:
- NETLIFY_AUTH_TOKEN: ${{ secrets.TEST_NETLIFY_TOKEN }}
-
- vercel_deploy:
- name: "Vercel Deploy"
- if: github.repository == 'remix-run/remix'
- runs-on: ubuntu-latest
- steps:
- - name: 🛑 Cancel Previous Runs
- uses: styfle/cancel-workflow-action@0.11.0
-
- - name: ⬇️ Checkout repo
- uses: actions/checkout@v3
-
- - name: ⎔ Setup node
- uses: actions/setup-node@v3
- with:
- node-version-file: ".nvmrc"
- cache: npm
- cache-dependency-path: ./scripts/deployment-test/package.json # no lockfile, key caching off package.json
-
- # some deployment targets require the latest version of npm
- # TODO: remove this eventually when the default version we get
- # is "latest" enough.
- - name: 📦 Install latest version of npm
- run: npm install -g npm@latest
- working-directory: ./scripts/deployment-test
-
- - name: 📥 Install deployment-test deps
- run: npm install
- working-directory: ./scripts/deployment-test
-
- - name: 🚀 Deploy to Vercel
- run: node ./vercel.mjs
- working-directory: ./scripts/deployment-test
- env:
- VERCEL_TOKEN: ${{ secrets.TEST_VERCEL_TOKEN }}
- VERCEL_ORG_ID: ${{ secrets.TEST_VERCEL_USER_ID }}
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index 38a1053793a..a184b16d35b 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -111,9 +111,6 @@ jobs:
TEST_CF_API_TOKEN: ${{ secrets.TEST_CF_API_TOKEN }}
TEST_DENO_DEPLOY_TOKEN: ${{ secrets.TEST_DENO_DEPLOY_TOKEN }}
TEST_FLY_TOKEN: ${{ secrets.TEST_FLY_TOKEN }}
- TEST_NETLIFY_TOKEN: ${{ secrets.TEST_NETLIFY_TOKEN }}
- TEST_VERCEL_TOKEN: ${{ secrets.TEST_VERCEL_TOKEN }}
- TEST_VERCEL_USER_ID: ${{ secrets.TEST_VERCEL_USER_ID }}
stacks:
needs: [nightly]
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index cd4016cff87..8f2194d2d58 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -113,9 +113,6 @@ jobs:
TEST_CF_API_TOKEN: ${{ secrets.TEST_CF_API_TOKEN }}
TEST_DENO_DEPLOY_TOKEN: ${{ secrets.TEST_DENO_DEPLOY_TOKEN }}
TEST_FLY_TOKEN: ${{ secrets.TEST_FLY_TOKEN }}
- TEST_NETLIFY_TOKEN: ${{ secrets.TEST_NETLIFY_TOKEN }}
- TEST_VERCEL_TOKEN: ${{ secrets.TEST_VERCEL_TOKEN }}
- TEST_VERCEL_USER_ID: ${{ secrets.TEST_VERCEL_USER_ID }}
stacks:
name: 🥞 Remix Stacks Test
diff --git a/contributors.yml b/contributors.yml
index 62d50ee7fd1..91a7e07bbdd 100644
--- a/contributors.yml
+++ b/contributors.yml
@@ -533,6 +533,7 @@
- vorcigernix
- wangbinyq
- weavdale
+- wilcoschoneveld
- willhack
- willin
- wizardlyhel
diff --git a/docs/file-conventions/remix-config.md b/docs/file-conventions/remix-config.md
index 78c3187394c..c782eed15da 100644
--- a/docs/file-conventions/remix-config.md
+++ b/docs/file-conventions/remix-config.md
@@ -228,9 +228,7 @@ There are a few conventions that Remix uses you should be aware of.
[cloudflare-pages]: https://pages.cloudflare.com
[cloudflare-workers]: https://workers.cloudflare.com
[deno]: https://deno.land
-[netlify]: https://www.netlify.com
[node-cjs]: https://nodejs.org/en
-[vercel]: https://vercel.com
[dilum-sanjaya]: https://twitter.com/DilumSanjaya
[an-awesome-visualization]: https://remix-routing-demo.netlify.app
[remix-dev]: ../other-api/dev#remix-dev
diff --git a/docs/guides/envvars.md b/docs/guides/envvars.md
index 0e80c598292..17d42246106 100644
--- a/docs/guides/envvars.md
+++ b/docs/guides/envvars.md
@@ -53,11 +53,8 @@ Then, you can pass those through via `getLoadContext` in your server file:
```ts
export const onRequest = createPagesFunctionHandler({
build,
- getLoadContext(context) {
- // Hand-off Cloudflare ENV vars to the Remix `context` object
- return { env: context.env };
- },
- mode: process.env.NODE_ENV,
+ getLoadContext: (context) => ({ env: context.env }), // Hand-off Cloudflare ENV vars to the Remix `context` object
+ mode: build.mode,
});
```
diff --git a/docs/guides/manual-mode.md b/docs/guides/manual-mode.md
index cb9cdd5bf98..68b1eacfd55 100644
--- a/docs/guides/manual-mode.md
+++ b/docs/guides/manual-mode.md
@@ -250,7 +250,7 @@ app.all(
? createDevRequestHandler(build)
: createRequestHandler({
build,
- mode: process.env.NODE_ENV,
+ mode: build.mode,
})
);
```
diff --git a/docs/other-api/adapter.md b/docs/other-api/adapter.md
index 35bc4d7a8c6..9c066a50337 100644
--- a/docs/other-api/adapter.md
+++ b/docs/other-api/adapter.md
@@ -13,10 +13,8 @@ Idiomatic Remix apps can generally be deployed anywhere because Remix adapts the
- `@remix-run/cloudflare-pages`
- `@remix-run/cloudflare-workers`
- `@remix-run/express`
-- `@remix-run/netlify`
-- `@remix-run/vercel`
-These adapters are imported into your server's entry and are not used inside of your Remix app itself.
+These adapters are imported into your server's entry and are not used inside your Remix app itself.
If you initialized your app with `npx create-remix@latest` with something other than the built-in Remix App Server, you will note a `server/index.js` file that imports and uses one of these adapters.
@@ -28,7 +26,10 @@ Each adapter has the same API. In the future we may have helpers specific to the
- [`@fastly/remix-server-adapter`][fastly-remix-server-adapter] - For [Fastly Compute@Edge][fastly-compute-at-edge].
- [`@mcansh/remix-fastify`][remix-fastify] - For [Fastify][fastify].
-- [`@mcansh/remix-raw-http`][remix-raw-http] - For a good ol barebones Node server.
+- [`@mcansh/remix-raw-http`][remix-raw-http] - For a good old bare bones Node server.
+- [`@netlify/remix-adapter`][netlify-remix-adapter] - For [Netlify][netlify].
+- [`@netlify/remix-edge-adapter`][netlify-remix-edge-adapter] - For [Netlify][netlify] Edge.
+- [`@vercel/remix`][vercel-remix] - For [Vercel][vercel].
- [`remix-google-cloud-functions`][remix-google-cloud-functions] - For [Google Cloud][google-cloud-functions] and [Firebase][firebase-functions] functions.
## Creating an Adapter
@@ -83,52 +84,6 @@ exports.handler = createRequestHandler({
});
```
-Here's an example with Vercel:
-
-```ts
-const {
- createRequestHandler,
-} = require("@remix-run/vercel");
-module.exports = createRequestHandler({
- build: require("./build"),
-});
-```
-
-Here's an example with Netlify:
-
-```ts
-const path = require("node:path");
-
-const {
- createRequestHandler,
-} = require("@remix-run/netlify");
-
-const BUILD_DIR = path.join(process.cwd(), "netlify");
-
-function purgeRequireCache() {
- // purge require cache on requests for "server side HMR" this won't let
- // you have in-memory objects between requests in development,
- // netlify typically does this for you, but we've found it to be hit or
- // miss and some times requires you to refresh the page after it auto reloads
- // or even have to restart your server
- for (const key in require.cache) {
- if (key.startsWith(BUILD_DIR)) {
- delete require.cache[key];
- }
- }
-}
-
-exports.handler =
- process.env.NODE_ENV === "production"
- ? createRequestHandler({ build: require("./build") })
- : (event, context) => {
- purgeRequireCache();
- return createRequestHandler({
- build: require("./build"),
- })(event, context);
- };
-```
-
Here's an example with the simplified Cloudflare Workers API:
```ts
@@ -189,3 +144,8 @@ addEventListener("fetch", (event) => {
[remix-fastify]: https://github.com/mcansh/remix-fastify
[fastify]: https://www.fastify.io
[remix-raw-http]: https://github.com/mcansh/remix-node-http-server
+[netlify-remix-adapter]: https://github.com/netlify/remix-compute/tree/main/packages/remix-adapter
+[netlify-remix-edge-adapter]: https://github.com/netlify/remix-compute/tree/main/packages/remix-edge-adapter
+[netlify]: https://netlify.com
+[vercel-remix]: https://github.com/vercel/remix/blob/main/packages/vercel-remix
+[vercel]: https://vercel.com
diff --git a/docs/other-api/dev-v2.md b/docs/other-api/dev-v2.md
deleted file mode 100644
index 33a8d6726c1..00000000000
--- a/docs/other-api/dev-v2.md
+++ /dev/null
@@ -1,389 +0,0 @@
----
-title: "@remix-run/dev CLI (v2)"
-order: 2
-new: true
----
-
-# Remix CLI (v2)
-
-The Remix CLI comes from the `@remix-run/dev` package. It also includes the compiler. Make sure it is in your `package.json` `devDependencies` so it doesn't get deployed to your server.
-
-To get a full list of available commands and flags, run:
-
-```sh
-npx @remix-run/dev -h
-```
-
-## `remix build`
-
-Builds your app for production. This command will set `process.env.NODE_ENV` to `production` and minify the output for deployment.
-
-```sh
-remix build
-```
-
-### Options
-
-| Option | flag | config | default |
-| ---------------------------------------- | ------------- | ------ | ------- |
-| Generate sourcemaps for production build | `--sourcemap` | N/A | `false` |
-
-## `remix dev`
-
-Runs the Remix compiler in watch mode and spins up your app server.
-
-The Remix compiler will:
-
-1. Set `NODE_ENV` to `development`
-2. Watch your app code for changes and trigger rebuilds
-3. Restart your app server whenever rebuilds succeed
-4. Send code updates to the browser via Live Reload and HMR + Hot Data Revalidation
-
-🎥 For an introduction and deep dive into HMR and HDR in Remix, check out our videos:
-
-- [HMR and Hot Data Revalidation 🔥][hmr-and-hdr]
-- [Mental model for the new dev flow 🧠][mental-model]
-- [Migrating your project to v2 dev flow 🚚][migrating]
-
-
-
-What is "Hot Data Revalidation"?
-
-Like HMR, HDR is a way of hot updating your app without needing to refresh the page.
-That way you can keep your app state as your edits are applied in your app.
-HMR handles client-side code updates like when you change the components, markup, or styles in your app.
-Likewise, HDR handles server-side code updates.
-
-That means any time your change a `loader` on your current page (or any code that your `loader` depends on), Remix will re-fetch data from your changed loader.
-That way your app is _always_ up-to-date with the latest code changes, client-side or server-side.
-
-To learn more about how HMR and HDR work together, check out [Pedro's talk at Remix Conf 2023][legendary-dx].
-
-
-
-### With custom app server
-
-If you used a template to get started, hopefully it's already integrated with `remix dev` out-of-the-box.
-If not, you can follow these steps to integrate your project with `remix dev`:
-
-1. Replace your dev scripts in `package.json` and use `-c` to specify your app server command:
-
-```json filename=package.json
-{
- "scripts": {
- "dev": "remix dev -c \"node ./server.js\""
- }
-}
-```
-
-2. Ensure `broadcastDevReady` is called when your app server is up and running:
-
-```js filename=server.js lines=[12,25-27]
-import path from "node:path";
-
-import { broadcastDevReady } from "@remix-run/node";
-import express from "express";
-
-const BUILD_DIR = path.resolve(__dirname, "build");
-const build = require(BUILD_DIR);
-
-const app = express();
-
-// ... code for setting up your express app goes here ...
-
-app.all(
- "*",
- createRequestHandler({
- build,
- mode: process.env.NODE_ENV,
- })
-);
-
-const port = 3000;
-app.listen(port, () => {
- console.log(`👉 http://localhost:${port}`);
-
- if (process.env.NODE_ENV === "development") {
- broadcastDevReady(build);
- }
-});
-```
-
-
-
-For CloudFlare, use `logDevReady` instead of `broadcastDevReady`.
-
-Why? `broadcastDevReady` uses `fetch` to send a ready message to the Remix compiler,
-but CloudFlare does not support async I/O like `fetch` outside of request handling.
-
-
-
-### Options
-
-Options priority order is: 1. flags, 2. config, 3. defaults.
-
-| Option | flag | config | default | description |
-| --------------- | ------------------ | --------- | --------------------------------- | -------------------------------------------------------- |
-| Command | `-c` / `--command` | `command` | `remix-serve ` | Command used to run your app server |
-| Manual | `--manual` | `manual` | `false` | See [guide for manual mode][manual-mode] |
-| Port | `--port` | `port` | Dynamically chosen open port | Internal port used by the Remix compiler for hot updates |
-| TLS key | `--tls-key` | `tlsKey` | N/A | TLS key for configuring local HTTPS |
-| TLS certificate | `--tls-cert` | `tlsCert` | N/A | TLS certificate for configuring local HTTPS |
-
-For example:
-
-```js filename=remix.config.js
-/** @type {import('@remix-run/dev').AppConfig} */
-module.exports = {
- dev: {
- // ...any other options you want to set go here...
- manual: true,
- tlsKey: "./key.pem",
- tlsCert: "./cert.pem",
- },
-};
-```
-
-### Setting a custom port
-
-The `remix dev --port` option sets the internal port used for hot updates.
-**It does not affect the port your app runs on.**
-
-To set your app server port, set it the way you normally would in production.
-For example, you may have it hardcoded in your `server.js` file.
-
-If you are using `remix-serve` as your app server, you can use its `--port` flag to set the app server port:
-
-```
-remix dev -c "remix-serve --port 8000 ./build"
-```
-
-In contrast, the `remix dev --port` option is an escape-hatch for users who need fine-grain control of network ports.
-Most users, should not need to use `remix dev --port`.
-
-### Manual mode
-
-By default, `remix dev` will restart your app server whenever a rebuild occurs.
-If you'd like to keep your app server running without restarts across rebuilds, check out our [guide for manual mode][manual-mode].
-
-You can see if app server restarts are a bottleneck for your project by comparing the times reported by `remix dev`:
-
-- `rebuilt (Xms)` 👉 the Remix compiler took `X` milliseconds to rebuild your app
-- `app server ready (Yms)` 👉 Remix restarted your app server and it took `Y` milliseconds to start with the new code changes
-
-### Pick up changes from other packages
-
-If you are using a monorepo, you might want Remix to perform hot updates not only when your app code changes, but whenever you change code in any of your apps dependencies.
-
-For example, you could have a UI library package (`packages/ui`) that is used within your Remix app (`packages/app`).
-To pick up changes in `packages/ui`, you can configure [watchPaths][watch-paths] to include your packages.
-
-### How to set up MSW
-
-To use [Mock Service Worker][msw] in development, you'll need to:
-
-1. Run MSW as part of your app server
-2. Configure MSW to not mock internal "dev ready" messages to the Remix compiler
-
-Make sure that you are setting up your mocks for your _app server_ within the `-c` flag so that the `REMIX_DEV_ORIGIN` environment variable is available to your mocks.
-For example, you can use `NODE_OPTIONS` to set Node's `--require` flag when running `remix-serve`:
-
-```json filename=package.json
-{
- "scripts": {
- "dev": "remix dev -c \"npm run dev:app\"",
- "dev:app": "cross-env NODE_OPTIONS=\"--require ./mocks\" remix-serve ./build"
- }
-}
-```
-
-Next, you can use `REMIX_DEV_ORIGIN` to let MSW forward internal "dev ready" messages on `/ping`:
-
-```ts
-import { rest } from "msw";
-
-const REMIX_DEV_PING = new URL(
- process.env.REMIX_DEV_ORIGIN
-);
-REMIX_DEV_PING.pathname = "/ping";
-
-export const server = setupServer(
- rest.post(REMIX_DEV_PING.href, (req) => req.passthrough())
- // ... other request handlers go here ...
-);
-```
-
-### How to set up local HTTPS
-
-For this example, let's use [mkcert][mkcert].
-After you have it installed, make sure to:
-
-- Create a local Certificate Authority if you haven't already done so
-- Use `NODE_EXTRA_CA_CERTS` for Node compatibility
-
-```sh
-mkcert -install # create a local CA
-export NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem" # tell Node to use our local CA
-```
-
-Now, create the TLS key and certificate:
-
-```sh
-mkcert -key-file key.pem -cert-file cert.pem localhost
-```
-
-👆 You can change `localhost` to something else if you are using custom hostnames.
-
-Next, use the `key.pem` and `cert.pem` to get HTTPS working locally with your app server.
-This depends on what you are using for your app server.
-For example, here's how you could use HTTPS with an Express server:
-
-```ts filename=server.js
-import fs from "node:fs";
-import https from "node:https";
-import path from "node:path";
-
-import express from "express";
-
-const BUILD_DIR = path.resolve(__dirname, "build");
-const build = require(BUILD_DIR);
-
-const app = express();
-
-// ... code setting up your express app goes here ...
-
-const server = https.createServer(
- {
- key: fs.readFileSync("path/to/key.pem"),
- cert: fs.readFileSync("path/to/cert.pem"),
- },
- app
-);
-
-const port = 3000;
-server.listen(port, () => {
- console.log(`👉 https://localhost:${port}`);
-
- if (process.env.NODE_ENV === "development") {
- broadcastDevReady(build);
- }
-});
-```
-
-Now that the app server is set up, you should be able to build and run your app in production mode with TLS.
-To get the Remix compiler to interop with TLS, you'll need to specify the TLS cert and key you created:
-
-```sh
-remix dev --tls-key=key.pem --tls-cert=cert.pem -c "node ./server.js"
-```
-
-Alternatively, you can specify the TLS key and cert via the `dev.tlsCert` and `dev.tlsKey` config options.
-Now your app server and Remix compiler are TLS ready!
-
-### How to integrate with a reverse proxy
-
-Let's say you have the app server and Remix compiler both running on the same machine:
-
-- App server 👉 `http://localhost:1234`
-- Remix compiler 👉 `http://localhost:5678`
-
-Then, you setup a reverse proxy in front of the app server:
-
-- Reverse proxy 👉 `https://myhost`
-
-But the internal HTTP and WebSocket connections to support hot updates will still try to reach the Remix compiler's unproxied origin:
-
-- Hot updates 👉 `http://localhost:5678` / `ws://localhost:5678` ❌
-
-To get the internal connections to point to the reverse proxy, you can use the `REMIX_DEV_ORIGIN` environment variable:
-
-```sh
-REMIX_DEV_ORIGIN=https://myhost remix dev
-```
-
-Now, hot updates will be sent correctly to the proxy:
-
-- Hot updates 👉 `https://myhost` / `wss://myhost` ✅
-
-### Performance tuning and debugging
-
-#### Path imports
-
-Currently, when Remix rebuilds your app, the compiler has to process your app code along with any of its dependencies.
-The compiler treeshakes unused code from app so that you don't ship any unused code to browser and so that you keep your server as slim as possible.
-But the compiler still needs to _crawl_ all the code to know what to keep and what to treeshake away.
-
-In short, this means that the way you do imports and exports can have a big impact on how long it takes to rebuild your app.
-For example, if you are using a library like Material UI or AntD you can likely speed up your builds by using [path imports][path-imports]:
-
-```diff
-- import { Button, TextField } from '@mui/material';
-+ import Button from '@mui/material/Button';
-+ import TextField from '@mui/material/TextField';
-```
-
-In the future, Remix could pre-bundle dependencies in development to avoid this problem entirely.
-But today, you can help the compiler out by using path imports.
-
-#### Debugging bundles
-
-Dependending on your app and dependencies, you might be processing much more code than your app needs.
-Check out our [bundle analysis guide][bundle-analysis] for more details.
-
-### Troubleshooting
-
-#### HMR: hot updates losing app state
-
-Hot Module Replacement is supposed to keep your app's state around between hot updates.
-But in some cases React cannot distinguish between existing components being changed and new components being added.
-[React needs `key`s][react-keys] to disambiguate these cases and track changes when sibling elements are modified.
-
-Additionally, when adding or removing hooks, React Refresh treats that as a brand-new component.
-So if you add `useLoaderData` to your component, you may lose state local to that component.
-
-These are limitations of React and [React Refresh][react-refresh], not Remix.
-
-#### HDR: every code change triggers HDR
-
-Hot Data Revalidation detects loader changes by trying to bundle each loader and then fingerprinting the content for each.
-It relies on tree shaking to determine whether your changes affect each loader or not.
-
-To ensure that tree shaking can reliably detect changes to loaders, make sure you declare that your app's package is side effect free:
-
-```json filename=package.json
-{
- "sideEffects": false
-}
-```
-
-#### HDR: harmless console errors when loader data is removed
-
-When you delete a loader or remove some of the data being returned by that loader, your app should be hot updated correctly.
-But you may notice console errors logged in your browser.
-
-React strict-mode and React Suspense can cause multiple renders when hot updates are applied.
-Most of these render correctly, including the final render that is visible to you.
-But intermediate renders can sometimes use new loader data with old React components, which is where those errors come from.
-
-We are continuing to investigate the underlying race condition to see if we can smooth that over.
-In the meantime, if those console errors bother you, you can refresh the page whenever they occur.
-
-#### HDR: performance
-
-When the Remix compiler builds (and rebuilds) your app, you may notice a slight slowdown as the compiler needs to crawl the dependencies for each loader.
-That way Remix can detect loader changes on rebuilds.
-
-While the initial build slowdown is inherently a cost for HDR, we plan to optimize rebuilds so that there is no perceivable slowdown for HDR rebuilds.
-
-[hmr-and-hdr]: https://www.youtube.com/watch?v=2c2OeqOX72s
-[mental-model]: https://www.youtube.com/watch?v=zTrjaUt9hLo
-[migrating]: https://www.youtube.com/watch?v=6jTL8GGbIuc
-[legendary-dx]: https://www.youtube.com/watch?v=79M4vYZi-po
-[watch-paths]: https://remix.run/docs/en/1.17.1/file-conventions/remix-config#watchpaths
-[react-keys]: https://react.dev/learn/rendering-lists#why-does-react-need-keys
-[react-refresh]: https://github.com/facebook/react/tree/main/packages/react-refresh
-[msw]: https://mswjs.io/
-[mkcert]: https://github.com/FiloSottile/mkcert
-[path-imports]: https://mui.com/material-ui/guides/minimizing-bundle-size/#option-one-use-path-imports
-[bundle-analysis]: ../guides/performance
-[manual-mode]: ../guides/manual-mode
diff --git a/docs/other-api/dev.md b/docs/other-api/dev.md
index d492ae6d583..848d3a351d0 100644
--- a/docs/other-api/dev.md
+++ b/docs/other-api/dev.md
@@ -95,7 +95,7 @@ app.all(
"*",
createRequestHandler({
build,
- mode: process.env.NODE_ENV,
+ mode: build.mode,
})
);
diff --git a/docs/pages/contributing.md b/docs/pages/contributing.md
index 5bc96409575..730af547f7b 100644
--- a/docs/pages/contributing.md
+++ b/docs/pages/contributing.md
@@ -243,7 +243,7 @@ Nightly releases will run the action files from the `main` branch as scheduled w
## End to end testing
-For every release of Remix (stable, experimental, nightly, and pre-releases), we will do a complete end-to-end test of Remix apps on each of our official adapters from `create-remix`, all the way to deploying them to production. We do this by utilizing the default [templates][templates] and the CLIs for Fly, Vercel, Netlify, and Arc. We'll then run some simple Cypress assertions to make sure everything is running properly for both development and the deployed app.
+For every release of Remix (stable, experimental, nightly, and pre-releases), we will do a complete end-to-end test of Remix apps on each of our official adapters from `create-remix`, all the way to deploying them to production. We do this by utilizing the default [templates][templates] and the CLIs for Fly, and Arc. We'll then run some simple Cypress assertions to make sure everything is running properly for both development and the deployed app.
## Conclusion
diff --git a/docs/pages/v2.md b/docs/pages/v2.md
index c892dfa975f..45868ca14ef 100644
--- a/docs/pages/v2.md
+++ b/docs/pages/v2.md
@@ -665,20 +665,6 @@ module.exports = {
};
```
-#### `netlify`
-
-```js filename=remix.config.js
-/** @type {import('@remix-run/dev').AppConfig} */
-module.exports = {
- publicPath: "/build/", // default value, can be removed
- serverBuildPath: ".netlify/functions-internal/server.js",
- serverMainFields: ["main", "module"], // default value, can be removed
- serverMinify: false, // default value, can be removed
- serverModuleFormat: "cjs", // default value, can be removed
- serverPlatform: "node", // default value, can be removed
-};
-```
-
#### `node-cjs`
```js filename=remix.config.js
@@ -693,20 +679,6 @@ module.exports = {
};
```
-#### `vercel`
-
-```js filename=remix.config.js
-/** @type {import('@remix-run/dev').AppConfig} */
-module.exports = {
- publicPath: "/build/", // default value, can be removed
- serverBuildPath: "api/index.js",
- serverMainFields: ["main", "module"], // default value, can be removed
- serverMinify: false, // default value, can be removed
- serverModuleFormat: "cjs", // default value, can be removed
- serverPlatform: "node", // default value, can be removed
-};
-```
-
## `serverModuleFormat`
The default server module output format will be changing from `cjs` to `esm`.
diff --git a/integration/helpers/cf-template/server.ts b/integration/helpers/cf-template/server.ts
index 4f4b2ad0aff..49eda486cf8 100644
--- a/integration/helpers/cf-template/server.ts
+++ b/integration/helpers/cf-template/server.ts
@@ -1,7 +1,4 @@
import { createEventHandler } from "@remix-run/cloudflare-workers";
import * as build from "@remix-run/dev/server-build";
-addEventListener(
- "fetch",
- createEventHandler({ build, mode: process.env.NODE_ENV })
-);
+addEventListener("fetch", createEventHandler({ build, mode: build.mode }));
diff --git a/integration/helpers/deno-template/server.ts b/integration/helpers/deno-template/server.ts
index 87e95bf7b18..943b4f0616e 100644
--- a/integration/helpers/deno-template/server.ts
+++ b/integration/helpers/deno-template/server.ts
@@ -5,8 +5,8 @@ import * as build from "@remix-run/dev/server-build";
const remixHandler = createRequestHandlerWithStaticFiles({
build,
- mode: process.env.NODE_ENV,
getLoadContext: () => ({}),
+ mode: build.mode,
});
const port = Number(Deno.env.get("PORT")) || 8000;
diff --git a/integration/hmr-test.ts b/integration/hmr-test.ts
index 69231d25384..5fefafc6058 100644
--- a/integration/hmr-test.ts
+++ b/integration/hmr-test.ts
@@ -59,12 +59,13 @@ let fixture = (options: { appPort: number; devPort: number }): FixtureInit => ({
app.use(express.static("public", { immutable: true, maxAge: "1y" }));
const BUILD_DIR = path.join(process.cwd(), "build");
+ const build = require(BUILD_DIR);
app.all(
"*",
createRequestHandler({
- build: require(BUILD_DIR),
- mode: process.env.NODE_ENV,
+ build,
+ mode: build.mode,
})
);
diff --git a/integration/svg-in-node-modules-test.ts b/integration/svg-in-node-modules-test.ts
new file mode 100644
index 00000000000..96e604c02b8
--- /dev/null
+++ b/integration/svg-in-node-modules-test.ts
@@ -0,0 +1,48 @@
+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.beforeEach(async ({ context }) => {
+ await context.route(/_data/, async (route) => {
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ route.continue();
+ });
+});
+
+test.beforeAll(async () => {
+ fixture = await createFixture({
+ files: {
+ "app/routes/_index.tsx": js`
+ import imgSrc from "getos/imgs/logo.svg";
+
+ export default function () {
+ return (
+
+
+
+ )
+ }
+ `,
+ },
+ });
+
+ appFixture = await createAppFixture(fixture);
+});
+
+test.afterAll(() => {
+ appFixture.close();
+});
+
+test("renders SVG images imported from node_modules", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ // You can test any request your app might get using `fixture`.
+ await app.goto("/");
+ expect(await page.getByTestId("example-svg").getAttribute("src")).toMatch(
+ /\/build\/_assets\/logo-.*\.svg/
+ );
+});
diff --git a/jest.config.js b/jest.config.js
index 276bfb3bb72..e2c605cdae6 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -17,13 +17,11 @@ module.exports = {
"packages/remix-dev",
"packages/remix-eslint-config",
"packages/remix-express",
- "packages/remix-netlify",
"packages/remix-node",
"packages/remix-react",
"packages/remix-serve",
"packages/remix-server-runtime",
"packages/remix-testing",
- "packages/remix-vercel",
],
watchPlugins: [
require.resolve("jest-watch-select-projects"),
diff --git a/package.json b/package.json
index 4a8836bd1bc..7cdc3eb55af 100644
--- a/package.json
+++ b/package.json
@@ -14,13 +14,11 @@
"packages/remix-dev",
"packages/remix-eslint-config",
"packages/remix-express",
- "packages/remix-netlify",
"packages/remix-node",
"packages/remix-react",
"packages/remix-serve",
"packages/remix-server-runtime",
- "packages/remix-testing",
- "packages/remix-vercel"
+ "packages/remix-testing"
],
"scripts": {
"bug-report-test": "yarn test:integration bug-report --project chromium",
@@ -70,7 +68,7 @@
"@rollup/plugin-node-resolve": "^11.0.1",
"@rollup/plugin-replace": "^5.0.2",
"@testing-library/cypress": "^8.0.2",
- "@testing-library/jest-dom": "^5.16.2",
+ "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.3.0",
"@types/cheerio": "^0.22.22",
"@types/cross-spawn": "^6.0.2",
diff --git a/packages/remix-dev/cache.ts b/packages/remix-dev/cache.ts
index bbec3ceee35..22a494d316a 100644
--- a/packages/remix-dev/cache.ts
+++ b/packages/remix-dev/cache.ts
@@ -1,13 +1,7 @@
import { put, get } from "cacache";
-export { put, get };
+export const putJson = async (cachePath: string, key: string, data: any) =>
+ put(cachePath, key, JSON.stringify(data));
-export function putJson(cachePath: string, key: string, data: any) {
- return put(cachePath, key, JSON.stringify(data));
-}
-
-export function getJson(cachePath: string, key: string) {
- return get(cachePath, key).then((obj) =>
- JSON.parse(obj.data.toString("utf-8"))
- );
-}
+export const getJson = async (cachePath: string, key: string) =>
+ get(cachePath, key).then((obj) => JSON.parse(obj.data.toString("utf-8")));
diff --git a/packages/remix-dev/compiler/server/plugins/bareImports.ts b/packages/remix-dev/compiler/server/plugins/bareImports.ts
index cbe99bab0f0..a4e1851248a 100644
--- a/packages/remix-dev/compiler/server/plugins/bareImports.ts
+++ b/packages/remix-dev/compiler/server/plugins/bareImports.ts
@@ -10,6 +10,7 @@ import { isCssSideEffectImportPath } from "../../plugins/cssSideEffectImports";
import { createMatchPath } from "../../utils/tsconfig";
import { detectPackageManager } from "../../../cli/detectPackageManager";
import type { Context } from "../../context";
+import { getLoaderForFile } from "../../utils/loaders";
/**
* A plugin responsible for resolving bare module ids based on server target.
@@ -59,8 +60,23 @@ export function serverBareModulesPlugin(ctx: Context): Plugin {
return undefined;
}
- // Always bundle CSS files so we get immutable fingerprinted asset URLs.
- if (path.endsWith(".css")) {
+ // Skip assets that are treated as files (.css, .svg, .png, etc.).
+ // Otherwise, esbuild would emit code that would attempt to require()
+ // or import these files --- which aren't JavaScript!
+ let loader;
+ try {
+ loader = getLoaderForFile(path);
+ } catch (e) {
+ if (
+ !(
+ e instanceof Error &&
+ e.message.startsWith("Cannot get loader for file")
+ )
+ ) {
+ throw e;
+ }
+ }
+ if (loader === "file") {
return undefined;
}
diff --git a/packages/remix-dev/devServer_unstable/index.ts b/packages/remix-dev/devServer_unstable/index.ts
index af69f463b54..249a25e376d 100644
--- a/packages/remix-dev/devServer_unstable/index.ts
+++ b/packages/remix-dev/devServer_unstable/index.ts
@@ -135,7 +135,7 @@ export let serve = async (
transform(chunk, _, callback) {
let str: string = chunk.toString();
let matches =
- str && str.matchAll(/\[REMIX DEV\] ([A-f0-9]+) ready/g);
+ str && str.matchAll(/\[REMIX DEV\] ([A-Fa-f0-9]+) ready/g);
if (matches) {
for (let match of matches) {
let buildHash = match[1];
diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json
index 78030778801..86d4b3e14b2 100644
--- a/packages/remix-dev/package.json
+++ b/packages/remix-dev/package.json
@@ -31,7 +31,7 @@
"@remix-run/server-runtime": "1.19.2",
"@vanilla-extract/integration": "^6.2.0",
"arg": "^5.0.1",
- "cacache": "^15.0.5",
+ "cacache": "^17.1.3",
"chalk": "^4.1.2",
"chokidar": "^3.5.1",
"dotenv": "^16.0.0",
@@ -77,6 +77,7 @@
"@types/gunzip-maybe": "^1.4.0",
"@types/jsesc": "^3.0.1",
"@types/lodash.debounce": "^4.0.6",
+ "@types/node": "^18.17.1",
"@types/node-fetch": "^2.5.7",
"@types/npmcli__package-json": "^2.0.0",
"@types/picomatch": "^2.3.0",
@@ -87,8 +88,7 @@
"msw": "^0.39.2",
"shelljs": "^0.8.5",
"strip-ansi": "^6.0.1",
- "tiny-invariant": "^1.2.0",
- "type-fest": "^4.0.0"
+ "tiny-invariant": "^1.2.0"
},
"peerDependencies": {
"@remix-run/serve": "^1.19.2",
diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json
index 0e81dfe3bd2..26d264a6ac6 100644
--- a/packages/remix-express/package.json
+++ b/packages/remix-express/package.json
@@ -22,7 +22,7 @@
"@types/supertest": "^2.0.10",
"express": "^4.17.1",
"node-mocks-http": "^1.10.1",
- "supertest": "^6.1.5",
+ "supertest": "^6.3.3",
"typescript": "^5.1.0"
},
"peerDependencies": {
diff --git a/packages/remix-netlify/CHANGELOG.md b/packages/remix-netlify/CHANGELOG.md
deleted file mode 100644
index e9ce82df366..00000000000
--- a/packages/remix-netlify/CHANGELOG.md
+++ /dev/null
@@ -1,262 +0,0 @@
-# `@remix-run/netlify`
-
-## 1.19.2
-
-### Patch Changes
-
-- Show deprecation warning on `@remix-run/netlify` usage, which is deprecated in favor of `@netlify/remix-adapter` ([#6937](https://github.com/remix-run/remix/pull/6937))
-- Updated dependencies:
- - `@remix-run/node@1.19.2`
-
-## 1.19.1
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.19.1`
-
-## 1.19.0
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.19.0`
-
-## 1.18.1
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.18.1`
-
-## 1.18.0
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.18.0`
-
-## 1.17.1
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.17.1`
-
-## 1.17.0
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.17.0`
-
-## 1.16.1
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.16.1`
-
-## 1.16.0
-
-### Patch Changes
-
-- feat: support async `getLoadContext` in all adapters ([#6170](https://github.com/remix-run/remix/pull/6170))
-- Updated dependencies:
- - `@remix-run/node@1.16.0`
-
-## 1.15.0
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.15.0`
-
-## 1.14.3
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.14.3`
-
-## 1.14.2
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.14.2`
-
-## 1.14.1
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.14.1`
-
-## 1.14.0
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.14.0`
-
-## 1.13.0
-
-### Patch Changes
-
-- Fix fetch `Request` creation for incoming URLs with double slashes ([#5336](https://github.com/remix-run/remix/pull/5336))
-- Updated dependencies:
- - `@remix-run/node@1.13.0`
-
-## 1.12.0
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.12.0`
-
-## 1.11.1
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.11.1`
-
-## 1.11.0
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.11.0`
-
-## 1.10.1
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.10.1`
-
-## 1.10.0
-
-### Patch Changes
-
-- Improve performance of `isBinaryType` ([#4761](https://github.com/remix-run/remix/pull/4761))
-- Updated dependencies:
- - `@remix-run/node@1.10.0`
-
-## 1.9.0
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.9.0`
-
-## 1.8.2
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.8.2`
-
-## 1.8.1
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.8.1`
-
-## 1.8.0
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.8.0`
-
-## 1.7.6
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.7.6`
-
-## 1.7.5
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.7.5`
-
-## 1.7.4
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.7.4`
-
-## 1.7.3
-
-### Patch Changes
-
-- Fixed a bug that affected `.wav` and `.webm` audio file imports ([#4290](https://github.com/remix-run/remix/pull/4290))
-- Updated dependencies:
- - `@remix-run/node@1.7.3`
-
-## 1.7.2
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.7.2`
-
-## 1.7.1
-
-### Patch Changes
-
-- Ensured that requests are properly aborted on closing of a `Response` instead of `Request` ([#3626](https://github.com/remix-run/remix/pull/3626))
-- Updated dependencies:
- - `@remix-run/node@1.7.1`
-
-## 1.7.0
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.7.0`
-
-## 1.6.8
-
-### Patch Changes
-
-- We've added type safety for load context. `AppLoadContext` is now an an interface mapping `string` to `unknown`, allowing users to extend it via module augmentation: ([#1876](https://github.com/remix-run/remix/pull/1876))
-
- ```ts
- declare module "@remix-run/server-runtime" {
- interface AppLoadContext {
- // add custom properties here!
- }
- }
- ```
-
-- Updated dependencies:
- - `@remix-run/node@1.6.8`
-
-## 1.6.7
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.6.7`
-
-## 1.6.6
-
-### Patch Changes
-
-- Updated dependencies:
- - `@remix-run/node@1.6.6`
-
-## 1.6.5
-
-### Patch Changes
-
-- Updated dependencies
- - `@remix-run/node@1.6.5`
diff --git a/packages/remix-netlify/README.md b/packages/remix-netlify/README.md
deleted file mode 100644
index 40685a7476f..00000000000
--- a/packages/remix-netlify/README.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Welcome to Remix!
-
-[Remix](https://remix.run) is a web framework that helps you build better websites with React.
-
-To get started, open a new shell and run:
-
-```sh
-npx create-remix@latest
-```
-
-Then follow the prompts you see in your terminal.
-
-For more information about Remix, [visit remix.run](https://remix.run)!
diff --git a/packages/remix-netlify/__tests__/554828.jpeg b/packages/remix-netlify/__tests__/554828.jpeg
deleted file mode 100644
index 830d2dbb3c2..00000000000
Binary files a/packages/remix-netlify/__tests__/554828.jpeg and /dev/null differ
diff --git a/packages/remix-netlify/__tests__/binaryTypes.test.ts b/packages/remix-netlify/__tests__/binaryTypes.test.ts
deleted file mode 100644
index 1418856d48e..00000000000
--- a/packages/remix-netlify/__tests__/binaryTypes.test.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { isBinaryType } from "../binaryTypes";
-
-describe("architect isBinaryType", () => {
- it("should detect binary contentType correctly", () => {
- expect(isBinaryType(undefined)).toBe(false);
- expect(isBinaryType(null)).toBe(false);
- expect(isBinaryType("text/html; charset=utf-8")).toBe(false);
- expect(isBinaryType("application/octet-stream")).toBe(true);
- expect(isBinaryType("application/octet-stream; charset=test")).toBe(true);
- });
-});
diff --git a/packages/remix-netlify/__tests__/server-test.ts b/packages/remix-netlify/__tests__/server-test.ts
deleted file mode 100644
index b09d9f03506..00000000000
--- a/packages/remix-netlify/__tests__/server-test.ts
+++ /dev/null
@@ -1,305 +0,0 @@
-import fsp from "node:fs/promises";
-import path from "node:path";
-import lambdaTester from "lambda-tester";
-import {
- createRequestHandler as createRemixRequestHandler,
- Response as NodeResponse,
-} from "@remix-run/node";
-import type { HandlerEvent } from "@netlify/functions";
-
-import {
- createRemixHeaders,
- createRemixRequest,
- createRequestHandler,
- sendRemixResponse,
-} from "../server";
-
-// We don't want to test that the remix server works here (that's what the
-// playwright tests do), we just want to test the netlify adapter
-jest.mock("@remix-run/node", () => {
- let original = jest.requireActual("@remix-run/node");
- return {
- ...original,
- createRequestHandler: jest.fn(),
- };
-});
-let mockedCreateRequestHandler =
- createRemixRequestHandler as jest.MockedFunction<
- typeof createRemixRequestHandler
- >;
-
-function createMockEvent(event: Partial = {}): HandlerEvent {
- return {
- rawUrl: "http://localhost:3000/",
- rawQuery: "",
- path: "/",
- httpMethod: "GET",
- headers: {
- host: "localhost:3000",
- },
- multiValueHeaders: {},
- queryStringParameters: null,
- multiValueQueryStringParameters: null,
- body: null,
- isBase64Encoded: false,
- ...event,
- };
-}
-
-describe("netlify createRequestHandler", () => {
- describe("basic requests", () => {
- afterEach(() => {
- mockedCreateRequestHandler.mockReset();
- });
-
- afterAll(() => {
- jest.restoreAllMocks();
- });
-
- it("handles requests", async () => {
- mockedCreateRequestHandler.mockImplementation(() => async (req) => {
- return new Response(`URL: ${new URL(req.url).pathname}`);
- });
-
- // We don't have a real app to test, but it doesn't matter. We won't ever
- // call through to the real createRequestHandler
- // @ts-expect-error
- await lambdaTester(createRequestHandler({ build: undefined }))
- .event(createMockEvent({ rawUrl: "http://localhost:3000/foo/bar" }))
- .expectResolve((res) => {
- expect(res.statusCode).toBe(200);
- expect(res.body).toBe("URL: /foo/bar");
- });
- });
-
- it("handles root // requests", async () => {
- mockedCreateRequestHandler.mockImplementation(() => async (req) => {
- return new Response(`URL: ${new URL(req.url).pathname}`);
- });
-
- // We don't have a real app to test, but it doesn't matter. We won't ever
- // call through to the real createRequestHandler
- // @ts-expect-error
- await lambdaTester(createRequestHandler({ build: undefined }))
- .event(createMockEvent({ rawUrl: "http://localhost:3000//" }))
- .expectResolve((res) => {
- expect(res.statusCode).toBe(200);
- expect(res.body).toBe("URL: //");
- });
- });
-
- it("handles nested // requests", async () => {
- mockedCreateRequestHandler.mockImplementation(() => async (req) => {
- return new Response(`URL: ${new URL(req.url).pathname}`);
- });
-
- // We don't have a real app to test, but it doesn't matter. We won't ever
- // call through to the real createRequestHandler
- // @ts-expect-error
- await lambdaTester(createRequestHandler({ build: undefined }))
- .event(createMockEvent({ rawUrl: "http://localhost:3000//foo//bar" }))
- .expectResolve((res) => {
- expect(res.statusCode).toBe(200);
- expect(res.body).toBe("URL: //foo//bar");
- });
- });
-
- it("handles root // requests (development)", async () => {
- let oldEnv = process.env.NODE_ENV;
- process.env.NODE_ENV = "development";
-
- mockedCreateRequestHandler.mockImplementation(() => async (req) => {
- return new Response(`URL: ${new URL(req.url).pathname}`);
- });
-
- // We don't have a real app to test, but it doesn't matter. We won't ever
- // call through to the real createRequestHandler
- // @ts-expect-error
- await lambdaTester(createRequestHandler({ build: undefined }))
- .event(createMockEvent({ path: "//" }))
- .expectResolve((res) => {
- expect(res.statusCode).toBe(200);
- expect(res.body).toBe("URL: //");
- });
-
- process.env.NODE_ENV = oldEnv;
- });
-
- it("handles nested // requests (development)", async () => {
- let oldEnv = process.env.NODE_ENV;
- process.env.NODE_ENV = "development";
-
- mockedCreateRequestHandler.mockImplementation(() => async (req) => {
- return new Response(`URL: ${new URL(req.url).pathname}`);
- });
-
- // We don't have a real app to test, but it doesn't matter. We won't ever
- // call through to the real createRequestHandler
- // @ts-expect-error
- await lambdaTester(createRequestHandler({ build: undefined }))
- .event(createMockEvent({ path: "//foo//bar" }))
- .expectResolve((res) => {
- expect(res.statusCode).toBe(200);
- expect(res.body).toBe("URL: //foo//bar");
- });
-
- process.env.NODE_ENV = oldEnv;
- });
-
- it("handles null body", async () => {
- mockedCreateRequestHandler.mockImplementation(() => async () => {
- return new Response(null, { status: 200 });
- });
-
- // We don't have a real app to test, but it doesn't matter. We won't ever
- // call through to the real createRequestHandler
- // @ts-expect-error
- await lambdaTester(createRequestHandler({ build: undefined }))
- .event(createMockEvent({ rawUrl: "http://localhost:3000" }))
- .expectResolve((res) => {
- expect(res.statusCode).toBe(200);
- });
- });
-
- it("handles status codes", async () => {
- mockedCreateRequestHandler.mockImplementation(() => async () => {
- return new Response(null, { status: 204 });
- });
-
- // We don't have a real app to test, but it doesn't matter. We won't ever
- // call through to the real createRequestHandler
- // @ts-expect-error
- await lambdaTester(createRequestHandler({ build: undefined }))
- .event(createMockEvent({ rawUrl: "http://localhost:3000" }))
- .expectResolve((res) => {
- expect(res.statusCode).toBe(204);
- });
- });
-
- it("sets headers", async () => {
- mockedCreateRequestHandler.mockImplementation(() => async () => {
- let headers = new Headers({ "X-Time-Of-Year": "most wonderful" });
- headers.append(
- "Set-Cookie",
- "first=one; Expires=0; Path=/; HttpOnly; Secure; SameSite=Lax"
- );
- headers.append(
- "Set-Cookie",
- "second=two; MaxAge=1209600; Path=/; HttpOnly; Secure; SameSite=Lax"
- );
- headers.append(
- "Set-Cookie",
- "third=three; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/; HttpOnly; Secure; SameSite=Lax"
- );
-
- return new Response(null, { headers });
- });
-
- // We don't have a real app to test, but it doesn't matter. We won't ever
- // call through to the real createRequestHandler
- // @ts-expect-error
- await lambdaTester(createRequestHandler({ build: undefined }))
- .event(createMockEvent({ rawUrl: "http://localhost:3000" }))
- .expectResolve((res) => {
- expect(res.multiValueHeaders["x-time-of-year"]).toEqual([
- "most wonderful",
- ]);
- expect(res.multiValueHeaders["set-cookie"]).toEqual([
- "first=one; Expires=0; Path=/; HttpOnly; Secure; SameSite=Lax",
- "second=two; MaxAge=1209600; Path=/; HttpOnly; Secure; SameSite=Lax",
- "third=three; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/; HttpOnly; Secure; SameSite=Lax",
- ]);
- });
- });
- });
-});
-
-describe("netlify createRemixHeaders", () => {
- describe("creates fetch headers from netlify headers", () => {
- it("handles empty headers", () => {
- let headers = createRemixHeaders({});
- expect(headers.raw()).toMatchInlineSnapshot(`Object {}`);
- });
-
- it("handles simple headers", () => {
- let headers = createRemixHeaders({ "x-foo": ["bar"] });
- expect(headers.get("x-foo")).toBe("bar");
- });
-
- it("handles multiple headers", () => {
- let headers = createRemixHeaders({ "x-foo": ["bar"], "x-bar": ["baz"] });
- expect(headers.get("x-foo")).toBe("bar");
- expect(headers.get("x-bar")).toBe("baz");
- });
-
- it("handles headers with multiple values", () => {
- let headers = createRemixHeaders({
- "x-foo": ["bar", "baz"],
- "x-bar": ["baz"],
- });
- expect(headers.getAll("x-foo")).toEqual(["bar", "baz"]);
- expect(headers.get("x-bar")).toBe("baz");
- });
-
- it("handles multiple set-cookie headers", () => {
- let headers = createRemixHeaders({
- "set-cookie": [
- "__session=some_value; Path=/; Secure; HttpOnly; MaxAge=7200; SameSite=Lax",
- "__other=some_other_value; Path=/; Secure; HttpOnly; Expires=Wed, 21 Oct 2015 07:28:00 GMT; SameSite=Lax",
- ],
- });
- expect(headers.getAll("set-cookie")).toEqual([
- "__session=some_value; Path=/; Secure; HttpOnly; MaxAge=7200; SameSite=Lax",
- "__other=some_other_value; Path=/; Secure; HttpOnly; Expires=Wed, 21 Oct 2015 07:28:00 GMT; SameSite=Lax",
- ]);
- });
- });
-});
-
-describe("netlify createRemixRequest", () => {
- it("creates a request with the correct headers", () => {
- let remixRequest = createRemixRequest(
- createMockEvent({ multiValueHeaders: { Cookie: ["__session=value"] } })
- );
-
- expect(remixRequest.method).toBe("GET");
- expect(remixRequest.headers.get("cookie")).toBe("__session=value");
- });
-});
-
-describe("sendRemixResponse", () => {
- it("handles regular responses", async () => {
- let response = new NodeResponse("anything");
- let result = await sendRemixResponse(response);
- expect(result.body).toBe("anything");
- });
-
- it("handles resource routes with regular data", async () => {
- let json = JSON.stringify({ foo: "bar" });
- let response = new NodeResponse(json, {
- headers: {
- "Content-Type": "application/json",
- "content-length": json.length.toString(),
- },
- });
-
- let result = await sendRemixResponse(response);
-
- expect(result.body).toMatch(json);
- });
-
- it("handles resource routes with binary data", async () => {
- let image = await fsp.readFile(path.join(__dirname, "554828.jpeg"));
-
- let response = new NodeResponse(image, {
- headers: {
- "content-type": "image/jpeg",
- "content-length": image.length.toString(),
- },
- });
-
- let result = await sendRemixResponse(response);
-
- expect(result.body).toMatch(image.toString("base64"));
- });
-});
diff --git a/packages/remix-netlify/__tests__/setup.ts b/packages/remix-netlify/__tests__/setup.ts
deleted file mode 100644
index 917305ac938..00000000000
--- a/packages/remix-netlify/__tests__/setup.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-import { installGlobals } from "@remix-run/node";
-installGlobals();
diff --git a/packages/remix-netlify/binaryTypes.ts b/packages/remix-netlify/binaryTypes.ts
deleted file mode 100644
index fee3d9619b0..00000000000
--- a/packages/remix-netlify/binaryTypes.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-/**
- * Common binary MIME types
- * @see https://github.com/architect/functions/blob/45254fc1936a1794c185aac07e9889b241a2e5c6/src/http/helpers/binary-types.js
- */
-const binaryTypes = [
- "application/octet-stream",
- // Docs
- "application/epub+zip",
- "application/msword",
- "application/pdf",
- "application/rtf",
- "application/vnd.amazon.ebook",
- "application/vnd.ms-excel",
- "application/vnd.ms-powerpoint",
- "application/vnd.openxmlformats-officedocument.presentationml.presentation",
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
- // Fonts
- "font/otf",
- "font/woff",
- "font/woff2",
- // Images
- "image/avif",
- "image/bmp",
- "image/gif",
- "image/jpeg",
- "image/png",
- "image/tiff",
- "image/vnd.microsoft.icon",
- "image/webp",
- // Audio
- "audio/3gpp",
- "audio/aac",
- "audio/basic",
- "audio/mpeg",
- "audio/ogg",
- "audio/wav",
- "audio/webm",
- "audio/x-aiff",
- "audio/x-midi",
- "audio/x-wav",
- // Video
- "video/3gpp",
- "video/mp2t",
- "video/mpeg",
- "video/ogg",
- "video/quicktime",
- "video/webm",
- "video/x-msvideo",
- // Archives
- "application/java-archive",
- "application/vnd.apple.installer+xml",
- "application/x-7z-compressed",
- "application/x-apple-diskimage",
- "application/x-bzip",
- "application/x-bzip2",
- "application/x-gzip",
- "application/x-java-archive",
- "application/x-rar-compressed",
- "application/x-tar",
- "application/x-zip",
- "application/zip",
-];
-
-export function isBinaryType(contentType: string | null | undefined) {
- if (!contentType) return false;
- let [test] = contentType.split(";");
- return binaryTypes.includes(test);
-}
diff --git a/packages/remix-netlify/index.ts b/packages/remix-netlify/index.ts
deleted file mode 100644
index 508625524ff..00000000000
--- a/packages/remix-netlify/index.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-const alreadyWarned: Record = {};
-const warnOnce = (message: string, key = message) => {
- if (!alreadyWarned[key]) {
- alreadyWarned[key] = true;
- console.warn(message);
- }
-};
-
-warnOnce(
- "⚠️ REMIX FUTURE CHANGE: The `@remix-run/netlify` runtime adapter " +
- "has been deprecated in favor of `@netlify/remix-adapter` and will be " +
- "removed in Remix v2. Please update your code by changing all " +
- "`@remix-run/netlify` imports to `@netlify/remix-adapter`.\n Keep in " +
- "mind that `@netlify/remix-adapter` requires `@netlify/functions@^1.0.0`, " +
- "which is a breaking change compared to the current supported " +
- "`@netlify/functions` versions in `@remix-run/netlify`." +
- "official-netlify-adapter"
-);
-
-export type { GetLoadContextFunction, RequestHandler } from "./server";
-export { createRequestHandler } from "./server";
diff --git a/packages/remix-netlify/jest.config.js b/packages/remix-netlify/jest.config.js
deleted file mode 100644
index a882efcb8ae..00000000000
--- a/packages/remix-netlify/jest.config.js
+++ /dev/null
@@ -1,5 +0,0 @@
-/** @type {import('@jest/types').Config.InitialOptions} */
-module.exports = {
- ...require("../../jest/jest.config.shared"),
- displayName: "netlify",
-};
diff --git a/packages/remix-netlify/package.json b/packages/remix-netlify/package.json
deleted file mode 100644
index d90ddf3e38a..00000000000
--- a/packages/remix-netlify/package.json
+++ /dev/null
@@ -1,42 +0,0 @@
-{
- "name": "@remix-run/netlify",
- "version": "1.19.2",
- "description": "Netlify server request handler for Remix",
- "bugs": {
- "url": "https://github.com/remix-run/remix/issues"
- },
- "repository": {
- "type": "git",
- "url": "https://github.com/remix-run/remix",
- "directory": "packages/remix-netlify"
- },
- "license": "MIT",
- "main": "dist/index.js",
- "typings": "dist/index.d.ts",
- "dependencies": {
- "@remix-run/node": "1.19.2"
- },
- "devDependencies": {
- "@netlify/functions": "^1.0.0",
- "@types/node": "^18.17.1",
- "typescript": "^5.1.0"
- },
- "peerDependencies": {
- "@netlify/functions": "^1.0.0",
- "typescript": "^5.1.0"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
- },
- "engines": {
- "node": ">=18.0.0"
- },
- "files": [
- "dist/",
- "CHANGELOG.md",
- "LICENSE.md",
- "README.md"
- ]
-}
diff --git a/packages/remix-netlify/rollup.config.js b/packages/remix-netlify/rollup.config.js
deleted file mode 100644
index 127d95b5fef..00000000000
--- a/packages/remix-netlify/rollup.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-const { getAdapterConfig } = require("../../rollup.utils");
-
-/** @returns {import("rollup").RollupOptions[]} */
-module.exports = function rollup() {
- return [getAdapterConfig("netlify")];
-};
diff --git a/packages/remix-netlify/server.ts b/packages/remix-netlify/server.ts
deleted file mode 100644
index b2fc98d2cfb..00000000000
--- a/packages/remix-netlify/server.ts
+++ /dev/null
@@ -1,156 +0,0 @@
-import type {
- AppLoadContext,
- ServerBuild,
- RequestInit as NodeRequestInit,
- Response as NodeResponse,
-} from "@remix-run/node";
-import {
- createRequestHandler as createRemixRequestHandler,
- Headers as NodeHeaders,
- Request as NodeRequest,
- readableStreamToString,
-} from "@remix-run/node";
-import type {
- Handler,
- HandlerEvent,
- HandlerContext,
- HandlerResponse,
-} from "@netlify/functions";
-
-import { isBinaryType } from "./binaryTypes";
-
-/**
- * A function that returns the value to use as `context` in route `loader` and
- * `action` functions.
- *
- * You can think of this as an escape hatch that allows you to pass
- * environment/platform-specific values through to your loader/action.
- */
-export type GetLoadContextFunction = (
- event: HandlerEvent,
- context: HandlerContext
-) => Promise | AppLoadContext;
-
-export type RequestHandler = Handler;
-
-export function createRequestHandler({
- build,
- getLoadContext,
- mode = process.env.NODE_ENV,
-}: {
- build: ServerBuild;
- getLoadContext?: GetLoadContextFunction;
- mode?: string;
-}): RequestHandler {
- let handleRequest = createRemixRequestHandler(build, mode);
-
- return async (event, context) => {
- let request = createRemixRequest(event);
- let loadContext = await getLoadContext?.(event, context);
-
- let response = (await handleRequest(request, loadContext)) as NodeResponse;
-
- return sendRemixResponse(response);
- };
-}
-
-export function createRemixRequest(event: HandlerEvent): NodeRequest {
- let url: URL;
-
- if (process.env.NODE_ENV !== "development") {
- url = new URL(event.rawUrl);
- } else {
- let origin = event.headers.host;
- let rawPath = getRawPath(event);
- url = new URL(`http://${origin}${rawPath}`);
- }
-
- // 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) {
- let isFormData = event.headers["content-type"]?.includes(
- "multipart/form-data"
- );
- init.body = event.isBase64Encoded
- ? isFormData
- ? Buffer.from(event.body, "base64")
- : Buffer.from(event.body, "base64").toString()
- : event.body;
- }
-
- return new NodeRequest(url.href, init);
-}
-
-export function createRemixHeaders(
- requestHeaders: HandlerEvent["multiValueHeaders"]
-): NodeHeaders {
- let headers = new NodeHeaders();
-
- for (let [key, values] of Object.entries(requestHeaders)) {
- if (values) {
- for (let value of values) {
- headers.append(key, value);
- }
- }
- }
-
- return headers;
-}
-
-// `netlify dev` doesn't return the full url in the event.rawUrl, so we need to create it ourselves
-function getRawPath(event: HandlerEvent): string {
- let rawPath = event.path;
- let searchParams = new URLSearchParams();
-
- if (!event.multiValueQueryStringParameters) {
- return rawPath;
- }
-
- let paramKeys = Object.keys(event.multiValueQueryStringParameters);
- for (let key of paramKeys) {
- let values = event.multiValueQueryStringParameters[key];
- if (!values) continue;
- for (let val of values) {
- searchParams.append(key, val);
- }
- }
-
- let rawParams = searchParams.toString();
-
- if (rawParams) rawPath += `?${rawParams}`;
-
- return rawPath;
-}
-
-export async function sendRemixResponse(
- nodeResponse: NodeResponse
-): Promise {
- let contentType = nodeResponse.headers.get("Content-Type");
- let body: string | undefined;
- let isBase64Encoded = isBinaryType(contentType);
-
- if (nodeResponse.body) {
- if (isBase64Encoded) {
- body = await readableStreamToString(nodeResponse.body, "base64");
- } else {
- body = await nodeResponse.text();
- }
- }
-
- let multiValueHeaders = nodeResponse.headers.raw();
-
- return {
- statusCode: nodeResponse.status,
- multiValueHeaders,
- body,
- isBase64Encoded,
- };
-}
diff --git a/packages/remix-netlify/tsconfig.json b/packages/remix-netlify/tsconfig.json
deleted file mode 100644
index 318d0791405..00000000000
--- a/packages/remix-netlify/tsconfig.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "exclude": ["dist", "__tests__", "node_modules"],
- "compilerOptions": {
- "lib": ["ES2019", "DOM.Iterable"],
- "target": "ES2019",
- "skipLibCheck": true,
- "composite": true,
-
- "moduleResolution": "node",
- "allowSyntheticDefaultImports": true,
- "strict": true,
- "declaration": true,
- "emitDeclarationOnly": true,
- "rootDir": ".",
- "outDir": "../../build/node_modules/@remix-run/netlify/dist"
- }
-}
diff --git a/packages/remix-react/components.tsx b/packages/remix-react/components.tsx
index 10b6e64a337..97c1a010475 100644
--- a/packages/remix-react/components.tsx
+++ b/packages/remix-react/components.tsx
@@ -608,21 +608,18 @@ export function Meta() {
}
if ("script:ld+json" in metaProps) {
- let json: string | null = null;
try {
- json = JSON.stringify(metaProps["script:ld+json"]);
- } catch (err) {}
- return (
- json != null && (
+ let json = JSON.stringify(metaProps["script:ld+json"]);
+ return (
- )
- );
+ );
+ } catch (err) {
+ return null;
+ }
}
return ;
})}
diff --git a/packages/remix-react/links.ts b/packages/remix-react/links.ts
index 5f28146c944..dd47c652f15 100644
--- a/packages/remix-react/links.ts
+++ b/packages/remix-react/links.ts
@@ -221,12 +221,42 @@ export function getKeyedLinksForMatches(
return dedupeLinkDescriptors(descriptors, preloads);
}
+let stylesheetPreloadTimeouts = 0;
+let isPreloadDisabled = false;
+
export async function prefetchStyleLinks(
routeModule: RouteModule
): Promise {
if (!routeModule.links) return;
let descriptors = routeModule.links();
if (!descriptors) return;
+ if (isPreloadDisabled) return;
+
+ // If we've hit our timeout 3 times, we may be in firefox with the
+ // `network.preload` config disabled and we'll _never_ get onload/onerror
+ // callbacks. Let's try to confirm this with a totally invalid link preload
+ // which should immediately throw the onerror
+ if (stylesheetPreloadTimeouts >= 3) {
+ let linkLoadedOrErrored = await prefetchStyleLink({
+ rel: "preload",
+ as: "style",
+ href: "__remix-preload-detection-404.css",
+ });
+ if (linkLoadedOrErrored) {
+ // If this processed correctly, then our previous timeouts were probably
+ // legit, reset the counter.
+ stylesheetPreloadTimeouts = 0;
+ } else {
+ // If this bogus preload also times out without an onerror then it's safe
+ // to assume preloading is disabled and let's just stop trying. This
+ // _will_ cause FOUC on destination pages but there's nothing we can
+ // really do there if preloading is disabled since client-side injected
+ // scripts aren't render blocking. Maybe eventually React's client side
+ // async component stuff will provide an easier solution here
+ console.warn("Disabling preload due to lack of browser support");
+ isPreloadDisabled = true;
+ }
+ }
let styleLinks: HtmlLinkDescriptor[] = [];
for (let descriptor of descriptors) {
@@ -246,13 +276,12 @@ export async function prefetchStyleLinks(
(!link.media || window.matchMedia(link.media).matches) &&
!document.querySelector(`link[rel="stylesheet"][href="${link.href}"]`)
);
-
await Promise.all(matchingLinks.map(prefetchStyleLink));
}
async function prefetchStyleLink(
descriptor: HtmlLinkDescriptor
-): Promise {
+): Promise {
return new Promise((resolve) => {
let link = document.createElement("link");
Object.assign(link, descriptor);
@@ -266,16 +295,20 @@ async function prefetchStyleLink(
}
}
- link.onload = () => {
+ // Allow 3s for the link preload to timeout
+ let timeoutId = setTimeout(() => {
+ stylesheetPreloadTimeouts++;
removeLink();
- resolve();
- };
+ resolve(false);
+ }, 3_000);
- link.onerror = () => {
+ let done = () => {
+ clearTimeout(timeoutId);
removeLink();
- resolve();
+ resolve(true);
};
-
+ link.onload = done;
+ link.onerror = done;
document.head.appendChild(link);
});
}
diff --git a/packages/remix-react/package.json b/packages/remix-react/package.json
index ef64c08b35e..269b48b7978 100644
--- a/packages/remix-react/package.json
+++ b/packages/remix-react/package.json
@@ -21,7 +21,7 @@
},
"devDependencies": {
"@remix-run/server-runtime": "1.19.2",
- "@testing-library/jest-dom": "^5.16.2",
+ "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.3.0",
"@types/react": "^18.0.15",
"react": "^18.2.0",
diff --git a/packages/remix-server-runtime/__tests__/serialize-test.ts b/packages/remix-server-runtime/__tests__/serialize-test.ts
index c3ed3fb1ac0..7b5a38b35d1 100644
--- a/packages/remix-server-runtime/__tests__/serialize-test.ts
+++ b/packages/remix-server-runtime/__tests__/serialize-test.ts
@@ -1,52 +1,54 @@
import type { SerializeFrom } from "../index";
import { defer, json } from "../index";
-import type { IsNever } from "./utils";
import { isEqual } from "./utils";
-describe("SerializeFrom", () => {
- it("infers types", () => {
- isEqual, string>(true);
- isEqual, number>(true);
- isEqual, boolean>(true);
- isEqual, String>(true);
- isEqual, Number>(true);
- isEqual, Boolean>(true);
- isEqual, null>(true);
-
- isEqual>, true>(true);
- isEqual>, true>(true);
- isEqual>, true>(true);
-
- isEqual, []>(true);
- isEqual, [string, number]>(true);
- isEqual, [number, number]>(true);
-
- isEqual>, string[]>(true);
- isEqual>, null[]>(true);
-
- isEqual, { hello: "remix" }>(true);
- isEqual<
- SerializeFrom<{ data: { hello: "remix" } }>,
- { data: { hello: "remix" } }
- >(true);
- });
+it("infers basic types", () => {
+ isEqual<
+ SerializeFrom<{
+ hello?: string;
+ count: number | undefined;
+ date: Date | number;
+ isActive: boolean;
+ items: { name: string; price: number; orderedAt: Date }[];
+ }>,
+ {
+ hello?: string;
+ count?: number;
+ date: string | number;
+ isActive: boolean;
+ items: { name: string; price: number; orderedAt: string }[];
+ }
+ >(true);
+});
- it("infers type from json responses", () => {
- let loader = () => json({ hello: "remix" });
- isEqual, { hello: string }>(true);
+it("infers deferred types", () => {
+ let get = (): Promise | undefined => {
+ if (Math.random() > 0.5) return Promise.resolve(new Date());
+ return undefined;
+ };
+ let loader = async () =>
+ defer({
+ critical: await Promise.resolve("hello"),
+ deferred: get(),
+ });
+ isEqual<
+ SerializeFrom,
+ {
+ critical: string;
+ deferred: Promise | undefined;
+ }
+ >(true);
+});
- let asyncLoader = async () => json({ hello: "remix" });
- isEqual, { hello: string }>(true);
- });
+it("infers types from json", () => {
+ let loader = () => json({ data: "remix" });
+ isEqual, { data: string }>(true);
- it("infers type from defer responses", () => {
- let loader = async () => defer({ data: { hello: "remix" } });
- isEqual, { data: { hello: string } }>(true);
- });
+ let asyncLoader = async () => json({ data: "remix" });
+ isEqual, { data: string }>(true);
+});
- // Special case that covers https://github.com/remix-run/remix/issues/5211
- it("infers type from json responses containing a data key", () => {
- let loader = async () => json({ data: { hello: "remix" } });
- isEqual, { data: { hello: string } }>(true);
- });
+it("infers type from defer", () => {
+ let loader = async () => defer({ data: "remix" });
+ isEqual, { data: string }>(true);
});
diff --git a/packages/remix-server-runtime/__tests__/utils.ts b/packages/remix-server-runtime/__tests__/utils.ts
index 380b2423a32..27c55ce3bd0 100644
--- a/packages/remix-server-runtime/__tests__/utils.ts
+++ b/packages/remix-server-runtime/__tests__/utils.ts
@@ -93,5 +93,3 @@ export function prettyHtml(source: string): string {
export function isEqual(
arg: A extends B ? (B extends A ? true : false) : false
): void {}
-
-export type IsNever = [T] extends [never] ? true : false;
diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json
index 380bf47e21a..1c633b009e1 100644
--- a/packages/remix-server-runtime/package.json
+++ b/packages/remix-server-runtime/package.json
@@ -21,7 +21,8 @@
"@web3-storage/multipart-parser": "^1.0.0",
"cookie": "^0.4.1",
"set-cookie-parser": "^2.4.8",
- "source-map": "^0.7.3"
+ "source-map": "^0.7.3",
+ "type-fest": "^4.0.0"
},
"devDependencies": {
"@remix-run/web-file": "^3.0.3",
diff --git a/packages/remix-server-runtime/serialize.ts b/packages/remix-server-runtime/serialize.ts
index 6fd357b315c..fb35d54cace 100644
--- a/packages/remix-server-runtime/serialize.ts
+++ b/packages/remix-server-runtime/serialize.ts
@@ -1,87 +1,41 @@
+import type { Jsonify } from "type-fest";
+
import type { AppData } from "./data";
import type { TypedDeferredData, TypedResponse } from "./responses";
-type JsonPrimitive =
- | string
- | number
- | boolean
- | String
- | Number
- | Boolean
- | null;
-type NonJsonPrimitive = undefined | Function | symbol;
-
-/*
- * `any` is the only type that can let you equate `0` with `1`
- * See https://stackoverflow.com/a/49928360/1490091
- */
-type IsAny = 0 extends 1 & T ? true : false;
-
-// prettier-ignore
-type Serialize =
- IsAny extends true ? any :
- T extends TypedDeferredData ? SerializeDeferred :
- T extends JsonPrimitive ? T :
- T extends NonJsonPrimitive ? never :
- T extends { toJSON(): infer U } ? U :
- T extends [] ? [] :
- T extends [unknown, ...unknown[]] ? SerializeTuple :
- T extends ReadonlyArray ? (U extends NonJsonPrimitive ? null : Serialize)[] :
- T extends object ? SerializeObject> :
- never
-;
-
-/** JSON serialize [tuples](https://www.typescriptlang.org/docs/handbook/2/objects.html#tuple-types) */
-type SerializeTuple = T extends [infer F, ...infer R]
- ? [Serialize, ...SerializeTuple]
- : [];
-
-/** JSON serialize objects (not including arrays) and classes */
-type SerializeObject = {
- [k in keyof T as T[k] extends NonJsonPrimitive ? never : k]: Serialize;
-};
+// Note: The return value has to be `any` and not `unknown` so it can match `void`.
+type Fn = (...args: any[]) => any;
// prettier-ignore
-type SerializeDeferred> = {
- [k in keyof T as
- T[k] extends Promise ? k :
- T[k] extends NonJsonPrimitive ? never :
- k
- ]:
- T[k] extends Promise
- ? Promise> extends never ? "wtf" : Promise>
- : Serialize extends never ? k : Serialize;
-};
-
-/*
- * For an object T, if it has any properties that are a union with `undefined`,
- * make those into optional properties instead.
- *
- * Example: { a: string | undefined} --> { a?: string}
- */
-type UndefinedToOptional = {
- // Property is not a union with `undefined`, keep as-is
- [k in keyof T as undefined extends T[k] ? never : k]: T[k];
-} & {
- // Property _is_ a union with `defined`. Set as optional (via `?`) and remove `undefined` from the union
- [k in keyof T as undefined extends T[k] ? k : never]?: Exclude<
- T[k],
- undefined
- >;
-};
-
-type ArbitraryFunction = (...args: any[]) => unknown;
-
/**
* Infer JSON serialized data type returned by a loader or action.
*
* For example:
* `type LoaderData = SerializeFrom`
*/
-export type SerializeFrom = Serialize<
- T extends (...args: any[]) => infer Output
- ? Awaited