From a6224857cd6bfbdb0ca8738fc8bbbf710daf3606 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Fri, 28 Jul 2023 11:14:21 +1000 Subject: [PATCH] feat!: remove v1 headers behavior --- .changeset/v2-use-deepest-headers-function.md | 8 + docs/pages/api-development-strategy.md | 1 - docs/route/headers.md | 27 +-- integration/headers-test.ts | 155 ------------------ integration/hmr-log-test.ts | 1 - integration/hmr-test.ts | 1 - .../remix-dev/__tests__/readConfig-test.ts | 2 - packages/remix-dev/config.ts | 12 -- packages/remix-react/entry.ts | 1 - packages/remix-server-runtime/entry.ts | 1 - packages/remix-server-runtime/headers.ts | 5 +- packages/remix-testing/create-remix-stub.tsx | 1 - templates/arc/remix.config.js | 1 - templates/cloudflare-pages/remix.config.js | 1 - templates/cloudflare-workers/remix.config.js | 1 - templates/deno/remix.config.js | 1 - templates/express/remix.config.js | 1 - templates/fly/remix.config.js | 1 - templates/netlify/remix.config.js | 1 - templates/remix/remix.config.js | 1 - templates/vercel/remix.config.js | 1 - 21 files changed, 13 insertions(+), 211 deletions(-) create mode 100644 .changeset/v2-use-deepest-headers-function.md diff --git a/.changeset/v2-use-deepest-headers-function.md b/.changeset/v2-use-deepest-headers-function.md new file mode 100644 index 00000000000..ec3abeb590f --- /dev/null +++ b/.changeset/v2-use-deepest-headers-function.md @@ -0,0 +1,8 @@ +--- +"@remix-run/dev": major +"@remix-run/react": major +"@remix-run/server-runtime": major +"@remix-run/testing": major +--- + +Remove `v2_headers` flag. It is now the default behavior to use the deepest `headers` function in the route tree. diff --git a/docs/pages/api-development-strategy.md b/docs/pages/api-development-strategy.md index 60ed13b57ff..6a97192e760 100644 --- a/docs/pages/api-development-strategy.md +++ b/docs/pages/api-development-strategy.md @@ -53,7 +53,6 @@ The lifecycle is thus either: | Flag | Description | | -------------------- | --------------------------------------------------------------- | | `v2_dev` | Enable the new development server (including HMR/HDR support) | -| `v2_headers` | Leverage ancestor `headers` if children do not export `headers` | | `v2_meta` | Enable the new API for your `meta` functions | | `v2_routeConvention` | Enable the flat routes style of file-based routing | diff --git a/docs/route/headers.md b/docs/route/headers.md index 7df39e0b14f..e1f824414d3 100644 --- a/docs/route/headers.md +++ b/docs/route/headers.md @@ -38,7 +38,7 @@ If an action or a loader threw a `Response` and we're rendering a boundary, any ## Nested Routes -Because Remix has nested routes, there's a battle of the headers to be won when nested routes match. The default behavior is that Remix only leverages the resulting headers from the leaf rendered route. Consider these files in the routes directory: +Because Remix has nested routes, there's a battle of the headers to be won when nested routes match. The default behavior is that Remix only leverages the resulting headers from the deepest `headers` function it finds in the renderable matches (up to and including the boundary route if an error is present). ``` ├── users.tsx @@ -58,11 +58,9 @@ If we are looking at `/users/123/profile` then three routes are rendering: ``` -If all three define `headers`, the deepest module wins, in this case `profile.tsx`. However, if your `profile.tsx` loader threw and bubbled to a boundary in `userId.tsx` - then `userId.tsx`'s `headers` function would be used as it is the leaf rendered route. +If a user is looking at `/users/123/profile` and `profile.tsx` does not export a `headers` function, then Remix will use the return value of `$userId.tsx`'s `headers` function. If that file doesn't export one, then it will use the result of the one in `users.tsx`, and so on. - -We realize that it can be tedious and error-prone to have to define `headers` on every possible leaf route so we're changing the current behavior in v2 behind the [`future.v2_headers`][v2_headers] flag. - +If all three define `headers`, the deepest module wins, in this case `profile.tsx`. However, if your `profile.tsx` loader threw and bubbled to a boundary in `userId.tsx` - then `userId.tsx`'s `headers` function would be used as it is the leaf rendered route. We don't want surprise headers in your responses, so it's your job to merge them if you'd like. Remix passes in the `parentHeaders` to your `headers` function. So `users.tsx` headers get passed to `$userId.tsx`, and then `$userId.tsx` headers are passed to `profile.tsx` headers. @@ -131,25 +129,6 @@ export default function handleRequest( Just keep in mind that doing this will apply to _all_ document requests, but does not apply to `data` requests (for client-side transitions for example). For those, use [`handleDataRequest`][handledatarequest]. -## v2 Behavior - -Since it can be tedious and error-prone to define a `header` function in every single possible leaf route, we're changing the behavior slightly in v2 and you can opt-into the new behavior via the `future.v2_headers` [Future Flag][future-flags] in `remix.config.js`. - -When enabling this flag, Remix will now use the deepest `headers` function it finds in the renderable matches (up to and including the boundary route if an error is present). You'll still need to handle merging together headers as shown above for any `headers` functions above this route. - -This means that, re-using the example above: - -``` -├── users.tsx -└── users - ├── $userId.tsx - └── $userId - └── profile.tsx -``` - -If a user is looking at `/users/123/profile` and `profile.tsx` does not export a `headers` function, then Remix will use the return value of `$userId.tsx`'s `headers` function. If that file doesn't export one, then it will use the result of the one in `users.tsx`, and so on. [headers]: https://developer.mozilla.org/en-US/docs/Web/API/Headers [handledatarequest]: ../file-conventions/entry.server -[v2_headers]: #v2-behavior -[future-flags]: ../pages/api-development-strategy diff --git a/integration/headers-test.ts b/integration/headers-test.ts index 17955377815..5365d85cc53 100644 --- a/integration/headers-test.ts +++ b/integration/headers-test.ts @@ -18,8 +18,6 @@ test.describe("headers export", () => { config: { future: { v2_routeConvention: true, - - v2_headers: true, }, }, files: { @@ -429,156 +427,3 @@ test.describe("headers export", () => { ); }); }); - -test.describe("v1 behavior (future.v2_headers=false)", () => { - let appFixture: Fixture; - - test.beforeAll(async () => { - appFixture = await createFixture( - { - config: { - future: { - v2_routeConvention: true, - - v2_headers: false, - }, - }, - files: { - "app/root.jsx": js` - import { json } from "@remix-run/node"; - import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; - - export const loader = () => json({}); - - export default function Root() { - return ( - - - - - - - - - - - ); - } - `, - - "app/routes/parent.jsx": js` - export function headers({ actionHeaders, errorHeaders, loaderHeaders, parentHeaders }) { - return new Headers([ - ...(parentHeaders ? Array.from(parentHeaders.entries()) : []), - ...(actionHeaders ? Array.from(actionHeaders.entries()) : []), - ...(loaderHeaders ? Array.from(loaderHeaders.entries()) : []), - ...(errorHeaders ? Array.from(errorHeaders.entries()) : []), - ]); - } - - export function loader({ request }) { - return new Response(null, { - headers: { 'X-Parent-Loader': 'success' }, - }) - } - - export default function Component() { return
} - `, - - "app/routes/parent.child.jsx": js` - export async function action({ request }) { - return null; - } - - export default function Component() { return
} - `, - - "app/routes/cookie.jsx": js` - import { json } from "@remix-run/server-runtime"; - import { Outlet } from "@remix-run/react"; - - export function loader({ request }) { - if (new URL(request.url).searchParams.has("parent-throw")) { - throw json(null, { headers: { "Set-Cookie": "parent-thrown-cookie=true" } }); - } - return null - }; - - export default function Parent() { - return ; - } - - export function ErrorBoundary() { - return

Caught!

; - } - `, - - "app/routes/cookie.child.jsx": js` - import { json } from "@remix-run/node"; - - export function loader({ request }) { - if (new URL(request.url).searchParams.has("throw")) { - throw json(null, { headers: { "Set-Cookie": "thrown-cookie=true" } }); - } - return json(null, { - headers: { "Set-Cookie": "normal-cookie=true" }, - }); - }; - - export default function Child() { - return

Child

; - } - `, - }, - }, - ServerMode.Test - ); - }); - - test("returns no headers when the leaf route doesn't export a header function (GET)", async () => { - let response = await appFixture.requestDocument("/parent/child"); - expect(JSON.stringify(Array.from(response.headers.entries()))).toBe( - JSON.stringify([["content-type", "text/html"]]) - ); - }); - - test("returns no headers when the leaf route doesn't export a header function (POST)", async () => { - let response = await appFixture.postDocument( - "/parent/child", - new URLSearchParams() - ); - expect(JSON.stringify(Array.from(response.headers.entries()))).toBe( - JSON.stringify([["content-type", "text/html"]]) - ); - }); - - test("automatically includes cookie headers from normal responses", async () => { - let response = await appFixture.requestDocument("/cookie/child"); - expect(JSON.stringify(Array.from(response.headers.entries()))).toBe( - JSON.stringify([ - ["content-type", "text/html"], - ["set-cookie", "normal-cookie=true"], - ]) - ); - }); - - test("automatically includes cookie headers from thrown responses", async () => { - let response = await appFixture.requestDocument("/cookie/child?throw"); - expect(JSON.stringify(Array.from(response.headers.entries()))).toBe( - JSON.stringify([ - ["content-type", "text/html"], - ["set-cookie", "thrown-cookie=true"], - ]) - ); - }); - - test("does not duplicate thrown cookie headers from boundary route", async () => { - let response = await appFixture.requestDocument("/cookie?parent-throw"); - expect(JSON.stringify(Array.from(response.headers.entries()))).toBe( - JSON.stringify([ - ["content-type", "text/html"], - ["set-cookie", "parent-thrown-cookie=true"], - ]) - ); - }); -}); diff --git a/integration/hmr-log-test.ts b/integration/hmr-log-test.ts index 1691476cf5d..590ff204533 100644 --- a/integration/hmr-log-test.ts +++ b/integration/hmr-log-test.ts @@ -20,7 +20,6 @@ let fixture = (options: { appPort: number; devPort: number }): FixtureInit => ({ v2_routeConvention: true, v2_meta: true, - v2_headers: true, }, }, files: { diff --git a/integration/hmr-test.ts b/integration/hmr-test.ts index 8df8a747317..704e4257ceb 100644 --- a/integration/hmr-test.ts +++ b/integration/hmr-test.ts @@ -20,7 +20,6 @@ let fixture = (options: { appPort: number; devPort: number }): FixtureInit => ({ v2_routeConvention: true, v2_meta: true, - v2_headers: true, }, }, files: { diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index da6bbd772be..76a4fcb3602 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -25,7 +25,6 @@ describe("readConfig", () => { entryServerFilePath: expect.any(String), tsconfigPath: expect.any(String), future: { - v2_headers: expect.any(Boolean), v2_meta: expect.any(Boolean), v2_routeConvention: expect.any(Boolean), }, @@ -43,7 +42,6 @@ describe("readConfig", () => { "entryServerFilePath": Any, "future": Object { "v2_dev": false, - "v2_headers": Any, "v2_meta": Any, "v2_routeConvention": Any, }, diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index 609ce31691f..9b22577bf76 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -38,7 +38,6 @@ type Dev = { interface FutureConfig { v2_dev: boolean | Dev; - v2_headers: boolean; v2_meta: boolean; v2_routeConvention: boolean; } @@ -401,10 +400,6 @@ export async function readConfig( metaWarning(); } - if (!appConfig.future?.v2_headers) { - headersWarning(); - } - let serverBuildPath = path.resolve( rootDirectory, appConfig.serverBuildPath ?? "build/index.js" @@ -653,7 +648,6 @@ export async function readConfig( let future: FutureConfig = { v2_dev: appConfig.future?.v2_dev ?? false, - v2_headers: appConfig.future?.v2_headers === true, v2_meta: appConfig.future?.v2_meta === true, v2_routeConvention: appConfig.future?.v2_routeConvention === true, }; @@ -793,9 +787,3 @@ let metaWarning = futureFlagWarning({ flag: "v2_meta", link: "https://remix.run/docs/en/v1.15.0/pages/v2#meta", }); - -let headersWarning = futureFlagWarning({ - message: "The route `headers` API is changing in v2", - flag: "v2_headers", - link: "https://remix.run/docs/en/v1.17.0/pages/v2#route-headers", -}); diff --git a/packages/remix-react/entry.ts b/packages/remix-react/entry.ts index d1513a7f6b3..d709accd982 100644 --- a/packages/remix-react/entry.ts +++ b/packages/remix-react/entry.ts @@ -34,7 +34,6 @@ type Dev = { export interface FutureConfig { v2_dev: boolean | Dev; - v2_headers: boolean; v2_meta: boolean; v2_routeConvention: boolean; } diff --git a/packages/remix-server-runtime/entry.ts b/packages/remix-server-runtime/entry.ts index eba9031e093..e668ce24492 100644 --- a/packages/remix-server-runtime/entry.ts +++ b/packages/remix-server-runtime/entry.ts @@ -22,7 +22,6 @@ type Dev = { export interface FutureConfig { v2_dev: boolean | Dev; - v2_headers: boolean; v2_meta: boolean; v2_routeConvention: boolean; } diff --git a/packages/remix-server-runtime/headers.ts b/packages/remix-server-runtime/headers.ts index 12bd2d66d99..d65644a0551 100644 --- a/packages/remix-server-runtime/headers.ts +++ b/packages/remix-server-runtime/headers.ts @@ -50,9 +50,8 @@ export function getDocumentHeadersRR( errorHeaders !== loaderHeaders && errorHeaders !== actionHeaders; - // When the future flag is enabled, use the parent headers for any route - // that doesn't have a `headers` export - if (routeModule.headers == null && build.future.v2_headers) { + // Use the parent headers for any route without a `headers` export + if (routeModule.headers == null) { let headers = new Headers(parentHeaders); if (includeErrorCookies) { prependCookies(errorHeaders!, headers); diff --git a/packages/remix-testing/create-remix-stub.tsx b/packages/remix-testing/create-remix-stub.tsx index 27cbd7fa6a8..b88474a7926 100644 --- a/packages/remix-testing/create-remix-stub.tsx +++ b/packages/remix-testing/create-remix-stub.tsx @@ -125,7 +125,6 @@ export function createRemixStub( remixContextRef.current = { future: { v2_dev: false, - v2_headers: false, v2_meta: false, v2_routeConvention: false, ...remixConfigFuture, diff --git a/templates/arc/remix.config.js b/templates/arc/remix.config.js index 310cfa560fd..9e9d6e0486d 100644 --- a/templates/arc/remix.config.js +++ b/templates/arc/remix.config.js @@ -10,7 +10,6 @@ export default { future: { v2_dev: true, - v2_headers: true, v2_meta: true, v2_routeConvention: true, }, diff --git a/templates/cloudflare-pages/remix.config.js b/templates/cloudflare-pages/remix.config.js index c5005a25e29..2e60562af38 100644 --- a/templates/cloudflare-pages/remix.config.js +++ b/templates/cloudflare-pages/remix.config.js @@ -16,7 +16,6 @@ export default { future: { v2_dev: true, - v2_headers: true, v2_meta: true, v2_routeConvention: true, }, diff --git a/templates/cloudflare-workers/remix.config.js b/templates/cloudflare-workers/remix.config.js index b5866e0a1b4..e142f30e45b 100644 --- a/templates/cloudflare-workers/remix.config.js +++ b/templates/cloudflare-workers/remix.config.js @@ -18,7 +18,6 @@ export default { future: { v2_dev: true, - v2_headers: true, v2_meta: true, v2_routeConvention: true, }, diff --git a/templates/deno/remix.config.js b/templates/deno/remix.config.js index deee56e1864..1b13228ec15 100644 --- a/templates/deno/remix.config.js +++ b/templates/deno/remix.config.js @@ -19,7 +19,6 @@ module.exports = { // serverBuildPath: "build/index.js", // publicPath: "/build/", future: { - v2_headers: true, v2_meta: true, v2_routeConvention: true, }, diff --git a/templates/express/remix.config.js b/templates/express/remix.config.js index ccaabb773a4..9f48a0e0b0f 100644 --- a/templates/express/remix.config.js +++ b/templates/express/remix.config.js @@ -9,7 +9,6 @@ export default { future: { v2_dev: true, - v2_headers: true, v2_meta: true, v2_routeConvention: true, }, diff --git a/templates/fly/remix.config.js b/templates/fly/remix.config.js index 0bd3fd95c8a..79f068a2775 100644 --- a/templates/fly/remix.config.js +++ b/templates/fly/remix.config.js @@ -9,7 +9,6 @@ module.exports = { future: { v2_dev: true, - v2_headers: true, v2_meta: true, v2_routeConvention: true, }, diff --git a/templates/netlify/remix.config.js b/templates/netlify/remix.config.js index abf7871a9fc..9601e7bf407 100644 --- a/templates/netlify/remix.config.js +++ b/templates/netlify/remix.config.js @@ -13,7 +13,6 @@ module.exports = { future: { v2_dev: true, - v2_headers: true, v2_meta: true, v2_routeConvention: true, }, diff --git a/templates/remix/remix.config.js b/templates/remix/remix.config.js index 0bd3fd95c8a..79f068a2775 100644 --- a/templates/remix/remix.config.js +++ b/templates/remix/remix.config.js @@ -9,7 +9,6 @@ module.exports = { future: { v2_dev: true, - v2_headers: true, v2_meta: true, v2_routeConvention: true, }, diff --git a/templates/vercel/remix.config.js b/templates/vercel/remix.config.js index 564f8cee51d..e1d1566d20a 100644 --- a/templates/vercel/remix.config.js +++ b/templates/vercel/remix.config.js @@ -13,7 +13,6 @@ module.exports = { future: { v2_dev: true, - v2_headers: true, v2_meta: true, v2_routeConvention: true, },