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 ( +
+ example img +
+ ) + } + `, + }, + }); + + 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 (