From 5cfd3c3cbec2e0e91a095ec9c18fc34750633d88 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 5 Sep 2024 09:54:06 -0400 Subject: [PATCH] Update/fix Single Fetch revalidation behavior (#9938) --- .changeset/flat-wasps-heal.md | 14 + docs/guides/single-fetch.md | 34 +- integration/error-boundary-v2-test.ts | 2 +- integration/package.json | 2 +- integration/single-fetch-test.ts | 947 +++++++++++++++++- packages/remix-dev/package.json | 2 +- packages/remix-react/browser.tsx | 3 +- packages/remix-react/package.json | 6 +- packages/remix-react/single-fetch.tsx | 313 ++++-- packages/remix-server-runtime/data.ts | 25 +- packages/remix-server-runtime/package.json | 2 +- packages/remix-server-runtime/single-fetch.ts | 28 +- packages/remix-testing/package.json | 4 +- pnpm-lock.yaml | 260 +++-- 14 files changed, 1458 insertions(+), 184 deletions(-) create mode 100644 .changeset/flat-wasps-heal.md diff --git a/.changeset/flat-wasps-heal.md b/.changeset/flat-wasps-heal.md new file mode 100644 index 00000000000..1fc73acc63d --- /dev/null +++ b/.changeset/flat-wasps-heal.md @@ -0,0 +1,14 @@ +--- +"@remix-run/react": patch +"@remix-run/server-runtime": patch +--- + +Single Fetch - fix revalidation behavior bugs + + - With Single Fetch, existing routes revalidate by default + - This means requests do not need special query params for granular route revalidations out of the box - i.e., `GET /a/b/c.data` + - There are two conditions that will trigger granular revalidation: + - If a route opts out of revalidation via `shouldRevalidate`, it will be excluded from the single fetch call + - If a route defines a `clientLoader` then it will be excluded from the single fetch call and if you call `serverLoader()` from your `clientLoader`, that will make a separarte HTTP call for just that route loader - i.e., `GET /a/b/c.data?_routes=routes/a` for a `clientLoader` in `routes/a.tsx` + - When one or more routes are excluded from the single fetch call, the remaining routes that have loaders are included as query params: + - For example, if A was excluded, and the `root` route and `routes/b` had a `loader` but `routes/c` did not, the single fetch request would be `GET /a/b/c.data?_routes=root,routes/a` diff --git a/docs/guides/single-fetch.md b/docs/guides/single-fetch.md index d68857f339b..d52d7648e07 100644 --- a/docs/guides/single-fetch.md +++ b/docs/guides/single-fetch.md @@ -86,6 +86,7 @@ There are a handful of breaking changes introduced with Single Fetch - some of w - Add `@remix-run/react/future/single-fetch.d.ts` to the end of your `tsconfig.json`'s `compilerOptions.types` array - Begin using `unstable_defineLoader`/`unstable_defineAction` in your routes - This can be done incrementally - you should have _mostly_ accurate type inference in your current state +- [**Default revalidation behavior changes to opt-out on GET navigations**][revalidation]: Default revalidation behavior on normal navigations changes from opt-in to opt-out and your server loaders will re-run by default - [**Opt-in `action` revalidation**][action-revalidation]: Revalidation after an `action` `4xx`/`5xx` `Response` is now opt-in, versus opt-out ## Adding a New Route with Single Fetch @@ -427,6 +428,32 @@ function handleBrowserRequest( ### Revalidations +#### Normal Navigation Behavior + +In addition to the simpler mental model and the alignment of document and data requests, another benefit of Single Fetch is simpler (and hopefully better) caching behavior. Generally, Single Fetch will make fewer HTTP requests and hopefully cache those results more frequently compared to the previous multiple-fetch behavior. + +To reduce cache fragmentation, Single Fetch changes the default revalidation behavior on GET navigations. Previously, Remix would not re-run loaders for reused ancestor routes unless you opted-in via `shouldRevalidate`. Now, Remix _will_ re-run those by default in the simple case for a Single Fetch request like `GET /a/b/c.data`. If you do not have any `shouldRevalidate` or `clientLoader` functions, this will be the behavior for your app. + +Adding either a `shouldRevalidate` or a `clientLoader` to any of the active routes will trigger granular Single Fetch calls that include a `_routes` parameter specifying the subset of routes to run. + +If a `clientLoader` calls `serverLoader()` internally, that will trigger a separate HTTP call for that specific route, akin to the old behavior. + +For example, if you are on `/a/b` and you navigate to `/a/b/c`: + +- When no `shouldRevalidate` or `clientLoader` functions exist: `GET /a/b/c.data` +- If all routes have loaders but `routes/a` opts out via `shouldRevalidate`: + - `GET /a/b/c.data?_routes=root,routes/b,routes/c` +- If all routes have loaders but `routes/b` has a `clientLoader`: + - `GET /a/b/c.data?_routes=root,routes/a,routes/c` + - And then if B's `clientLoader` calls `serverLoader()`: + - `GET /a/b/c.data?_routes=routes/b` + +If this new behavior is sub-optimal for your application, you should be able to opt-back into the old behavior of not-revalidating by adding a `shouldRevalidate` that returns `false` in the desired scenarios to your parent routes. + +Another option is to leverage a server-side cache for expensive parent loader calculations. + +#### Submission Revalidation Behavior + Previously, Remix would always revalidate all active loaders after _any_ action submission, regardless of the result of the action. You could opt-out of revalidation on a per-route basis via [`shouldRevalidate`][should-revalidate]. With Single Fetch, if an `action` returns or throws a `Response` with a `4xx/5xx` status code, Remix will _not revalidate_ loaders by default. If an `action` returns or throws anything that is not a 4xx/5xx Response, then the revalidation behavior is unchanged. The reasoning here is that in most cases, if you return a `4xx`/`5xx` Response, you didn't actually mutate any data so there is no need to reload data. @@ -458,9 +485,14 @@ Revalidation is handled via a `?_routes` query string parameter on the single fe [merging-remix-and-rr]: https://remix.run/blog/merging-remix-and-react-router [migration-guide]: #migrating-a-route-with-single-fetch [breaking-changes]: #breaking-changes -[action-revalidation]: #streaming-data-format +[revalidation]: #normal-navigation-behavior +[action-revalidation]: #submission-revalidation-behavior [start]: #enabling-single-fetch [type-inference-section]: #type-inference [compatibility-flag]: https://developers.cloudflare.com/workers/configuration/compatibility-dates [data-utility]: ../utils/data [augment]: https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation + +``` + +``` diff --git a/integration/error-boundary-v2-test.ts b/integration/error-boundary-v2-test.ts index 3b4bab581c0..8d06573154e 100644 --- a/integration/error-boundary-v2-test.ts +++ b/integration/error-boundary-v2-test.ts @@ -416,7 +416,7 @@ test.describe("single fetch", () => { await waitForAndAssert( page, app, - "#child-error", + "#parent-error", "Unable to decode turbo-stream response from URL" ); }); diff --git a/integration/package.json b/integration/package.json index bedeb62ea6f..90c677e4844 100644 --- a/integration/package.json +++ b/integration/package.json @@ -14,7 +14,7 @@ "@remix-run/dev": "workspace:*", "@remix-run/express": "workspace:*", "@remix-run/node": "workspace:*", - "@remix-run/router": "0.0.0-experimental-7d87ffb8c", + "@remix-run/router": "1.19.2-pre.0", "@remix-run/server-runtime": "workspace:*", "@types/express": "^4.17.9", "@vanilla-extract/css": "^1.10.0", diff --git a/integration/single-fetch-test.ts b/integration/single-fetch-test.ts index 503ec75689b..ec3de5e2ae8 100644 --- a/integration/single-fetch-test.ts +++ b/integration/single-fetch-test.ts @@ -379,15 +379,18 @@ test.describe("single-fetch", () => { test("loads proper data on client side action navigation", async ({ page, }) => { - let fixture = await createFixture({ - config: { - future: { - unstable_singleFetch: true, + let fixture = await createFixture( + { + config: { + future: { + unstable_singleFetch: true, + }, }, + files, }, - files, - }); - let appFixture = await createAppFixture(fixture); + ServerMode.Development + ); + let appFixture = await createAppFixture(fixture, ServerMode.Development); let app = new PlaywrightFixture(appFixture, page); await app.goto("/"); await app.clickSubmitButton("/data"); @@ -672,6 +675,7 @@ test.describe("single-fetch", () => { await page.waitForSelector("#error"); expect(urls).toEqual([]); }); + test("returns headers correctly for singular loader and action calls", async () => { let fixture = await createFixture({ config: { @@ -1714,15 +1718,16 @@ test.describe("single-fetch", () => { test("allows fetcher to hit resource route and return via turbo stream", async ({ page, }) => { - let fixture = await createFixture({ - config: { - future: { - unstable_singleFetch: true, + let fixture = await createFixture( + { + config: { + future: { + unstable_singleFetch: true, + }, }, - }, - files: { - ...files, - "app/routes/_index.tsx": js` + files: { + ...files, + "app/routes/_index.tsx": js` import { useFetcher } from "@remix-run/react"; export default function Component() { @@ -1737,7 +1742,7 @@ test.describe("single-fetch", () => { ); } `, - "app/routes/resource.tsx": js` + "app/routes/resource.tsx": js` export function loader() { // Fetcher calls to resource routes will append ".data" and we'll go through // the turbo-stream flow. If a user were to curl this endpoint they'd go @@ -1748,9 +1753,11 @@ test.describe("single-fetch", () => { }; } `, + }, }, - }); - let appFixture = await createAppFixture(fixture); + ServerMode.Development + ); + let appFixture = await createAppFixture(fixture, ServerMode.Development); let app = new PlaywrightFixture(appFixture, page); await app.goto("/"); await app.clickElement("#load"); @@ -1760,6 +1767,617 @@ test.describe("single-fetch", () => { ); }); + test("Strips ?_routes query param from loader/action requests", async ({ + page, + }) => { + let fixture = await createFixture( + { + config: { + future: { + unstable_singleFetch: true, + }, + }, + files: { + ...files, + "app/routes/_index.tsx": js` + import { Link } from '@remix-run/react'; + export default function Component() { + return Go to /parent/a; + } + `, + "app/routes/parent.tsx": js` + import { Link, Outlet, useLoaderData } from '@remix-run/react'; + export function loader({ request }) { + return { url: request.url }; + } + export default function Component() { + return ( + <> +

Parent loader URL: {useLoaderData().url}

+ + + ); + } + `, + "app/routes/parent.a.tsx": js` + import { useLoaderData } from '@remix-run/react'; + export function loader({ request }) { + return { url: request.url }; + } + export async function clientLoader({ request, serverLoader }) { + let serverData = await serverLoader(); + return { + serverUrl: serverData.url, + clientUrl: request.url + } + } + export default function Component() { + let data = useLoaderData(); + return ( + <> +

A server loader URL: {data.serverUrl}

+

A client loader URL: {data.clientUrl}

+ + ); + } + `, + }, + }, + ServerMode.Development + ); + let appFixture = await createAppFixture(fixture, ServerMode.Development); + let app = new PlaywrightFixture(appFixture, page); + + let urls: string[] = []; + page.on("request", (req) => { + if (req.url().includes(".data")) { + urls.push(req.url()); + } + }); + + await app.goto("/"); + + await app.clickLink("/parent/a"); + await page.waitForSelector("#a-server"); + + // HTTP Requests contained routes params + expect(urls.length).toBe(2); + expect(urls[0].endsWith("/parent/a.data?_routes=routes%2Fparent.a")).toBe( + true + ); + expect( + urls[1].endsWith("/parent/a.data?_routes=root%2Croutes%2Fparent") + ).toBe(true); + + // But loaders don't receive any routes params + expect(await app.getHtml("#parent")).toMatch( + />Parent loader URL: http:\/\/localhost:\d+\/parent\/aA server loader URL: http:\/\/localhost:\d+\/parent\/aA client loader URL: http:\/\/localhost:\d+\/parent\/a { + let fixture = await createFixture( + { + config: { + future: { + unstable_singleFetch: true, + }, + }, + files: { + ...files, + "app/routes/page.tsx": js` + import { Form, useActionData, useLoaderData } from '@remix-run/react'; + let count = 0; + export function loader({ request }) { + return { count: ++count }; + } + export function action({ request }) { + return { message: "ACTION" }; + } + export default function Component() { + let data = useLoaderData(); + let actionData = useActionData(); + return ( + <> +

{"Count:" + data.count}

+
+ + {actionData ?

{actionData.message}

: null} +
+ + ) + } + `, + }, + }, + ServerMode.Development + ); + let appFixture = await createAppFixture(fixture, ServerMode.Development); + let app = new PlaywrightFixture(appFixture, page); + + let urls: string[] = []; + page.on("request", (req) => { + if (req.url().includes(".data")) { + urls.push(req.method() + " " + req.url()); + } + }); + + await app.goto("/page"); + expect(await app.getHtml("#data")).toContain("Count:1"); + + await app.clickSubmitButton("/page"); + await page.waitForSelector("#action"); + expect(await app.getHtml("#data")).toContain("Count:2"); + + // HTTP Requests contained routes params + expect(urls).toEqual([ + expect.stringMatching(/POST .*\/page.data$/), + expect.stringMatching(/GET .*\/page.data$/), + ]); + }); + + test.describe("revalidations/_routes param", () => { + test("does not make a server call if no loaders need to run", async ({ + page, + }) => { + let fixture = await createFixture( + { + config: { + future: { + unstable_singleFetch: true, + }, + }, + files: { + "app/root.tsx": js` + import { Link, Links, Meta, Outlet, Scripts } from "@remix-run/react"; + + export default function Root() { + return ( + + + + + + + Home
+ /a/b
+ + + + + ); + } + `, + "app/routes/a.tsx": js` + import { Outlet } from "@remix-run/react"; + + export default function Root() { + return ; + } + `, + "app/routes/a.b.tsx": js` + export default function Root() { + return

B

; + } + `, + }, + }, + ServerMode.Development + ); + + let urls: string[] = []; + page.on("request", (req) => { + if (req.method() === "GET" && req.url().includes(".data")) { + urls.push(req.url()); + } + }); + + let appFixture = await createAppFixture(fixture, ServerMode.Development); + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + + await app.clickLink("/a/b"); + await page.waitForSelector("h1"); + expect(await app.getHtml("h1")).toBe("

B

"); + expect(urls.length).toBe(0); + }); + + test("calls reused parent routes by default", async ({ page }) => { + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, + }, + }, + files: { + ...files, + "app/routes/_index.tsx": js` + import { Link } from '@remix-run/react'; + export default function Component() { + return Go to /parent/a; + } + `, + "app/routes/parent.tsx": js` + import { Link, Outlet, useLoaderData } from '@remix-run/react'; + let count = 0; + export function loader({ request }) { + return { count: ++count }; + } + export default function Component() { + return ( + <> +

Parent Count: {useLoaderData().count}

+ Go to /parent/a + Go to /parent/b + + + ); + } + `, + "app/routes/parent.a.tsx": js` + import { useLoaderData } from '@remix-run/react'; + let count = 0; + export function loader({ request }) { + return { count: ++count }; + } + export default function Component() { + return

A Count: {useLoaderData().count}

; + } + `, + "app/routes/parent.b.tsx": js` + import { useLoaderData } from '@remix-run/react'; + let count = 0; + export function loader({ request }) { + return { count: ++count }; + } + export default function Component() { + return

B Count: {useLoaderData().count}

; + } + `, + }, + }); + let appFixture = await createAppFixture(fixture); + let app = new PlaywrightFixture(appFixture, page); + + let urls: string[] = []; + page.on("request", (req) => { + if (req.url().includes(".data")) { + urls.push(req.url()); + } + }); + + await app.goto("/"); + + await app.clickLink("/parent/a"); + await page.waitForSelector("#a"); + expect(await app.getHtml("#parent")).toContain("Parent Count: 1"); + expect(await app.getHtml("#a")).toContain("A Count: 1"); + expect(urls.length).toBe(1); + expect(urls[0].endsWith("/parent/a.data")).toBe(true); + urls = []; + + await app.clickLink("/parent/b"); + await page.waitForSelector("#b"); + expect(await app.getHtml("#parent")).toContain("Parent Count: 2"); + expect(await app.getHtml("#b")).toContain("B Count: 1"); + expect(urls.length).toBe(1); + expect(urls[0].endsWith("/parent/b.data")).toBe(true); + urls = []; + + await app.clickLink("/parent/a"); + await page.waitForSelector("#a"); + expect(await app.getHtml("#parent")).toContain("Parent Count: 3"); + expect(await app.getHtml("#a")).toContain("A Count: 2"); + expect(urls.length).toBe(1); + expect(urls[0].endsWith("/parent/a.data")).toBe(true); + }); + + test("allows reused routes to opt out via shouldRevalidate", async ({ + page, + }) => { + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, + }, + }, + files: { + ...files, + "app/routes/_index.tsx": js` + import { Link } from '@remix-run/react'; + export default function Component() { + return Go to /parent/a; + } + `, + "app/routes/parent.tsx": js` + import { Link, Outlet, useLoaderData } from '@remix-run/react'; + let count = 0; + export function loader({ request }) { + return { count: ++count }; + } + export function shouldRevalidate() { + return false; + } + export default function Component() { + return ( + <> +

Parent Count: {useLoaderData().count}

+ Go to /parent/a + Go to /parent/b + + + ); + } + `, + "app/routes/parent.a.tsx": js` + import { useLoaderData } from '@remix-run/react'; + let count = 0; + export function loader({ request }) { + return { count: ++count }; + } + export default function Component() { + return

A Count: {useLoaderData().count}

; + } + `, + "app/routes/parent.b.tsx": js` + import { useLoaderData } from '@remix-run/react'; + let count = 0; + export function loader({ request }) { + return { count: ++count }; + } + export default function Component() { + return

B Count: {useLoaderData().count}

; + } + `, + }, + }); + let appFixture = await createAppFixture(fixture); + let app = new PlaywrightFixture(appFixture, page); + + let urls: string[] = []; + page.on("request", (req) => { + if (req.url().includes(".data")) { + urls.push(req.url()); + } + }); + + await app.goto("/"); + + await app.clickLink("/parent/a"); + await page.waitForSelector("#a"); + expect(await app.getHtml("#parent")).toContain("Parent Count: 1"); + expect(await app.getHtml("#a")).toContain("A Count: 1"); + expect(urls.length).toBe(1); + // Not a revalidation on the first navigation so no params + expect(urls[0].endsWith("/parent/a.data")).toBe(true); + urls = []; + + await app.clickLink("/parent/b"); + await page.waitForSelector("#b"); + expect(await app.getHtml("#parent")).toContain("Parent Count: 1"); + expect(await app.getHtml("#b")).toContain("B Count: 1"); + expect(urls.length).toBe(1); + // Don't reload the parent route + expect( + urls[0].endsWith("/parent/b.data?_routes=root%2Croutes%2Fparent.b") + ).toBe(true); + urls = []; + + await app.clickLink("/parent/a"); + await page.waitForSelector("#a"); + expect(await app.getHtml("#parent")).toContain("Parent Count: 1"); + expect(await app.getHtml("#a")).toContain("A Count: 2"); + expect(urls.length).toBe(1); + // Don't reload the parent route + expect( + urls[0].endsWith("/parent/a.data?_routes=root%2Croutes%2Fparent.a") + ).toBe(true); + }); + + test("allows reused routes to opt out via shouldRevalidate (w/clientLoader)", async ({ + page, + }) => { + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, + }, + }, + files: { + ...files, + "app/routes/_index.tsx": js` + import { Link } from '@remix-run/react'; + export default function Component() { + return Go to /parent/a; + } + `, + "app/routes/parent.tsx": js` + import { Link, Outlet, useLoaderData } from '@remix-run/react'; + let count = 0; + export function loader({ request }) { + return { count: ++count }; + } + export function shouldRevalidate() { + return false; + } + export function clientLoader({ serverLoader }) { + return serverLoader() + } + export default function Component() { + return ( + <> +

Parent Count: {useLoaderData().count}

+ Go to /parent/a + Go to /parent/b + + + ); + } + `, + "app/routes/parent.a.tsx": js` + import { useLoaderData } from '@remix-run/react'; + let count = 0; + export function loader({ request }) { + return { count: ++count }; + } + export default function Component() { + return

A Count: {useLoaderData().count}

; + } + `, + "app/routes/parent.b.tsx": js` + import { useLoaderData } from '@remix-run/react'; + let count = 0; + export function loader({ request }) { + return { count: ++count }; + } + export default function Component() { + return

B Count: {useLoaderData().count}

; + } + `, + }, + }); + let appFixture = await createAppFixture(fixture); + let app = new PlaywrightFixture(appFixture, page); + + let urls: string[] = []; + page.on("request", (req) => { + if (req.url().includes(".data")) { + urls.push(req.url()); + } + }); + + await app.goto("/"); + + await app.clickLink("/parent/a"); + await page.waitForSelector("#a"); + expect(await app.getHtml("#parent")).toContain("Parent Count: 1"); + expect(await app.getHtml("#a")).toContain("A Count: 1"); + expect(urls.length).toBe(2); + // Client loader triggers 2 requests on the first navigation + expect(urls[0].endsWith("/parent/a.data?_routes=routes%2Fparent")).toBe( + true + ); + expect( + urls[1].endsWith("/parent/a.data?_routes=root%2Croutes%2Fparent.a") + ).toBe(true); + urls = []; + + await app.clickLink("/parent/b"); + await page.waitForSelector("#b"); + expect(await app.getHtml("#parent")).toContain("Parent Count: 1"); + expect(await app.getHtml("#b")).toContain("B Count: 1"); + expect(urls.length).toBe(1); + // Don't reload the parent route + expect( + urls[0].endsWith("/parent/b.data?_routes=root%2Croutes%2Fparent.b") + ).toBe(true); + urls = []; + + await app.clickLink("/parent/a"); + await page.waitForSelector("#a"); + expect(await app.getHtml("#parent")).toContain("Parent Count: 1"); + expect(await app.getHtml("#a")).toContain("A Count: 2"); + expect(urls.length).toBe(1); + // Don't reload the parent route + expect( + urls[0].endsWith("/parent/a.data?_routes=root%2Croutes%2Fparent.a") + ).toBe(true); + }); + + test("does not add a _routes param for routes without loaders", async ({ + page, + }) => { + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, + }, + }, + files: { + ...files, + "app/routes/_index.tsx": js` + import { Link } from '@remix-run/react'; + export default function Component() { + return Go to /parent/a; + } + `, + "app/routes/parent.tsx": js` + import { Link, Outlet, useLoaderData } from '@remix-run/react'; + let count = 0; + export function loader({ request }) { + return { count: ++count }; + } + export function shouldRevalidate() { + return false; + } + export default function Component() { + return ( + <> +

Parent Count: {useLoaderData().count}

+ Go to /parent/a + Go to /parent/b + + + ); + } + `, + "app/routes/parent.a.tsx": js` + import { useLoaderData } from '@remix-run/react'; + let count = 0; + export function loader({ request }) { + return { count: ++count }; + } + export default function Component() { + return

A Count: {useLoaderData().count}

; + } + `, + "app/routes/parent.b.tsx": js` + export default function Component() { + return

B

; + } + `, + }, + }); + let appFixture = await createAppFixture(fixture); + let app = new PlaywrightFixture(appFixture, page); + + let urls: string[] = []; + page.on("request", (req) => { + if (req.url().includes(".data")) { + urls.push(req.url()); + } + }); + + await app.goto("/"); + + await app.clickLink("/parent/a"); + await page.waitForSelector("#a"); + expect(await app.getHtml("#parent")).toContain("Parent Count: 1"); + expect(await app.getHtml("#a")).toContain("A Count: 1"); + expect(urls.length).toBe(1); + // Not a revalidation on the first navigation so no params + expect(urls[0].endsWith("/parent/a.data")).toBe(true); + urls = []; + + await app.clickLink("/parent/b"); + await page.waitForSelector("#b"); + expect(await app.getHtml("#parent")).toContain("Parent Count: 1"); + expect(await app.getHtml("#b")).toContain("B"); + expect(urls.length).toBe(1); + // Don't reload the parent route + expect(urls[0].endsWith("/parent/b.data?_routes=root")).toBe(true); + urls = []; + }); + }); + test.describe("client loaders", () => { test("when no routes have client loaders", async ({ page }) => { let fixture = await createFixture( @@ -1940,10 +2558,10 @@ test.describe("single-fetch", () => { "C server loader (C client loader)" ); - // A/B can be loaded together, C needs it's own call due to it's clientLoader + // root/A/B can be loaded together, C needs it's own call due to it's clientLoader expect(urls.sort()).toEqual([ expect.stringMatching( - /\/a\/b\/c\.data\?_routes=routes%2Fa%2Croutes%2Fa\.b$/ + /\/a\/b\/c\.data\?_routes=root%2Croutes%2Fa%2Croutes%2Fa\.b$/ ), expect.stringMatching(/\/a\/b\/c\.data\?_routes=routes%2Fa\.b\.c$/), ]); @@ -2047,10 +2665,9 @@ test.describe("single-fetch", () => { "C server loader (C client loader)" ); - // B/C have client loaders so they get individual calls, which leaves A - // getting it's own "individual" since it's the last route standing + // B/C have client loaders so they get individual calls, root/A go together expect(urls.sort()).toEqual([ - expect.stringMatching(/\/a\/b\/c\.data\?_routes=routes%2Fa$/), + expect.stringMatching(/\/a\/b\/c\.data\?_routes=root%2Croutes%2Fa$/), expect.stringMatching(/\/a\/b\/c\.data\?_routes=routes%2Fa\.b$/), expect.stringMatching(/\/a\/b\/c\.data\?_routes=routes%2Fa\.b\.c$/), ]); @@ -2161,8 +2778,9 @@ test.describe("single-fetch", () => { "C server loader (C client loader)" ); - // A/B/C all have client loaders so they get individual calls + // root/A/B/C all have client loaders so they get individual calls expect(urls.sort()).toEqual([ + expect.stringMatching(/\/a\/b\/c.data\?_routes=root$/), expect.stringMatching(/\/a\/b\/c.data\?_routes=routes%2Fa$/), expect.stringMatching(/\/a\/b\/c.data\?_routes=routes%2Fa.b$/), expect.stringMatching(/\/a\/b\/c.data\?_routes=routes%2Fa.b.c$/), @@ -2170,6 +2788,287 @@ test.describe("single-fetch", () => { }); }); + test.describe("fetchers", () => { + test("Fetcher loaders call singular routes", async ({ page }) => { + let fixture = await createFixture( + { + config: { + future: { + unstable_singleFetch: true, + }, + }, + files: { + ...files, + "app/routes/a.tsx": js` + import { Outlet } from '@remix-run/react'; + export default function Comp() { + return ; + } + `, + "app/routes/a.b.tsx": js` + import { useFetcher } from '@remix-run/react'; + + export function loader() { + return { message: 'LOADER' }; + } + + export default function Comp() { + let fetcher = useFetcher(); + return ( + <> + + {fetcher.data ?

{fetcher.data.message}

: null} + + ); + } + `, + }, + }, + ServerMode.Development + ); + + let urls: string[] = []; + page.on("request", (req) => { + if (req.method() === "GET" && req.url().includes(".data")) { + urls.push(req.url()); + } + }); + + let appFixture = await createAppFixture(fixture, ServerMode.Development); + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/a/b"); + await app.clickElement("#load"); + await page.waitForSelector("#data"); + expect(await app.getHtml("#data")).toContain("LOADER"); + + // No clientLoaders so we can make a single parameter-less fetch + expect(urls.length).toBe(1); + expect(urls[0].endsWith("/a/b.data?_routes=routes%2Fa.b")).toBe(true); + }); + + test("Fetcher actions call singular routes", async ({ page }) => { + let fixture = await createFixture( + { + config: { + future: { + unstable_singleFetch: true, + }, + }, + files: { + ...files, + "app/routes/a.tsx": js` + import { Outlet } from '@remix-run/react'; + export default function Comp() { + return ; + } + `, + "app/routes/a.b.tsx": js` + import { useFetcher } from '@remix-run/react'; + + export function action() { + return { message: 'ACTION' }; + } + + export default function Comp() { + let fetcher = useFetcher(); + return ( + <> + + {fetcher.data ?

{fetcher.data.message}

: null} + + ); + } + `, + }, + }, + ServerMode.Development + ); + + let urls: string[] = []; + page.on("request", (req) => { + if (req.method() === "GET" && req.url().includes(".data")) { + urls.push(req.url()); + } + }); + + let appFixture = await createAppFixture(fixture, ServerMode.Development); + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/a/b"); + await app.clickElement("#submit"); + await page.waitForSelector("#data"); + expect(await app.getHtml("#data")).toContain("ACTION"); + + // No clientLoaders so we can make a single parameter-less fetch + expect(urls.length).toBe(1); + expect(urls[0].endsWith("/a/b.data")).toBe(true); + }); + + test("Fetcher loads do not revalidate on GET navigations by default", async ({ + page, + }) => { + let fixture = await createFixture( + { + config: { + future: { + unstable_singleFetch: true, + }, + }, + files: { + ...files, + "app/routes/parent.tsx": js` + import { Link, Outlet, useFetcher } from '@remix-run/react'; + export default function Component() { + let fetcher = useFetcher(); + return ( + <> + Go to /parent/a + Go to /parent/b + + {fetcher.data ?

Fetch Count: {fetcher.data.count}

: null} + + + ); + } + `, + "app/routes/parent.a.tsx": js` + export default function Component() { + return

A

; + } + `, + "app/routes/parent.b.tsx": js` + export default function Component() { + return

B

; + } + `, + "app/routes/fetch.tsx": js` + let count = 0; + export function loader({ request }) { + return { count: ++count }; + } + export default function Component() { + return

Fetch

; + } + `, + }, + }, + ServerMode.Development + ); + let appFixture = await createAppFixture(fixture, ServerMode.Development); + let app = new PlaywrightFixture(appFixture, page); + + let urls: string[] = []; + page.on("request", (req) => { + if (req.url().includes(".data")) { + urls.push(req.url()); + } + }); + + await app.goto("/parent/a"); + await app.clickElement("#load"); + await page.waitForSelector("#fetch"); + expect(await app.getHtml("#fetch")).toContain("Fetch Count: 1"); + expect(urls.length).toBe(1); + expect(urls[0].endsWith("/fetch.data?_routes=routes%2Ffetch")).toBe(true); + urls = []; + + await app.clickLink("/parent/b"); + await page.waitForSelector("#b"); + expect(await app.getHtml("#fetch")).toContain("Fetch Count: 1"); + expect(urls.length).toBe(1); + expect(urls[0].endsWith("/parent/b.data")).toBe(true); + }); + + test("Fetcher loads can opt into revalidation on GET navigations", async ({ + page, + }) => { + let fixture = await createFixture( + { + config: { + future: { + unstable_singleFetch: true, + }, + }, + files: { + ...files, + "app/routes/parent.tsx": js` + import { Link, Outlet, useFetcher } from '@remix-run/react'; + export default function Component() { + let fetcher = useFetcher(); + return ( + <> + Go to /parent/a + Go to /parent/b + + {fetcher.data ?

Fetch Count: {fetcher.data.count}

: null} + + + ); + } + `, + "app/routes/parent.a.tsx": js` + export default function Component() { + return

A

; + } + `, + "app/routes/parent.b.tsx": js` + export default function Component() { + return

B

; + } + `, + "app/routes/fetch.tsx": js` + let count = 0; + export function loader({ request }) { + return { count: ++count }; + } + export function shouldRevalidate() { + return true; + } + export default function Component() { + return

Fetch

; + } + `, + }, + }, + ServerMode.Development + ); + let appFixture = await createAppFixture(fixture, ServerMode.Development); + let app = new PlaywrightFixture(appFixture, page); + + let urls: string[] = []; + page.on("request", (req) => { + if (req.url().includes(".data")) { + urls.push(req.url()); + } + }); + + await app.goto("/parent/a"); + await app.clickElement("#load"); + await page.waitForSelector("#fetch"); + expect(await app.getHtml("#fetch")).toContain("Fetch Count: 1"); + expect(urls.length).toBe(1); + expect(urls[0].endsWith("/fetch.data?_routes=routes%2Ffetch")).toBe(true); + urls = []; + + await app.clickLink("/parent/b"); + await page.waitForSelector("#b"); + expect(await app.getHtml("#fetch")).toContain("Fetch Count: 2"); + expect(urls.length).toBe(2); + expect(urls[0].endsWith("/fetch.data?_routes=routes%2Ffetch")).toBe(true); + expect(urls[1].endsWith("/parent/b.data")).toBe(true); + }); + }); + test.describe("prefetching", () => { test("when no routes have client loaders", async ({ page }) => { let fixture = await createFixture( diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index c11aa6b2ac8..8e3e6db0d0c 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -32,7 +32,7 @@ "@mdx-js/mdx": "^2.3.0", "@npmcli/package-json": "^4.0.1", "@remix-run/node": "workspace:*", - "@remix-run/router": "0.0.0-experimental-7d87ffb8c", + "@remix-run/router": "1.19.2-pre.0", "@remix-run/server-runtime": "workspace:*", "@types/mdx": "^2.0.5", "@vanilla-extract/integration": "^6.2.0", diff --git a/packages/remix-react/browser.tsx b/packages/remix-react/browser.tsx index 5595fc46d80..1ee298c23f0 100644 --- a/packages/remix-react/browser.tsx +++ b/packages/remix-react/browser.tsx @@ -305,7 +305,8 @@ export function RemixBrowser(_props: RemixBrowserProps): ReactElement { unstable_dataStrategy: window.__remixContext.future.unstable_singleFetch ? getSingleFetchDataStrategy( window.__remixManifest, - window.__remixRouteModules + window.__remixRouteModules, + () => router ) : undefined, unstable_patchRoutesOnNavigation: getPatchRoutesOnNavigationFunction( diff --git a/packages/remix-react/package.json b/packages/remix-react/package.json index 67817bb7a4f..c450e161fc2 100644 --- a/packages/remix-react/package.json +++ b/packages/remix-react/package.json @@ -19,10 +19,10 @@ "tsc": "tsc" }, "dependencies": { - "@remix-run/router": "0.0.0-experimental-7d87ffb8c", + "@remix-run/router": "1.19.2-pre.0", "@remix-run/server-runtime": "workspace:*", - "react-router": "0.0.0-experimental-7d87ffb8c", - "react-router-dom": "0.0.0-experimental-7d87ffb8c", + "react-router": "6.26.2-pre.0", + "react-router-dom": "6.26.2-pre.0", "turbo-stream": "2.4.0" }, "devDependencies": { diff --git a/packages/remix-react/single-fetch.tsx b/packages/remix-react/single-fetch.tsx index 927f329cf82..e10da5f199f 100644 --- a/packages/remix-react/single-fetch.tsx +++ b/packages/remix-react/single-fetch.tsx @@ -1,7 +1,9 @@ import * as React from "react"; import type { unstable_DataStrategyFunction as DataStrategyFunction, - unstable_HandlerResult as HandlerResult, + unstable_DataStrategyResult as DataStrategyResult, + unstable_DataStrategyMatch, + Router as RemixRouter, } from "@remix-run/router"; import { UNSAFE_ErrorResponseImpl as ErrorResponseImpl, @@ -23,7 +25,7 @@ import { decode } from "turbo-stream"; import { createRequestInit, isResponse } from "./data"; import type { AssetsManifest, EntryContext } from "./entry"; import { escapeHtml } from "./markup"; -import type { RouteModules } from "./routeModules"; +import { type RouteModules } from "./routeModules"; import invariant from "./invariant"; interface StreamTransferProps { @@ -119,100 +121,243 @@ export function StreamTransfer({ export function getSingleFetchDataStrategy( manifest: AssetsManifest, - routeModules: RouteModules + routeModules: RouteModules, + getRouter: () => RemixRouter ): DataStrategyFunction { - return async ({ request, matches }) => - request.method !== "GET" - ? singleFetchActionStrategy(request, matches) - : singleFetchLoaderStrategy(manifest, routeModules, request, matches); + return async ({ request, matches, fetcherKey }) => { + // Actions are simple and behave the same for navigations and fetchers + if (request.method !== "GET") { + return singleFetchActionStrategy(request, matches); + } + + // Fetcher loads are singular calls to one loader + if (fetcherKey) { + return singleFetchLoaderFetcherStrategy(request, matches); + } + + // Navigational loads are more complex... + return singleFetchLoaderNavigationStrategy( + manifest, + routeModules, + getRouter(), + request, + matches + ); + }; } -// Actions are simple since they're singular calls to the server -function singleFetchActionStrategy( +// Actions are simple since they're singular calls to the server for both +// navigations and fetchers) +async function singleFetchActionStrategy( request: Request, matches: DataStrategyFunctionArgs["matches"] ) { - return Promise.all( - matches.map(async (m) => { - let actionStatus: number | undefined; - let result = await m.resolve(async (handler): Promise => { - let result = await handler(async () => { - let url = singleFetchUrl(request.url); - let init = await createRequestInit(request); - let { data, status } = await fetchAndDecode(url, init); - actionStatus = status; - return unwrapSingleFetchResult(data as SingleFetchResult, m.route.id); - }); - return { type: "data", result }; - }); + let actionMatch = matches.find((m) => m.shouldLoad); + invariant(actionMatch, "No action match found"); + let actionStatus: number | undefined = undefined; + let result = await actionMatch.resolve(async (handler) => { + let result = await handler(async () => { + let url = singleFetchUrl(request.url); + let init = await createRequestInit(request); + let { data, status } = await fetchAndDecode(url, init); + actionStatus = status; + return unwrapSingleFetchResult( + data as SingleFetchResult, + actionMatch!.route.id + ); + }); + return result; + }); - if (isResponse(result.result) || isRouteErrorResponse(result.result)) { - return result; - } + if (isResponse(result.result) || isRouteErrorResponse(result.result)) { + return { [actionMatch.route.id]: result }; + } - // For non-responses, proxy along the statusCode via unstable_data() - // (most notably for skipping action error revalidation) - return { - type: result.type, - result: unstable_data(result.result, actionStatus), - }; - }) - ); + // For non-responses, proxy along the statusCode via unstable_data() + // (most notably for skipping action error revalidation) + return { + [actionMatch.route.id]: { + type: result.type, + result: unstable_data(result.result, actionStatus), + }, + }; } // Loaders are trickier since we only want to hit the server once, so we // create a singular promise for all server-loader routes to latch onto. -function singleFetchLoaderStrategy( +async function singleFetchLoaderNavigationStrategy( manifest: AssetsManifest, routeModules: RouteModules, + router: RemixRouter, request: Request, matches: DataStrategyFunctionArgs["matches"] ) { - let singleFetchPromise: Promise | undefined; - return Promise.all( - matches.map(async (m) => - m.resolve(async (handler): Promise => { - let result: unknown; - let url = stripIndexParam(singleFetchUrl(request.url)); - let init = await createRequestInit(request); - - // When a route has a client loader, it calls it's singular server loader + // Track which routes need a server load - in case we need to tack on a + // `_routes` param + let routesParams = new Set(); + + // We only add `_routes` when one or more routes opts out of a load via + // `shouldRevalidate` or `clientLoader` + let foundOptOutRoute = false; + + // Deferreds for each route so we can be sure they've all loaded via + // `match.resolve()`, and a singular promise that can tell us all routes + // have been resolved + let routeDfds = matches.map(() => createDeferred()); + let routesLoadedPromise = Promise.all(routeDfds.map((d) => d.promise)); + + // Deferred that we'll use for the call to the server that each match can + // await and parse out it's specific result + let singleFetchDfd = createDeferred(); + + // Base URL and RequestInit for calls to the server + let url = stripIndexParam(singleFetchUrl(request.url)); + let init = await createRequestInit(request); + + // We'll build up this results object as we loop through matches + let results: Record = {}; + + let resolvePromise = Promise.all( + matches.map(async (m, i) => + m.resolve(async (handler) => { + routeDfds[i].resolve(); + + if (!m.shouldLoad) { + // If we're not yet initialized and this is the initial load, respect + // `shouldLoad` because we're only dealing with `clientLoader.hydrate` + // routes which will fall into the `clientLoader` section below. + if (!router.state.initialized) { + return; + } + + // Otherwise, we opt out if we currently have data, a `loader`, and a + // `shouldRevalidate` function. This implies that the user opted out + // via `shouldRevalidate` + if ( + m.route.id in router.state.loaderData && + manifest.routes[m.route.id].hasLoader && + routeModules[m.route.id]?.shouldRevalidate + ) { + foundOptOutRoute = true; + return; + } + } + + // When a route has a client loader, it opts out of the singular call and + // calls it's server loader via `serverLoader()` using a `?_routes` param if (manifest.routes[m.route.id].hasClientLoader) { - result = await handler(async () => { - url.searchParams.set("_routes", m.route.id); - let { data } = await fetchAndDecode(url, init); - return unwrapSingleFetchResults( - data as SingleFetchResults, + if (manifest.routes[m.route.id].hasLoader) { + foundOptOutRoute = true; + } + try { + let result = await fetchSingleLoader( + handler, + url, + init, m.route.id ); - }); - } else { - result = await handler(async () => { - // Otherwise we let multiple routes hook onto the same promise - if (!singleFetchPromise) { - url = addRevalidationParam( - manifest, - routeModules, - matches.map((m) => m.route), - matches.filter((m) => m.shouldLoad).map((m) => m.route), - url - ); - singleFetchPromise = fetchAndDecode(url, init).then( - ({ data }) => data as SingleFetchResults - ); - } - let results = await singleFetchPromise; - return unwrapSingleFetchResults(results, m.route.id); - }); + results[m.route.id] = { type: "data", result }; + } catch (e) { + results[m.route.id] = { type: "error", result: e }; + } + return; + } else if (!manifest.routes[m.route.id].hasLoader) { + // If we don't have a server loader, then we don't care about the HTTP + // call and can just send back a `null` - because we _do_ have a `loader` + // in the client router handling route module/styles loads + results[m.route.id] = { + type: "data", + result: null, + }; + return; } - return { - type: "data", - result, - }; + // Otherwise, we want to load this route on the server and can lump this + // it in with the others on a singular promise + routesParams.add(m.route.id); + + await handler(async () => { + try { + let data = await singleFetchDfd.promise; + results[m.route.id] = { + type: "data", + result: unwrapSingleFetchResults(data, m.route.id), + }; + } catch (e) { + results[m.route.id] = { + type: "error", + result: e, + }; + } + }); }) ) ); + + // Wait for all routes to resolve above before we make the HTTP call + await routesLoadedPromise; + + // Don't make any single fetch server calls: + // - On initial hydration - only clientLoaders can pass through via `clientLoader.hydrate` + // - If there are no routes to fetch from the server + if (!router.state.initialized || routesParams.size === 0) { + singleFetchDfd.resolve({}); + } else { + try { + // When one or more routes have opted out, we add a _routes param to + // limit the loaders to those that have a server loader and did not + // opt out + if (foundOptOutRoute && routesParams.size > 0) { + url.searchParams.set( + "_routes", + matches + .filter((m) => routesParams.has(m.route.id)) + .map((m) => m.route.id) + .join(",") + ); + } + + let data = await fetchAndDecode(url, init); + singleFetchDfd.resolve(data.data as SingleFetchResults); + } catch (e) { + singleFetchDfd.reject(e as Error); + } + } + + await resolvePromise; + + return results; +} + +// Fetcher loader calls are much simpler than navigational loader calls +async function singleFetchLoaderFetcherStrategy( + request: Request, + matches: DataStrategyFunctionArgs["matches"] +) { + let fetcherMatch = matches.find((m) => m.shouldLoad); + invariant(fetcherMatch, "No fetcher match found"); + let result = await fetcherMatch.resolve(async (handler) => { + let url = stripIndexParam(singleFetchUrl(request.url)); + let init = await createRequestInit(request); + return fetchSingleLoader(handler, url, init, fetcherMatch!.route.id); + }); + return { [fetcherMatch.route.id]: result }; +} + +function fetchSingleLoader( + handler: Parameters< + NonNullable[0]> + >[0], + url: URL, + init: RequestInit, + routeId: string +) { + return handler(async () => { + let singleLoaderUrl = new URL(url); + singleLoaderUrl.searchParams.set("_routes", routeId); + let { data } = await fetchAndDecode(singleLoaderUrl, init); + return unwrapSingleFetchResults(data as SingleFetchResults, routeId); + }); } function stripIndexParam(url: URL) { @@ -405,3 +550,29 @@ function unwrapSingleFetchResult(result: SingleFetchResult, routeId: string) { throw new Error(`No response found for routeId "${routeId}"`); } } + +function createDeferred() { + let resolve: (val?: any) => Promise; + let reject: (error?: Error) => Promise; + let promise = new Promise((res, rej) => { + resolve = async (val: T) => { + res(val); + try { + await promise; + } catch (e) {} + }; + reject = async (error?: Error) => { + rej(error); + try { + await promise; + } catch (e) {} + }; + }); + return { + promise, + //@ts-ignore + resolve, + //@ts-ignore + reject, + }; +} diff --git a/packages/remix-server-runtime/data.ts b/packages/remix-server-runtime/data.ts index e4c0ffc4e6b..afdda2ac145 100644 --- a/packages/remix-server-runtime/data.ts +++ b/packages/remix-server-runtime/data.ts @@ -43,7 +43,9 @@ export async function callRouteAction({ singleFetch: boolean; }) { let result = await action({ - request: stripDataParam(stripIndexParam(request)), + request: singleFetch + ? stripRoutesParam(stripIndexParam(request)) + : stripDataParam(stripIndexParam(request)), context: loadContext, params, }); @@ -79,7 +81,9 @@ export async function callRouteLoader({ singleFetch: boolean; }) { let result = await loader({ - request: stripDataParam(stripIndexParam(request)), + request: singleFetch + ? stripRoutesParam(stripIndexParam(request)) + : stripDataParam(stripIndexParam(request)), context: loadContext, params, }); @@ -158,3 +162,20 @@ function stripDataParam(request: Request) { return new Request(url.href, init); } + +function stripRoutesParam(request: Request) { + let url = new URL(request.url); + url.searchParams.delete("_routes"); + let init: RequestInit = { + method: request.method, + body: request.body, + headers: request.headers, + signal: request.signal, + }; + + if (init.body) { + (init as { duplex: "half" }).duplex = "half"; + } + + return new Request(url.href, init); +} diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 6f86e439891..4cff98475ce 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -19,7 +19,7 @@ "tsc": "tsc" }, "dependencies": { - "@remix-run/router": "0.0.0-experimental-7d87ffb8c", + "@remix-run/router": "1.19.2-pre.0", "@types/cookie": "^0.6.0", "@web3-storage/multipart-parser": "^1.0.0", "cookie": "^0.6.0", diff --git a/packages/remix-server-runtime/single-fetch.ts b/packages/remix-server-runtime/single-fetch.ts index 6d62b357b9b..8e917e28593 100644 --- a/packages/remix-server-runtime/single-fetch.ts +++ b/packages/remix-server-runtime/single-fetch.ts @@ -63,27 +63,21 @@ export function getSingleFetchDataStrategy({ return async ({ request, matches }: DataStrategyFunctionArgs) => { // Don't call loaders on action data requests if (isActionDataRequest && request.method === "GET") { - return await Promise.all( - matches.map((m) => - m.resolve(async () => ({ type: "data", result: null })) - ) - ); + return {}; } + // Only run opt-in loaders when fine-grained revalidation is enabled + let matchesToLoad = loadRouteIds + ? matches.filter((m) => loadRouteIds.includes(m.route.id)) + : matches; let results = await Promise.all( - matches.map(async (match) => { - let result = await match.resolve(async (handler) => { - // Only run opt-in loaders when fine-grained revalidation is enabled - let data = - loadRouteIds && !loadRouteIds.includes(match.route.id) - ? null - : await handler(); - return { type: "data", result: data }; - }); - return result; - }) + matchesToLoad.map((match) => match.resolve()) + ); + return results.reduce( + (acc, result, i) => + Object.assign(acc, { [matchesToLoad[i].route.id]: result }), + {} ); - return results; }; } diff --git a/packages/remix-testing/package.json b/packages/remix-testing/package.json index 0bd9a637e56..7929a470d07 100644 --- a/packages/remix-testing/package.json +++ b/packages/remix-testing/package.json @@ -21,8 +21,8 @@ "dependencies": { "@remix-run/node": "workspace:*", "@remix-run/react": "workspace:*", - "@remix-run/router": "0.0.0-experimental-7d87ffb8c", - "react-router-dom": "0.0.0-experimental-7d87ffb8c" + "@remix-run/router": "1.19.2-pre.0", + "react-router-dom": "6.26.2-pre.0" }, "devDependencies": { "@remix-run/server-runtime": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40a65124166..a336462ae6d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,7 +39,7 @@ importers: version: 2.25.2 '@cloudflare/kv-asset-handler': specifier: ^0.3.0 - version: 0.3.4 + version: 0.3.1 '@manypkg/get-packages': specifier: ^1.1.3 version: 1.1.3 @@ -300,13 +300,13 @@ importers: version: 7.0.1 wrangler: specifier: ^3.72.3 - version: 3.72.3(@cloudflare/workers-types@4.20240208.0) + version: 3.74.0 integration: dependencies: '@cloudflare/kv-asset-handler': specifier: ^0.3.0 - version: 0.3.4 + version: 0.3.1 '@cloudflare/workers-types': specifier: ^4.20230518.0 version: 4.20240208.0 @@ -323,8 +323,8 @@ importers: specifier: workspace:* version: link:../packages/remix-node '@remix-run/router': - specifier: 0.0.0-experimental-7d87ffb8c - version: 0.0.0-experimental-7d87ffb8c + specifier: 1.19.2-pre.0 + version: 1.19.2-pre.0 '@remix-run/server-runtime': specifier: workspace:* version: link:../packages/remix-server-runtime @@ -414,13 +414,13 @@ importers: version: 4.3.1(typescript@5.1.6)(vite@5.1.3) wrangler: specifier: ^3.28.2 - version: 3.72.3(@cloudflare/workers-types@4.20240208.0) + version: 3.28.2(@cloudflare/workers-types@4.20240208.0) integration/helpers/cf-template: dependencies: '@cloudflare/kv-asset-handler': specifier: ^0.3.0 - version: 0.3.4 + version: 0.3.1 '@remix-run/cloudflare': specifier: workspace:* version: link:../../../packages/remix-cloudflare @@ -457,7 +457,7 @@ importers: version: 5.1.6 wrangler: specifier: ^3.24.0 - version: 3.72.3(@cloudflare/workers-types@4.20240208.0) + version: 3.28.2(@cloudflare/workers-types@4.20240208.0) integration/helpers/deno-template: dependencies: @@ -561,7 +561,7 @@ importers: version: 4.4.0 miniflare: specifier: ^3.20231030.4 - version: 3.20240821.0 + version: 3.20240129.2 react: specifier: ^18.2.0 version: 18.2.0 @@ -595,7 +595,7 @@ importers: version: 4.3.1(typescript@5.1.6)(vite@5.1.3) wrangler: specifier: ^3.24.0 - version: 3.72.3(@cloudflare/workers-types@4.20240208.0) + version: 3.28.2(@cloudflare/workers-types@4.20240208.0) integration/helpers/vite-template: dependencies: @@ -662,7 +662,7 @@ importers: version: 4.3.1(typescript@5.1.6)(vite@5.1.0) wrangler: specifier: ^3.24.0 - version: 3.72.3(@cloudflare/workers-types@4.20240208.0) + version: 3.28.2(@cloudflare/workers-types@4.20240208.0) packages/create-remix: dependencies: @@ -871,8 +871,8 @@ importers: specifier: ^2.11.2 version: link:../remix-react '@remix-run/router': - specifier: 0.0.0-experimental-7d87ffb8c - version: 0.0.0-experimental-7d87ffb8c + specifier: 1.19.2-pre.0 + version: 1.19.2-pre.0 '@remix-run/server-runtime': specifier: workspace:* version: link:../remix-server-runtime @@ -1083,7 +1083,7 @@ importers: version: 5.1.3(@types/node@18.17.1) wrangler: specifier: ^3.28.2 - version: 3.72.3(@cloudflare/workers-types@4.20240208.0) + version: 3.28.2(@cloudflare/workers-types@4.20240208.0) packages/remix-eslint-config: dependencies: @@ -1217,17 +1217,17 @@ importers: packages/remix-react: dependencies: '@remix-run/router': - specifier: 0.0.0-experimental-7d87ffb8c - version: 0.0.0-experimental-7d87ffb8c + specifier: 1.19.2-pre.0 + version: 1.19.2-pre.0 '@remix-run/server-runtime': specifier: workspace:* version: link:../remix-server-runtime react-router: - specifier: 0.0.0-experimental-7d87ffb8c - version: 0.0.0-experimental-7d87ffb8c(react@18.2.0) + specifier: 6.26.2-pre.0 + version: 6.26.2-pre.0(react@18.2.0) react-router-dom: - specifier: 0.0.0-experimental-7d87ffb8c - version: 0.0.0-experimental-7d87ffb8c(react-dom@18.2.0)(react@18.2.0) + specifier: 6.26.2-pre.0 + version: 6.26.2-pre.0(react-dom@18.2.0)(react@18.2.0) turbo-stream: specifier: 2.4.0 version: 2.4.0 @@ -1303,8 +1303,8 @@ importers: packages/remix-server-runtime: dependencies: '@remix-run/router': - specifier: 0.0.0-experimental-7d87ffb8c - version: 0.0.0-experimental-7d87ffb8c + specifier: 1.19.2-pre.0 + version: 1.19.2-pre.0 '@types/cookie': specifier: ^0.6.0 version: 0.6.0 @@ -1340,11 +1340,11 @@ importers: specifier: workspace:* version: link:../remix-react '@remix-run/router': - specifier: 0.0.0-experimental-7d87ffb8c - version: 0.0.0-experimental-7d87ffb8c + specifier: 1.19.2-pre.0 + version: 1.19.2-pre.0 react-router-dom: - specifier: 0.0.0-experimental-7d87ffb8c - version: 0.0.0-experimental-7d87ffb8c(react-dom@18.2.0)(react@18.2.0) + specifier: 6.26.2-pre.0 + version: 6.26.2-pre.0(react-dom@18.2.0)(react@18.2.0) devDependencies: '@remix-run/server-runtime': specifier: workspace:* @@ -2877,11 +2877,25 @@ packages: mime: 2.6.0 dev: false + /@cloudflare/kv-asset-handler@0.3.1: + resolution: {integrity: sha512-lKN2XCfKCmpKb86a1tl4GIwsJYDy9TGuwjhDELLmpKygQhw8X2xR4dusgpC5Tg7q1pB96Eb0rBo81kxSILQMwA==} + dependencies: + mime: 3.0.0 + /@cloudflare/kv-asset-handler@0.3.4: resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==} engines: {node: '>=16.13'} dependencies: mime: 3.0.0 + dev: false + + /@cloudflare/workerd-darwin-64@1.20240129.0: + resolution: {integrity: sha512-DfVVB5IsQLVcWPJwV019vY3nEtU88c2Qu2ST5SQxqcGivZ52imagLRK0RHCIP8PK4piSiq90qUC6ybppUsw8eg==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + requiresBuild: true + optional: true /@cloudflare/workerd-darwin-64@1.20240821.1: resolution: {integrity: sha512-CDBpfZKrSy4YrIdqS84z67r3Tzal2pOhjCsIb63IuCnvVes59/ft1qhczBzk9EffeOE2iTCrA4YBT7Sbn7USew==} @@ -2889,6 +2903,15 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true + dev: false + optional: true + + /@cloudflare/workerd-darwin-arm64@1.20240129.0: + resolution: {integrity: sha512-t0q8ABkmumG1zRM/MZ/vIv/Ysx0vTAXnQAPy/JW5aeQi/tqrypXkO9/NhPc0jbF/g/hIPrWEqpDgEp3CB7Da7Q==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + requiresBuild: true optional: true /@cloudflare/workerd-darwin-arm64@1.20240821.1: @@ -2897,6 +2920,15 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true + dev: false + optional: true + + /@cloudflare/workerd-linux-64@1.20240129.0: + resolution: {integrity: sha512-sFV1uobHgDI+6CKBS/ZshQvOvajgwl6BtiYaH4PSFSpvXTmRx+A9bcug+6BnD+V4WgwxTiEO2iR97E1XuwDAVw==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + requiresBuild: true optional: true /@cloudflare/workerd-linux-64@1.20240821.1: @@ -2905,6 +2937,15 @@ packages: cpu: [x64] os: [linux] requiresBuild: true + dev: false + optional: true + + /@cloudflare/workerd-linux-arm64@1.20240129.0: + resolution: {integrity: sha512-O7q7htHaFRp8PgTqNJx1/fYc3+LnvAo6kWWB9a14C5OWak6AAZk42PNpKPx+DXTmGvI+8S1+futBGUeJ8NPDXg==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + requiresBuild: true optional: true /@cloudflare/workerd-linux-arm64@1.20240821.1: @@ -2913,6 +2954,15 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true + dev: false + optional: true + + /@cloudflare/workerd-windows-64@1.20240129.0: + resolution: {integrity: sha512-YqGno0XSqqqkDmNoGEX6M8kJlI2lEfWntbTPVtHaZlaXVR9sWfoD7TEno0NKC95cXFz+ioyFLbgbOdnfWwmVAA==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + requiresBuild: true optional: true /@cloudflare/workerd-windows-64@1.20240821.1: @@ -2921,11 +2971,13 @@ packages: cpu: [x64] os: [win32] requiresBuild: true + dev: false optional: true - /@cloudflare/workers-shared@0.4.0: - resolution: {integrity: sha512-XAFOldVQsbxQ7mjbqX2q1dNIgcLbKSytk41pwuZTn9e0p7OeTpFTosJef8uwosL6CcOAHqcW1f1HJxyjwmtGxw==} + /@cloudflare/workers-shared@0.4.1: + resolution: {integrity: sha512-nYh4r8JwOOjYIdH2zub++CmIKlkYFlpxI1nBHimoiHcytJXD/b7ldJ21TtfzUZMCgI78mxVlymMHA/ReaOxKlA==} engines: {node: '>=16.7.0'} + dev: false /@cloudflare/workers-types@4.20240208.0: resolution: {integrity: sha512-MVGTTjZpJu4kJONvai5SdJzWIhOJbuweVZ3goI7FNyG+JdoQH41OoB+nMhLsX626vPLZVWGPIWsiSo/WZHzgQw==} @@ -4206,8 +4258,8 @@ packages: - encoding dev: false - /@remix-run/router@0.0.0-experimental-7d87ffb8c: - resolution: {integrity: sha512-Vu0Zw3Pm+/tEpE3MFI0ONEQbZEMlMddBCf8JwnYeQGwsaIXYk3oLpv1zE7n8QdF6A+Z7C2ieWMDde+wb+4060g==} + /@remix-run/router@1.19.2-pre.0: + resolution: {integrity: sha512-6hY0ygbEysBa8BWmO2uJbOjll+1kCHjJTg5qqCH2T+VVkGTjiZFd/Veruj4smF9rZP2PmZoFxSpZbWDr+xVieA==} engines: {node: '>=14.0.0'} dev: false @@ -6590,10 +6642,6 @@ packages: yargs: 16.2.0 dev: false - /consola@3.2.3: - resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} - engines: {node: ^14.18.0 || >=16.10.0} - /content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -6836,6 +6884,7 @@ packages: /date-fns@3.6.0: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + dev: false /dayjs@1.11.1: resolution: {integrity: sha512-ER7EjqVAMkRRsxNCC5YqJ9d9VQYuWdGt7aiH2qA5R5wt8ZmWaP2dLUSIK6y/kVzLMlmh1Tvu5xUf4M/wdGJ5KA==} @@ -6988,6 +7037,7 @@ packages: /defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + dev: false /degenerator@5.0.1: resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} @@ -10050,7 +10100,7 @@ packages: whatwg-encoding: 2.0.0 whatwg-mimetype: 3.0.0 whatwg-url: 12.0.1 - ws: 8.18.0 + ws: 8.16.0 xml-name-validator: 4.0.0 transitivePeerDependencies: - bufferutil @@ -11497,8 +11547,30 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} - /miniflare@3.20240821.0: - resolution: {integrity: sha512-4BhLGpssQxM/O6TZmJ10GkT3wBJK6emFkZ3V87/HyvQmVt8zMxEBvyw5uv6kdtp+7F54Nw6IKFJjPUL8rFVQrQ==} + /miniflare@3.20240129.2: + resolution: {integrity: sha512-BPUg8HsPmWQlRFUeiQk274i8M9L0gOvzbkjryuTvCX+M53EwBpP0gM2wyrRr/HokQoJcxWGh3InBu6L8+0bbPw==} + engines: {node: '>=16.13'} + hasBin: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + acorn: 8.11.3 + acorn-walk: 8.3.2 + capnp-ts: 0.7.0 + exit-hook: 2.2.1 + glob-to-regexp: 0.4.1 + stoppable: 1.1.0 + undici: 5.28.3 + workerd: 1.20240129.0 + ws: 8.16.0 + youch: 3.3.3 + zod: 3.22.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + /miniflare@3.20240821.1: + resolution: {integrity: sha512-81qdiryDG7VXzZuoa0EwhkaIYYrn7+StRIrd/2i7SPqPUNICUBjbhFFKqTnvE1+fqIPPB6l8ShKFaFvmnZOASg==} engines: {node: '>=16.13'} hasBin: true dependencies: @@ -11518,6 +11590,7 @@ packages: - bufferutil - supports-color - utf-8-validate + dev: false /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -11636,7 +11709,7 @@ packages: acorn: 8.11.3 pathe: 1.1.2 pkg-types: 1.0.3 - ufo: 1.5.4 + ufo: 1.4.0 /modern-ahocorasick@1.0.1: resolution: {integrity: sha512-yoe+JbhTClckZ67b2itRtistFKf8yPYelHLc7e5xAwtNAXxM6wJTUx2C7QeVSJFDzKT7bCIFyBVybPMKvmB9AA==} @@ -11753,9 +11826,6 @@ packages: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} dev: false - /node-fetch-native@1.6.4: - resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} - /node-fetch@2.6.9: resolution: {integrity: sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==} engines: {node: 4.x || >=6.0.0} @@ -11981,6 +12051,10 @@ packages: es-abstract: 1.22.4 dev: false + /ohash@1.1.3: + resolution: {integrity: sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==} + dev: false + /on-finished@2.3.0: resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} engines: {node: '>= 0.8'} @@ -12804,26 +12878,26 @@ packages: engines: {node: '>=0.10.0'} dev: false - /react-router-dom@0.0.0-experimental-7d87ffb8c(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-5l2H4fEjrfJICAUkBwpJHQV4LMYXixTAL7eLqQIoKVI3zqyh3kPhY9CHxEk+5liiVrJhxmhIt4loFycLTAS4Hg==} + /react-router-dom@6.26.2-pre.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-oHlxmc/5SufjxHI96rT1xGII8eKRlIn8juA86YK/2OUbs4HnV+8khNjakJSzTyLx86RY/kc09lDrfpbCWspSpg==} engines: {node: '>=14.0.0'} peerDependencies: react: '>=16.8' react-dom: '>=16.8' dependencies: - '@remix-run/router': 0.0.0-experimental-7d87ffb8c + '@remix-run/router': 1.19.2-pre.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-router: 0.0.0-experimental-7d87ffb8c(react@18.2.0) + react-router: 6.26.2-pre.0(react@18.2.0) dev: false - /react-router@0.0.0-experimental-7d87ffb8c(react@18.2.0): - resolution: {integrity: sha512-DWE9pSHTPECV+9RHyzSJTgA1rexfR6tj0LjRzgxBxOVgmojtbNP9tXZc8UrP+5l9LxfGmLtvQu6w4EO3tyRSkw==} + /react-router@6.26.2-pre.0(react@18.2.0): + resolution: {integrity: sha512-nO4Kv9HNXZyaVzKxRcmgDn0tVHEs2pNN3lrTEbxYupDa/6igNHnKOe1TXlc13D9FMRUpjlpGhd/Ork/kcQ1PVw==} engines: {node: '>=14.0.0'} peerDependencies: react: '>=16.8' dependencies: - '@remix-run/router': 0.0.0-experimental-7d87ffb8c + '@remix-run/router': 1.19.2-pre.0 react: 18.2.0 dev: false @@ -14446,8 +14520,12 @@ packages: engines: {node: '>=14.17'} hasBin: true + /ufo@1.4.0: + resolution: {integrity: sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==} + /ufo@1.5.4: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + dev: false /uid-safe@2.1.5: resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} @@ -14469,26 +14547,32 @@ packages: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} dev: true + /undici@5.28.3: + resolution: {integrity: sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==} + engines: {node: '>=14.0'} + dependencies: + '@fastify/busboy': 2.1.0 + /undici@5.28.4: resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} engines: {node: '>=14.0'} dependencies: '@fastify/busboy': 2.1.0 + dev: false /undici@6.11.1: resolution: {integrity: sha512-KyhzaLJnV1qa3BSHdj4AZ2ndqI0QWPxYzaIOio0WzcEJB9gvuysprJSLtpvc2D9mhR9jPDUk7xlJlZbH2KR5iw==} engines: {node: '>=18.0'} dev: false - /unenv-nightly@1.10.0-1717606461.a117952: - resolution: {integrity: sha512-u3TfBX02WzbHTpaEfWEKwDijDSFAHcgXkayUZ+MVDrjhLFvgAJzFGTSTmwlEhwWi2exyRQey23ah9wELMM6etg==} + /unenv-nightly@2.0.0-1724863496.70db6f1: + resolution: {integrity: sha512-r+VIl1gnsI4WQxluruSQhy8alpAf1AsLRLm4sEKp3otCyTIVD6I6wHEYzeQnwsyWgaD4+3BD4A/eqrgOpdTzhw==} dependencies: - consola: 3.2.3 defu: 6.1.4 - mime: 3.0.0 - node-fetch-native: 1.6.4 + ohash: 1.1.3 pathe: 1.1.2 ufo: 1.5.4 + dev: false /unicode-canonical-property-names-ecmascript@2.0.0: resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} @@ -15128,6 +15212,18 @@ packages: is-number: 3.0.0 dev: false + /workerd@1.20240129.0: + resolution: {integrity: sha512-t4pnsmjjk/u+GdVDgH2M1AFmJaBUABshYK/vT/HNrAXsHSwN6VR8Yqw0JQ845OokO34VLkuUtYQYyxHHKpdtsw==} + engines: {node: '>=16'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20240129.0 + '@cloudflare/workerd-darwin-arm64': 1.20240129.0 + '@cloudflare/workerd-linux-64': 1.20240129.0 + '@cloudflare/workerd-linux-arm64': 1.20240129.0 + '@cloudflare/workerd-windows-64': 1.20240129.0 + /workerd@1.20240821.1: resolution: {integrity: sha512-y4phjCnEG96u8ZkgkkHB+gSw0i6uMNo23rBmixylWpjxDklB+LWD8dztasvsu7xGaZbLoTxQESdEw956F7VJDA==} engines: {node: '>=16'} @@ -15139,9 +15235,42 @@ packages: '@cloudflare/workerd-linux-64': 1.20240821.1 '@cloudflare/workerd-linux-arm64': 1.20240821.1 '@cloudflare/workerd-windows-64': 1.20240821.1 + dev: false + + /wrangler@3.28.2(@cloudflare/workers-types@4.20240208.0): + resolution: {integrity: sha512-hlD4f2avBZuR1+qo9Um6D1prdWrSRtGTo9h6o/AKce+bHQEJWoJgJKHeLmrpZlLtHg/gGR1Xa1xzrexhuIzeJw==} + engines: {node: '>=16.17.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20230914.0 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + dependencies: + '@cloudflare/kv-asset-handler': 0.3.1 + '@cloudflare/workers-types': 4.20240208.0 + '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) + '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) + blake3-wasm: 2.1.5 + chokidar: 3.6.0 + esbuild: 0.17.19 + miniflare: 3.20240129.2 + nanoid: 3.3.7 + path-to-regexp: 6.2.1 + resolve: 1.22.8 + resolve.exports: 2.0.2 + selfsigned: 2.4.1 + source-map: 0.6.1 + xxhash-wasm: 1.0.2 + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate - /wrangler@3.72.3(@cloudflare/workers-types@4.20240208.0): - resolution: {integrity: sha512-EBlJGOcwanbzFkiJkRB47WKhvevh1AZK0ty0MyD0gptsgWnAxBfmFGiBuzOuRXbvH45ZrFrTqgi8c67EwcV1nA==} + /wrangler@3.74.0: + resolution: {integrity: sha512-wmtb+tQrgb61yN+Wa2JM98G1Gt4tKFRYPw6xwuyzUcA74L+Dum1A13w22/manl9Gq1jA3dPn+7UzT5sYEVHRog==} engines: {node: '>=16.17.0'} hasBin: true peerDependencies: @@ -15151,22 +15280,21 @@ packages: optional: true dependencies: '@cloudflare/kv-asset-handler': 0.3.4 - '@cloudflare/workers-shared': 0.4.0 - '@cloudflare/workers-types': 4.20240208.0 + '@cloudflare/workers-shared': 0.4.1 '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) blake3-wasm: 2.1.5 chokidar: 3.6.0 date-fns: 3.6.0 esbuild: 0.17.19 - miniflare: 3.20240821.0 + miniflare: 3.20240821.1 nanoid: 3.3.7 path-to-regexp: 6.2.1 resolve: 1.22.8 resolve.exports: 2.0.2 selfsigned: 2.4.1 source-map: 0.6.1 - unenv: /unenv-nightly@1.10.0-1717606461.a117952 + unenv: /unenv-nightly@2.0.0-1724863496.70db6f1 workerd: 1.20240821.1 xxhash-wasm: 1.0.2 optionalDependencies: @@ -15175,6 +15303,7 @@ packages: - bufferutil - supports-color - utf-8-validate + dev: false /wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} @@ -15223,6 +15352,18 @@ packages: optional: true dev: false + /ws@8.16.0: + resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + /ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -15234,6 +15375,7 @@ packages: optional: true utf-8-validate: optional: true + dev: false /xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}