From f2c207d78729f4fd18fe2cb73695a62c657fae29 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 10 Mar 2022 09:00:06 -0500 Subject: [PATCH 001/119] feat: add history package and createMemoryHistory implementation (#8702) * feat: add initial empty history package * feat: add createMemoryHistory + tests * chore: add rollup buld for history * chore: change state type from unknown -> any * feat: Change listen interface to be pop only * chore: fix lint warning * Add history package to yarn workspace * chore: Address PR feedback --- package.json | 1 + packages/history/.eslintrc | 12 + packages/history/README.md | 5 + packages/history/__tests__/.eslintrc | 8 + .../EncodedReservedCharacters.ts | 27 ++ .../history/__tests__/TestSequences/GoBack.ts | 31 ++ .../__tests__/TestSequences/GoForward.ts | 48 +++ .../InitialLocationDefaultKey.ts | 5 + .../TestSequences/InitialLocationHasKey.ts | 5 + .../history/__tests__/TestSequences/Listen.ts | 10 + .../__tests__/TestSequences/ListenPopOnly.ts | 14 + .../TestSequences/PushMissingPathname.ts | 23 ++ .../TestSequences/PushNewLocation.ts | 17 + .../TestSequences/PushRelativePathname.ts | 23 ++ .../PushRelativePathnameWarning.ts | 28 ++ .../__tests__/TestSequences/PushSamePath.ts | 25 ++ .../__tests__/TestSequences/PushState.ts | 16 + .../TestSequences/ReplaceNewLocation.ts | 26 ++ .../TestSequences/ReplaceSamePath.ts | 23 ++ .../__tests__/TestSequences/ReplaceState.ts | 16 + packages/history/__tests__/memory-test.ts | 180 +++++++++ packages/history/index.ts | 366 ++++++++++++++++++ packages/history/jest.config.js | 7 + packages/history/node-main.js | 7 + packages/history/package.json | 21 + packages/history/tsconfig.json | 19 + rollup.config.js | 167 ++++++++ tsconfig.json | 1 + 28 files changed, 1131 insertions(+) create mode 100644 packages/history/.eslintrc create mode 100644 packages/history/README.md create mode 100644 packages/history/__tests__/.eslintrc create mode 100644 packages/history/__tests__/TestSequences/EncodedReservedCharacters.ts create mode 100644 packages/history/__tests__/TestSequences/GoBack.ts create mode 100644 packages/history/__tests__/TestSequences/GoForward.ts create mode 100644 packages/history/__tests__/TestSequences/InitialLocationDefaultKey.ts create mode 100644 packages/history/__tests__/TestSequences/InitialLocationHasKey.ts create mode 100644 packages/history/__tests__/TestSequences/Listen.ts create mode 100644 packages/history/__tests__/TestSequences/ListenPopOnly.ts create mode 100644 packages/history/__tests__/TestSequences/PushMissingPathname.ts create mode 100644 packages/history/__tests__/TestSequences/PushNewLocation.ts create mode 100644 packages/history/__tests__/TestSequences/PushRelativePathname.ts create mode 100644 packages/history/__tests__/TestSequences/PushRelativePathnameWarning.ts create mode 100644 packages/history/__tests__/TestSequences/PushSamePath.ts create mode 100644 packages/history/__tests__/TestSequences/PushState.ts create mode 100644 packages/history/__tests__/TestSequences/ReplaceNewLocation.ts create mode 100644 packages/history/__tests__/TestSequences/ReplaceSamePath.ts create mode 100644 packages/history/__tests__/TestSequences/ReplaceState.ts create mode 100644 packages/history/__tests__/memory-test.ts create mode 100644 packages/history/index.ts create mode 100644 packages/history/jest.config.js create mode 100644 packages/history/node-main.js create mode 100644 packages/history/package.json create mode 100644 packages/history/tsconfig.json diff --git a/package.json b/package.json index 07d30bc89a..65b852851f 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ }, "workspaces": { "packages": [ + "packages/history", "packages/react-router", "packages/react-router-dom", "packages/react-router-native" diff --git a/packages/history/.eslintrc b/packages/history/.eslintrc new file mode 100644 index 0000000000..a64f72e629 --- /dev/null +++ b/packages/history/.eslintrc @@ -0,0 +1,12 @@ +{ + "env": { + "browser": true, + "commonjs": true + }, + "globals": { + "__DEV__": true + }, + "rules": { + "strict": 0 + } +} diff --git a/packages/history/README.md b/packages/history/README.md new file mode 100644 index 0000000000..d6b10e9a97 --- /dev/null +++ b/packages/history/README.md @@ -0,0 +1,5 @@ +# history + +The `history` library lets you easily manage session history anywhere JavaScript runs. A `history` object abstracts away the differences in various environments and provides a minimal API that lets you manage the history stack, navigate, and persist state between sessions. + +This internal package is an evolution of the [`history`](https://www.npmjs.com/package/history) `npm` package from https://github.com/remix-run/history, and has now been internalized to `react-router`. diff --git a/packages/history/__tests__/.eslintrc b/packages/history/__tests__/.eslintrc new file mode 100644 index 0000000000..a676852ea2 --- /dev/null +++ b/packages/history/__tests__/.eslintrc @@ -0,0 +1,8 @@ +{ + "env": { + "jest": true + }, + "rules": { + "no-console": 0 + } +} diff --git a/packages/history/__tests__/TestSequences/EncodedReservedCharacters.ts b/packages/history/__tests__/TestSequences/EncodedReservedCharacters.ts new file mode 100644 index 0000000000..5ed68a59c7 --- /dev/null +++ b/packages/history/__tests__/TestSequences/EncodedReservedCharacters.ts @@ -0,0 +1,27 @@ +import type { History } from "../../index"; + +export default function EncodeReservedCharacters(history: History) { + let pathname; + + // encoded string + pathname = "/view/%23abc"; + history.replace(pathname); + expect(history.location).toMatchObject({ + pathname: "/view/%23abc", + }); + + // encoded object + pathname = "/view/%23abc"; + history.replace({ pathname }); + expect(history.location).toMatchObject({ + pathname: "/view/%23abc", + }); + + // unencoded string + pathname = "/view/#abc"; + history.replace(pathname); + expect(history.location).toMatchObject({ + pathname: "/view/", + hash: "#abc", + }); +} diff --git a/packages/history/__tests__/TestSequences/GoBack.ts b/packages/history/__tests__/TestSequences/GoBack.ts new file mode 100644 index 0000000000..59134bc75a --- /dev/null +++ b/packages/history/__tests__/TestSequences/GoBack.ts @@ -0,0 +1,31 @@ +import type { History } from "../../index"; + +export default function GoBack(history: History, spy: jest.SpyInstance) { + expect(history.location).toMatchObject({ + pathname: "/", + }); + + history.push("/home"); + expect(history.action).toEqual("PUSH"); + expect(history.location).toMatchObject({ + pathname: "/home", + }); + expect(spy).not.toHaveBeenCalled(); + + history.go(-1); + expect(history.action).toEqual("POP"); + expect(history.location).toMatchObject({ + pathname: "/", + }); + expect(spy).toHaveBeenCalledWith({ + action: "POP", + location: { + hash: "", + key: expect.any(String), + pathname: "/", + search: "", + state: null, + }, + }); + expect(spy.mock.calls.length).toBe(1); +} diff --git a/packages/history/__tests__/TestSequences/GoForward.ts b/packages/history/__tests__/TestSequences/GoForward.ts new file mode 100644 index 0000000000..0066175607 --- /dev/null +++ b/packages/history/__tests__/TestSequences/GoForward.ts @@ -0,0 +1,48 @@ +import type { History } from "../../index"; + +export default function GoForward(history: History, spy: jest.SpyInstance) { + expect(history.location).toMatchObject({ + pathname: "/", + }); + + history.push("/home"); + expect(history.action).toEqual("PUSH"); + expect(history.location).toMatchObject({ + pathname: "/home", + }); + expect(spy).not.toHaveBeenCalled(); + + history.go(-1); + expect(history.action).toEqual("POP"); + expect(history.location).toMatchObject({ + pathname: "/", + }); + expect(spy).toHaveBeenCalledWith({ + action: "POP", + location: { + hash: "", + key: expect.any(String), + pathname: "/", + search: "", + state: null, + }, + }); + expect(spy.mock.calls.length).toBe(1); + + history.go(1); + expect(history.action).toEqual("POP"); + expect(history.location).toMatchObject({ + pathname: "/home", + }); + expect(spy).toHaveBeenCalledWith({ + action: "POP", + location: { + hash: "", + key: expect.any(String), + pathname: "/home", + search: "", + state: null, + }, + }); + expect(spy.mock.calls.length).toBe(2); +} diff --git a/packages/history/__tests__/TestSequences/InitialLocationDefaultKey.ts b/packages/history/__tests__/TestSequences/InitialLocationDefaultKey.ts new file mode 100644 index 0000000000..7534c4990f --- /dev/null +++ b/packages/history/__tests__/TestSequences/InitialLocationDefaultKey.ts @@ -0,0 +1,5 @@ +import type { History } from "../../index"; + +export default function InitialLocationDefaultKey(history: History) { + expect(history.location.key).toBe("default"); +} diff --git a/packages/history/__tests__/TestSequences/InitialLocationHasKey.ts b/packages/history/__tests__/TestSequences/InitialLocationHasKey.ts new file mode 100644 index 0000000000..57fa4c7efc --- /dev/null +++ b/packages/history/__tests__/TestSequences/InitialLocationHasKey.ts @@ -0,0 +1,5 @@ +import type { History } from "../../index"; + +export default function InitialLocationHasKey(history: History) { + expect(history.location.key).toBeTruthy(); +} diff --git a/packages/history/__tests__/TestSequences/Listen.ts b/packages/history/__tests__/TestSequences/Listen.ts new file mode 100644 index 0000000000..d7a4c85767 --- /dev/null +++ b/packages/history/__tests__/TestSequences/Listen.ts @@ -0,0 +1,10 @@ +import type { History } from "../../index"; + +export default function Listen(history: History) { + let spy = jest.fn(); + let unlisten = history.listen(spy); + + expect(spy).not.toHaveBeenCalled(); + + unlisten(); +} diff --git a/packages/history/__tests__/TestSequences/ListenPopOnly.ts b/packages/history/__tests__/TestSequences/ListenPopOnly.ts new file mode 100644 index 0000000000..bdc293d5dc --- /dev/null +++ b/packages/history/__tests__/TestSequences/ListenPopOnly.ts @@ -0,0 +1,14 @@ +import type { History } from "../../index"; + +export default function Listen(history: History) { + let spy = jest.fn(); + let unlisten = history.listen(spy); + + history.push("/2"); + expect(history.location.pathname).toBe("/2"); + history.replace("/3"); + expect(history.location.pathname).toBe("/3"); + + expect(spy).not.toHaveBeenCalled(); + unlisten(); +} diff --git a/packages/history/__tests__/TestSequences/PushMissingPathname.ts b/packages/history/__tests__/TestSequences/PushMissingPathname.ts new file mode 100644 index 0000000000..549ae24ba3 --- /dev/null +++ b/packages/history/__tests__/TestSequences/PushMissingPathname.ts @@ -0,0 +1,23 @@ +import type { History } from "../../index"; + +export default function PushMissingPathname(history: History) { + expect(history.location).toMatchObject({ + pathname: "/", + }); + + history.push("/home?the=query#the-hash"); + expect(history.action).toBe("PUSH"); + expect(history.location).toMatchObject({ + pathname: "/home", + search: "?the=query", + hash: "#the-hash", + }); + + history.push("?another=query#another-hash"); + expect(history.action).toBe("PUSH"); + expect(history.location).toMatchObject({ + pathname: "/home", + search: "?another=query", + hash: "#another-hash", + }); +} diff --git a/packages/history/__tests__/TestSequences/PushNewLocation.ts b/packages/history/__tests__/TestSequences/PushNewLocation.ts new file mode 100644 index 0000000000..cfd9d624dd --- /dev/null +++ b/packages/history/__tests__/TestSequences/PushNewLocation.ts @@ -0,0 +1,17 @@ +import type { History } from "../../index"; + +export default function PushNewLocation(history: History) { + expect(history.location).toMatchObject({ + pathname: "/", + }); + + history.push("/home?the=query#the-hash"); + expect(history.action).toBe("PUSH"); + expect(history.location).toMatchObject({ + pathname: "/home", + search: "?the=query", + hash: "#the-hash", + state: null, + key: expect.any(String), + }); +} diff --git a/packages/history/__tests__/TestSequences/PushRelativePathname.ts b/packages/history/__tests__/TestSequences/PushRelativePathname.ts new file mode 100644 index 0000000000..04cd50d273 --- /dev/null +++ b/packages/history/__tests__/TestSequences/PushRelativePathname.ts @@ -0,0 +1,23 @@ +import type { History } from "../../index"; + +export default function PushRelativePathname(history: History) { + expect(history.location).toMatchObject({ + pathname: "/", + }); + + history.push("/the/path?the=query#the-hash"); + expect(history.action).toBe("PUSH"); + expect(history.location).toMatchObject({ + pathname: "/the/path", + search: "?the=query", + hash: "#the-hash", + }); + + history.push("../other/path?another=query#another-hash"); + expect(history.action).toBe("PUSH"); + expect(history.location).toMatchObject({ + pathname: "/other/path", + search: "?another=query", + hash: "#another-hash", + }); +} diff --git a/packages/history/__tests__/TestSequences/PushRelativePathnameWarning.ts b/packages/history/__tests__/TestSequences/PushRelativePathnameWarning.ts new file mode 100644 index 0000000000..a766e6f203 --- /dev/null +++ b/packages/history/__tests__/TestSequences/PushRelativePathnameWarning.ts @@ -0,0 +1,28 @@ +import type { History } from "../../index"; + +export default function PushRelativePathnameWarning(history: History) { + expect(history.location).toMatchObject({ + pathname: "/", + }); + + history.push("/the/path?the=query#the-hash"); + expect(history.action).toBe("PUSH"); + expect(history.location).toMatchObject({ + pathname: "/the/path", + search: "?the=query", + hash: "#the-hash", + }); + + let spy = jest.spyOn(console, "warn").mockImplementation(() => {}); + history.push("../other/path?another=query#another-hash"); + expect(spy).toHaveBeenCalledWith( + expect.stringContaining("relative pathnames are not supported") + ); + spy.mockReset(); + + expect(history.location).toMatchObject({ + pathname: "../other/path", + search: "?another=query", + hash: "#another-hash", + }); +} diff --git a/packages/history/__tests__/TestSequences/PushSamePath.ts b/packages/history/__tests__/TestSequences/PushSamePath.ts new file mode 100644 index 0000000000..adf9821e50 --- /dev/null +++ b/packages/history/__tests__/TestSequences/PushSamePath.ts @@ -0,0 +1,25 @@ +import type { History } from "../../index"; + +export default function PushSamePath(history: History) { + expect(history.location).toMatchObject({ + pathname: "/", + }); + + history.push("/home"); + expect(history.action).toBe("PUSH"); + expect(history.location).toMatchObject({ + pathname: "/home", + }); + + history.push("/home"); + expect(history.action).toBe("PUSH"); + expect(history.location).toMatchObject({ + pathname: "/home", + }); + + history.go(-1); + expect(history.action).toBe("POP"); + expect(history.location).toMatchObject({ + pathname: "/home", + }); +} diff --git a/packages/history/__tests__/TestSequences/PushState.ts b/packages/history/__tests__/TestSequences/PushState.ts new file mode 100644 index 0000000000..0789504fb4 --- /dev/null +++ b/packages/history/__tests__/TestSequences/PushState.ts @@ -0,0 +1,16 @@ +import type { History } from "../../index"; + +export default function PushState(history: History) { + expect(history.location).toMatchObject({ + pathname: "/", + }); + + history.push("/home?the=query#the-hash", { the: "state" }); + expect(history.action).toBe("PUSH"); + expect(history.location).toMatchObject({ + pathname: "/home", + search: "?the=query", + hash: "#the-hash", + state: { the: "state" }, + }); +} diff --git a/packages/history/__tests__/TestSequences/ReplaceNewLocation.ts b/packages/history/__tests__/TestSequences/ReplaceNewLocation.ts new file mode 100644 index 0000000000..7ca93efd6a --- /dev/null +++ b/packages/history/__tests__/TestSequences/ReplaceNewLocation.ts @@ -0,0 +1,26 @@ +import type { History } from "../../index"; + +export default function ReplaceNewLocation(history: History) { + expect(history.location).toMatchObject({ + pathname: "/", + }); + + history.replace("/home?the=query#the-hash"); + expect(history.action).toBe("REPLACE"); + expect(history.location).toMatchObject({ + pathname: "/home", + search: "?the=query", + hash: "#the-hash", + state: null, + key: expect.any(String), + }); + + history.replace("/"); + expect(history.action).toBe("REPLACE"); + expect(history.location).toMatchObject({ + pathname: "/", + search: "", + state: null, + key: expect.any(String), + }); +} diff --git a/packages/history/__tests__/TestSequences/ReplaceSamePath.ts b/packages/history/__tests__/TestSequences/ReplaceSamePath.ts new file mode 100644 index 0000000000..0260be604b --- /dev/null +++ b/packages/history/__tests__/TestSequences/ReplaceSamePath.ts @@ -0,0 +1,23 @@ +import type { History } from "../../index"; + +export default function ReplaceSamePath(history: History) { + expect(history.location).toMatchObject({ + pathname: "/", + }); + + history.replace("/home"); + expect(history.action).toBe("REPLACE"); + expect(history.location).toMatchObject({ + pathname: "/home", + }); + + let prevLocation = history.location; + + history.replace("/home"); + expect(history.action).toBe("REPLACE"); + expect(history.location).toMatchObject({ + pathname: "/home", + }); + + expect(history.location).not.toBe(prevLocation); +} diff --git a/packages/history/__tests__/TestSequences/ReplaceState.ts b/packages/history/__tests__/TestSequences/ReplaceState.ts new file mode 100644 index 0000000000..c7039f3b40 --- /dev/null +++ b/packages/history/__tests__/TestSequences/ReplaceState.ts @@ -0,0 +1,16 @@ +import type { History } from "../../index"; + +export default function ReplaceState(history: History) { + expect(history.location).toMatchObject({ + pathname: "/", + }); + + history.replace("/home?the=query#the-hash", { the: "state" }); + expect(history.action).toBe("REPLACE"); + expect(history.location).toMatchObject({ + pathname: "/home", + search: "?the=query", + hash: "#the-hash", + state: { the: "state" }, + }); +} diff --git a/packages/history/__tests__/memory-test.ts b/packages/history/__tests__/memory-test.ts new file mode 100644 index 0000000000..9460c533ee --- /dev/null +++ b/packages/history/__tests__/memory-test.ts @@ -0,0 +1,180 @@ +import type { MemoryHistory } from "../index"; +import { createMemoryHistory } from "../index"; + +import Listen from "./TestSequences/Listen"; +import InitialLocationHasKey from "./TestSequences/InitialLocationHasKey"; +import PushNewLocation from "./TestSequences/PushNewLocation"; +import PushSamePath from "./TestSequences/PushSamePath"; +import PushState from "./TestSequences/PushState"; +import PushMissingPathname from "./TestSequences/PushMissingPathname"; +import PushRelativePathnameWarning from "./TestSequences/PushRelativePathnameWarning"; +import ReplaceNewLocation from "./TestSequences/ReplaceNewLocation"; +import ReplaceSamePath from "./TestSequences/ReplaceSamePath"; +import ReplaceState from "./TestSequences/ReplaceState"; +import EncodedReservedCharacters from "./TestSequences/EncodedReservedCharacters"; +import GoBack from "./TestSequences/GoBack"; +import GoForward from "./TestSequences/GoForward"; +import ListenPopOnly from "./TestSequences/ListenPopOnly"; + +describe("a memory history", () => { + let history: MemoryHistory; + + beforeEach(() => { + history = createMemoryHistory(); + }); + + it("has an index property", () => { + expect(typeof history.index).toBe("number"); + }); + + it("knows how to create hrefs", () => { + const href = history.createHref({ + pathname: "/the/path", + search: "?the=query", + hash: "#the-hash", + }); + + expect(href).toEqual("/the/path?the=query#the-hash"); + }); + + it("knows how to create hrefs from strings", () => { + const href = history.createHref("/the/path?the=query#the-hash"); + expect(href).toEqual("/the/path?the=query#the-hash"); + }); + + it("does not encode the generated path", () => { + const encodedHref = history.createHref({ + pathname: "/%23abc", + }); + expect(encodedHref).toEqual("/%23abc"); + + const unencodedHref = history.createHref({ + pathname: "/#abc", + }); + expect(unencodedHref).toEqual("/#abc"); + }); + + describe("the initial location", () => { + it("has a key", () => { + InitialLocationHasKey(history); + }); + }); + + describe("listen", () => { + it("does not immediately call listeners", () => { + Listen(history); + }); + + it("calls listeners only for POP actions", () => { + ListenPopOnly(history); + }); + }); + + describe("push", () => { + it("pushes the new location", () => { + PushNewLocation(history); + }); + + it("pushes the same location", () => { + PushSamePath(history); + }); + + it("pushes with state", () => { + PushState(history); + }); + + it("reuses the current location pathname", () => { + PushMissingPathname(history); + }); + + it("issues a warning on relative pathnames", () => { + PushRelativePathnameWarning(history); + }); + }); + + describe("replace", () => { + it("replaces with a new location", () => { + ReplaceNewLocation(history); + }); + }); + + describe("replace the same path", () => { + it("replaces with the same location", () => { + ReplaceSamePath(history); + }); + + it("replaces the state", () => { + ReplaceState(history); + }); + }); + + describe("location created with encoded/unencoded reserved characters", () => { + it("produces different location objects", () => { + EncodedReservedCharacters(history); + }); + }); + + describe("go", () => { + it("goes back", () => { + let spy: jest.SpyInstance = jest.fn(); + //@ts-ignore + history.listen(spy); + GoBack(history, spy); + }); + it("goes forward", () => { + let spy: jest.SpyInstance = jest.fn(); + //@ts-ignore + history.listen(spy); + GoForward(history, spy); + }); + }); +}); + +describe("a memory history without an onPopState callback", () => { + it("fails gracefully on go() calls", () => { + let history = createMemoryHistory(); + history.push("/page1"); + history.push("/page2"); + expect(history.location.pathname).toBe("/page2"); + history.go(-2); + expect(history.location.pathname).toBe("/"); + history.go(1); + expect(history.location.pathname).toBe("/page1"); + }); +}); + +describe("a memory history with some initial entries", () => { + it("clamps the initial index to a valid value", () => { + let history = createMemoryHistory({ + initialEntries: ["/one", "/two", "/three"], + initialIndex: 3, // invalid + }); + + expect(history.index).toBe(2); + }); + + it("starts at the last entry by default", () => { + let history = createMemoryHistory({ + initialEntries: ["/one", "/two", "/three"], + }); + + expect(history.index).toBe(2); + expect(history.location).toMatchObject({ + pathname: "/three", + search: "", + hash: "", + state: null, + key: expect.any(String), + }); + + history.go(-1); + expect(history.index).toBe(1); + expect(history.location).toMatchObject({ + pathname: "/two", + search: "", + hash: "", + state: null, + key: expect.any(String), + }); + }); +}); diff --git a/packages/history/index.ts b/packages/history/index.ts new file mode 100644 index 0000000000..62e14cf4f8 --- /dev/null +++ b/packages/history/index.ts @@ -0,0 +1,366 @@ +//////////////////////////////////////////////////////////////////////////////// +//#region TYPES +//////////////////////////////////////////////////////////////////////////////// + +/** + * Actions represent the type of change to a location value. + */ +export enum Action { + /** + * A POP indicates a change to an arbitrary index in the history stack, such + * as a back or forward navigation. It does not describe the direction of the + * navigation, only that the current index changed. + * + * Note: This is the default action for newly created history objects. + */ + Pop = "POP", + + /** + * A PUSH indicates a new entry being added to the history stack, such as when + * a link is clicked and a new page loads. When this happens, all subsequent + * entries in the stack are lost. + */ + Push = "PUSH", + + /** + * A REPLACE indicates the entry at the current index in the history stack + * being replaced by a new one. + */ + Replace = "REPLACE", +} + +/** + * The pathname, search, and hash values of a URL. + */ +export interface Path { + /** + * A URL pathname, beginning with a /. + */ + pathname: string; + + /** + * A URL search string, beginning with a ?. + */ + search: string; + + /** + * A URL fragment identifier, beginning with a #. + */ + hash: string; +} + +/** + * An entry in a history stack. A location contains information about the + * URL path, as well as possibly some arbitrary state and a key. + */ +export interface Location extends Path { + /** + * A value of arbitrary data associated with this location. + */ + state: any; + + /** + * A unique string associated with this location. May be used to safely store + * and retrieve data in some other storage API, like `localStorage`. + * + * Note: This value is always "default" on the initial location. + */ + key: string; +} + +/** + * A change to the current location. + */ +export interface Update { + /** + * The action that triggered the change. + */ + action: Action; + + /** + * The new location. + */ + location: Location; +} + +/** + * A function that receives notifications about location changes. + */ +export interface Listener { + (update: Update): void; +} + +/** + * Describes a location that is the destination of some navigation, either via + * `history.push` or `history.replace`. May be either a URL or the pieces of a + * URL path. + */ +export type To = string | Partial; + +/** + * A history is an interface to the navigation stack. The history serves as the + * source of truth for the current location, as well as provides a set of + * methods that may be used to change it. + * + * It is similar to the DOM's `window.history` object, but with a smaller, more + * focused API. + */ +export interface History { + /** + * The last action that modified the current location. This will always be + * Action.Pop when a history instance is first created. This value is mutable. + */ + readonly action: Action; + + /** + * The current location. This value is mutable. + */ + readonly location: Location; + + /** + * Returns a valid href for the given `to` value that may be used as + * the value of an attribute. + * + * @param to - The destination URL + */ + createHref(to: To): string; + + /** + * Pushes a new location onto the history stack, increasing its length by one. + * If there were any entries in the stack after the current one, they are + * lost. + * + * @param to - The new URL + * @param state - Data to associate with the new location + */ + push(to: To, state?: any): void; + + /** + * Replaces the current location in the history stack with a new one. The + * location that was replaced will no longer be available. + * + * @param to - The new URL + * @param state - Data to associate with the new location + */ + replace(to: To, state?: any): void; + + /** + * Navigates `n` entries backward/forward in the history stack relative to the + * current index. For example, a "back" navigation would use go(-1). + * + * @param delta - The delta in the stack index + */ + go(delta: number): void; + + /** + * Sets up a listener that will be called whenever the current location + * changes. + * + * @param listener - A function that will be called when the location changes + * @returns unlisten - A function that may be used to stop listening + */ + listen(listener: Listener): () => void; +} + +/** + * A memory history stores locations in memory. This is useful in stateful + * environments where there is no web browser, such as node tests or React + * Native. + */ +export interface MemoryHistory extends History { + /** + * The current index in the history stack. + */ + readonly index: number; +} +//#endregion + +//////////////////////////////////////////////////////////////////////////////// +//#region MEMORY +//////////////////////////////////////////////////////////////////////////////// + +/** + * A user-supplied object that describes a location. Used when providing + * entries to `createMemoryHistory` via its `initialEntries` option. + */ +export type InitialEntry = string | Partial; + +export type MemoryHistoryOptions = { + initialEntries?: InitialEntry[]; + initialIndex?: number; +}; + +/** + * Memory history stores the current location in memory. It is designed for use + * in stateful non-browser environments like tests and React Native. + */ +export function createMemoryHistory( + options: MemoryHistoryOptions = {} +): MemoryHistory { + let { initialEntries = ["/"], initialIndex } = options; + let entries: Location[]; // Declare so we can access from createLocation + entries = initialEntries.map((entry) => createLocation(entry)); + let index = clampIndex( + initialIndex == null ? entries.length - 1 : initialIndex + ); + let action = Action.Pop; + let listeners = createEvents(); + + function clampIndex(n: number): number { + return Math.min(Math.max(n, 0), entries.length - 1); + } + function getCurrentLocation(): Location { + return entries[index]; + } + function createLocation(to: To, state: any = null): Location { + let location = readOnly({ + pathname: entries ? getCurrentLocation().pathname : "/", + search: "", + hash: "", + ...(typeof to === "string" ? parsePath(to) : to), + state, + key: createKey(), + }); + warning( + location.pathname.charAt(0) === "/", + `relative pathnames are not supported in memory history: ${JSON.stringify( + to + )}` + ); + return location; + } + + let history: MemoryHistory = { + get index() { + return index; + }, + get action() { + return action; + }, + get location() { + return getCurrentLocation(); + }, + createHref(to) { + return typeof to === "string" ? to : createPath(to); + }, + push(to, state) { + action = Action.Push; + let nextLocation = createLocation(to, state); + index += 1; + entries.splice(index, entries.length, nextLocation); + }, + replace(to, state) { + action = Action.Replace; + let nextLocation = createLocation(to, state); + entries[index] = nextLocation; + }, + go(delta) { + action = Action.Pop; + index = clampIndex(index + delta); + listeners.call({ action, location: getCurrentLocation() }); + }, + listen(listener) { + return listeners.push(listener); + }, + }; + + return history; +} +//#endregion + +//////////////////////////////////////////////////////////////////////////////// +//#region UTILS +//////////////////////////////////////////////////////////////////////////////// + +const readOnly: (obj: T) => Readonly = __DEV__ + ? (obj) => Object.freeze(obj) + : (obj) => obj; + +function warning(cond: any, message: string) { + if (!cond) { + // eslint-disable-next-line no-console + if (typeof console !== "undefined") console.warn(message); + + try { + // Welcome to debugging history! + // + // This error is thrown as a convenience so you can more easily + // find the source for a warning that appears in the console by + // enabling "pause on exceptions" in your JavaScript debugger. + throw new Error(message); + // eslint-disable-next-line no-empty + } catch (e) {} + } +} + +type Events = { + length: number; + push: (fn: F) => () => void; + call: (arg: any) => void; +}; + +function createEvents(): Events { + let handlers: F[] = []; + + return { + get length() { + return handlers.length; + }, + push(fn: F) { + handlers.push(fn); + return function () { + handlers = handlers.filter((handler) => handler !== fn); + }; + }, + call(arg) { + handlers.forEach((fn) => fn && fn(arg)); + }, + }; +} + +function createKey() { + return Math.random().toString(36).substr(2, 8); +} + +/** + * Creates a string URL path from the given pathname, search, and hash components. + */ +export function createPath({ + pathname = "/", + search = "", + hash = "", +}: Partial) { + if (search && search !== "?") + pathname += search.charAt(0) === "?" ? search : "?" + search; + if (hash && hash !== "#") + pathname += hash.charAt(0) === "#" ? hash : "#" + hash; + return pathname; +} + +/** + * Parses a string URL path into its separate pathname, search, and hash components. + */ +export function parsePath(path: string): Partial { + let parsedPath: Partial = {}; + + if (path) { + let hashIndex = path.indexOf("#"); + if (hashIndex >= 0) { + parsedPath.hash = path.substr(hashIndex); + path = path.substr(0, hashIndex); + } + + let searchIndex = path.indexOf("?"); + if (searchIndex >= 0) { + parsedPath.search = path.substr(searchIndex); + path = path.substr(0, searchIndex); + } + + if (path) { + parsedPath.pathname = path; + } + } + + return parsedPath; +} +//#endregion diff --git a/packages/history/jest.config.js b/packages/history/jest.config.js new file mode 100644 index 0000000000..70bca74e43 --- /dev/null +++ b/packages/history/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + testMatch: ["**/__tests__/*-test.(js|ts)"], + preset: "ts-jest", + globals: { + __DEV__: true, + }, +}; diff --git a/packages/history/node-main.js b/packages/history/node-main.js new file mode 100644 index 0000000000..684ed32559 --- /dev/null +++ b/packages/history/node-main.js @@ -0,0 +1,7 @@ +/* eslint-env node */ + +if (process.env.NODE_ENV === "production") { + module.exports = require("./umd/history.production.min.js"); +} else { + module.exports = require("./umd/history.development.js"); +} diff --git a/packages/history/package.json b/packages/history/package.json new file mode 100644 index 0000000000..1b05b3937f --- /dev/null +++ b/packages/history/package.json @@ -0,0 +1,21 @@ +{ + "name": "history", + "version": "6.0.0", + "author": "Remix Software ", + "description": "Manage session history with JavaScript", + "repository": { + "type": "git", + "url": "https://github.com/remix-run/react-router.git", + "directory": "packages/history" + }, + "license": "MIT", + "main": "./main.js", + "module": "./index.js", + "types": "./index.d.ts", + "unpkg": "./umd/history.production.min.js", + "sideEffects": false, + "keywords": [ + "history", + "location" + ] +} diff --git a/packages/history/tsconfig.json b/packages/history/tsconfig.json new file mode 100644 index 0000000000..8502bc34d0 --- /dev/null +++ b/packages/history/tsconfig.json @@ -0,0 +1,19 @@ +{ + "files": ["index.ts"], + "compilerOptions": { + "lib": ["ES2020"], + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "node", + + "strict": true, + + "declaration": true, + "emitDeclarationOnly": true, + + "skipLibCheck": true, + + "outDir": "../../build/node_modules/history", + "rootDir": "." + } +} diff --git a/rollup.config.js b/rollup.config.js index 2d3fb360dd..a3e2bdf53d 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -25,6 +25,172 @@ function getVersion(sourceDir) { return require(`./${sourceDir}/package.json`).version; } +function history() { + const SOURCE_DIR = "packages/history"; + const OUTPUT_DIR = "build/node_modules/history"; + const version = getVersion(SOURCE_DIR); + + // JS modules for bundlers + const modules = [ + { + input: `${SOURCE_DIR}/index.ts`, + output: { + file: `${OUTPUT_DIR}/index.js`, + format: "esm", + sourcemap: !PRETTY, + banner: createBanner("history", version), + }, + plugins: [ + babel({ + exclude: /node_modules/, + presets: [ + ["@babel/preset-env", { loose: true }], + "@babel/preset-typescript", + ], + plugins: ["babel-plugin-dev-expression"], + extensions: [".ts"], + }), + copy({ + targets: [ + { src: `${SOURCE_DIR}/package.json`, dest: OUTPUT_DIR }, + { src: `${SOURCE_DIR}/README.md`, dest: OUTPUT_DIR }, + { src: "LICENSE.md", dest: OUTPUT_DIR }, + ], + verbose: true, + }), + ].concat(PRETTY ? prettier({ parser: "babel" }) : []), + }, + ]; + + // JS modules for + + diff --git a/examples/data-router/package-lock.json b/examples/data-router/package-lock.json new file mode 100644 index 0000000000..bb0d4f4360 --- /dev/null +++ b/examples/data-router/package-lock.json @@ -0,0 +1,2483 @@ +{ + "name": "data-router", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "data-router", + "dependencies": { + "react": "18.1.0", + "react-dom": "18.1.0", + "react-router": "6.3.0", + "react-router-dom": "6.3.0" + }, + "devDependencies": { + "@rollup/plugin-replace": "4.0.0", + "@types/node": "17.0.32", + "@types/react": "18.0.9", + "@types/react-dom": "18.0.3", + "@vitejs/plugin-react": "1.3.2", + "typescript": "4.6.4", + "vite": "2.9.9" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.10.tgz", + "integrity": "sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.10.tgz", + "integrity": "sha512-liKoppandF3ZcBnIYFjfSDHZLKdLHGJRkoWtG8zQyGJBQfIYobpnVGI5+pLBNtS6psFLDzyq8+h5HiVljW9PNA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.17.10", + "@babel/helper-compilation-targets": "^7.17.10", + "@babel/helper-module-transforms": "^7.17.7", + "@babel/helpers": "^7.17.9", + "@babel/parser": "^7.17.10", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.10", + "@babel/types": "^7.17.10", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", + "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.17.10", + "@jridgewell/gen-mapping": "^0.1.0", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", + "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.10.tgz", + "integrity": "sha512-gh3RxjWbauw/dFiU/7whjd0qN9K6nPJMqe6+Er7rOavFh0CQUSwhAE3IcTho2rywPJFxej6TUUHDkWcYI6gGqQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.17.10", + "@babel/helper-validator-option": "^7.16.7", + "browserslist": "^4.20.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz", + "integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==", + "dev": true, + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", + "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.16.7", + "@babel/types": "^7.17.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", + "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", + "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", + "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-simple-access": "^7.17.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/helper-validator-identifier": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.3", + "@babel/types": "^7.17.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", + "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", + "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.17.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", + "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", + "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.9.tgz", + "integrity": "sha512-cPCt915ShDWUEzEp3+UNRktO2n6v49l5RSnG9M5pS24hA+2FAc5si+Pn1i4VVbQQ+jh+bIZhPFQOJOzbrOYY1Q==", + "dev": true, + "dependencies": { + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.9", + "@babel/types": "^7.17.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", + "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.16.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.10.tgz", + "integrity": "sha512-n2Q6i+fnJqzOaq2VkdXxy2TCPCWQZHiCo0XqmrCvDWcZQKRyZzYi4Z0yxlBuN0w+r2ZHmre+Q087DSrw3pbJDQ==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.7.tgz", + "integrity": "sha512-Esxmk7YjA8QysKeT3VhTXvF6y77f/a91SIs4pWb4H2eWGQkCKFgQaG6hdoEVZtGsrAcb2K5BW66XsOErD4WU3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.17.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.17.3.tgz", + "integrity": "sha512-9tjBm4O07f7mzKSIlEmPdiE6ub7kfIe6Cd+w+oQebpATfTQMAgW+YOuWxogbKVTulA+MEO7byMeIUtQ1z+z+ZQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-jsx": "^7.16.7", + "@babel/types": "^7.17.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.16.7.tgz", + "integrity": "sha512-RMvQWvpla+xy6MlBpPlrKZCMRs2AGiHOGHY3xRwl0pEeim348dDyxeH4xBsMPbIMhujeq7ihE702eM2Ew0Wo+A==", + "dev": true, + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.16.7.tgz", + "integrity": "sha512-oe5VuWs7J9ilH3BCCApGoYjHoSO48vkjX2CbA5bFVhIuO2HKxA3vyF7rleA4o6/4rTDbk6r8hBW7Ul8E+UZrpA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.16.7.tgz", + "integrity": "sha512-rONFiQz9vgbsnaMtQlZCjIRwhJvlrPET8TabIUK2hzlXw9B9s2Ieaxte1SCOOXMbWRHodbKixNf3BLcWVOQ8Bw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz", + "integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==", + "dependencies": { + "regenerator-runtime": "^0.13.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", + "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.16.7", + "@babel/parser": "^7.16.7", + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", + "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.17.10", + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-function-name": "^7.17.9", + "@babel/helper-hoist-variables": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/parser": "^7.17.10", + "@babel/types": "^7.17.10", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", + "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz", + "integrity": "sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.1.tgz", + "integrity": "sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", + "integrity": "sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz", + "integrity": "sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-4.0.0.tgz", + "integrity": "sha512-+rumQFiaNac9y64OHtkHGmdjm7us9bo1PlbgQfdihQtuNxzjpaB064HbRnewUOggLQxVCCyINfStkgmBeQpv1g==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + }, + "peerDependencies": { + "rollup": "^1.20.0 || ^2.0.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "node_modules/@types/node": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.32.tgz", + "integrity": "sha512-eAIcfAvhf/BkHcf4pkLJ7ECpBAhh9kcxRBpip9cTiO+hf+aJrsxYxBeS6OXvOd9WqNAJmavXVpZvY1rBjNsXmw==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.4", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", + "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.0.9", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.9.tgz", + "integrity": "sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.3.tgz", + "integrity": "sha512-1RRW9kst+67gveJRYPxGmVy8eVJ05O43hg77G2j5m76/RFJtMbcfAs2viQ2UNsvvDg8F7OfQZx8qQcl6ymygaQ==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", + "dev": true + }, + "node_modules/@vitejs/plugin-react": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-1.3.2.tgz", + "integrity": "sha512-aurBNmMo0kz1O4qRoY+FM4epSA39y3ShWGuqfLRA/3z0oEJAdtoSfgA3aO98/PCCHAqMaduLxIxErWrVKIFzXA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.17.10", + "@babel/plugin-transform-react-jsx": "^7.17.3", + "@babel/plugin-transform-react-jsx-development": "^7.16.7", + "@babel/plugin-transform-react-jsx-self": "^7.16.7", + "@babel/plugin-transform-react-jsx-source": "^7.16.7", + "@rollup/pluginutils": "^4.2.1", + "react-refresh": "^0.13.0", + "resolve": "^1.22.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@vitejs/plugin-react/node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@vitejs/plugin-react/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/browserslist": { + "version": "4.20.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", + "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001332", + "electron-to-chromium": "^1.4.118", + "escalade": "^3.1.1", + "node-releases": "^2.0.3", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001339", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001339.tgz", + "integrity": "sha512-Es8PiVqCe+uXdms0Gu5xP5PF2bxLR7OBp3wUzUnuO7OHzhOfCyg3hdiGWVPVxhiuniOzng+hTc1u3fEQ0TlkSQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.1" + } + }, + "node_modules/csstype": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz", + "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.137", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz", + "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.38.tgz", + "integrity": "sha512-12fzJ0fsm7gVZX1YQ1InkOE5f9Tl7cgf6JPYXRJtPIoE0zkWAbHdPHVPPaLi9tYAcEBqheGzqLn/3RdTOyBfcA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "esbuild-android-64": "0.14.38", + "esbuild-android-arm64": "0.14.38", + "esbuild-darwin-64": "0.14.38", + "esbuild-darwin-arm64": "0.14.38", + "esbuild-freebsd-64": "0.14.38", + "esbuild-freebsd-arm64": "0.14.38", + "esbuild-linux-32": "0.14.38", + "esbuild-linux-64": "0.14.38", + "esbuild-linux-arm": "0.14.38", + "esbuild-linux-arm64": "0.14.38", + "esbuild-linux-mips64le": "0.14.38", + "esbuild-linux-ppc64le": "0.14.38", + "esbuild-linux-riscv64": "0.14.38", + "esbuild-linux-s390x": "0.14.38", + "esbuild-netbsd-64": "0.14.38", + "esbuild-openbsd-64": "0.14.38", + "esbuild-sunos-64": "0.14.38", + "esbuild-windows-32": "0.14.38", + "esbuild-windows-64": "0.14.38", + "esbuild-windows-arm64": "0.14.38" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.38.tgz", + "integrity": "sha512-aRFxR3scRKkbmNuGAK+Gee3+yFxkTJO/cx83Dkyzo4CnQl/2zVSurtG6+G86EQIZ+w+VYngVyK7P3HyTBKu3nw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.38.tgz", + "integrity": "sha512-L2NgQRWuHFI89IIZIlpAcINy9FvBk6xFVZ7xGdOwIm8VyhX1vNCEqUJO3DPSSy945Gzdg98cxtNt8Grv1CsyhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.38.tgz", + "integrity": "sha512-5JJvgXkX87Pd1Og0u/NJuO7TSqAikAcQQ74gyJ87bqWRVeouky84ICoV4sN6VV53aTW+NE87qLdGY4QA2S7KNA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.38.tgz", + "integrity": "sha512-eqF+OejMI3mC5Dlo9Kdq/Ilbki9sQBw3QlHW3wjLmsLh+quNfHmGMp3Ly1eWm981iGBMdbtSS9+LRvR2T8B3eQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.38.tgz", + "integrity": "sha512-epnPbhZUt93xV5cgeY36ZxPXDsQeO55DppzsIgWM8vgiG/Rz+qYDLmh5ts3e+Ln1wA9dQ+nZmVHw+RjaW3I5Ig==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.38.tgz", + "integrity": "sha512-/9icXUYJWherhk+y5fjPI5yNUdFPtXHQlwP7/K/zg8t8lQdHVj20SqU9/udQmeUo5pDFHMYzcEFfJqgOVeKNNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.38.tgz", + "integrity": "sha512-QfgfeNHRFvr2XeHFzP8kOZVnal3QvST3A0cgq32ZrHjSMFTdgXhMhmWdKzRXP/PKcfv3e2OW9tT9PpcjNvaq6g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.38.tgz", + "integrity": "sha512-uuZHNmqcs+Bj1qiW9k/HZU3FtIHmYiuxZ/6Aa+/KHb/pFKr7R3aVqvxlAudYI9Fw3St0VCPfv7QBpUITSmBR1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.38.tgz", + "integrity": "sha512-FiFvQe8J3VKTDXG01JbvoVRXQ0x6UZwyrU4IaLBZeq39Bsbatd94Fuc3F1RGqPF5RbIWW7RvkVQjn79ejzysnA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.38.tgz", + "integrity": "sha512-HlMGZTEsBrXrivr64eZ/EO0NQM8H8DuSENRok9d+Jtvq8hOLzrxfsAT9U94K3KOGk2XgCmkaI2KD8hX7F97lvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.38.tgz", + "integrity": "sha512-qd1dLf2v7QBiI5wwfil9j0HG/5YMFBAmMVmdeokbNAMbcg49p25t6IlJFXAeLzogv1AvgaXRXvgFNhScYEUXGQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.38.tgz", + "integrity": "sha512-mnbEm7o69gTl60jSuK+nn+pRsRHGtDPfzhrqEUXyCl7CTOCLtWN2bhK8bgsdp6J/2NyS/wHBjs1x8aBWwP2X9Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.38.tgz", + "integrity": "sha512-+p6YKYbuV72uikChRk14FSyNJZ4WfYkffj6Af0/Tw63/6TJX6TnIKE+6D3xtEc7DeDth1fjUOEqm+ApKFXbbVQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.38.tgz", + "integrity": "sha512-0zUsiDkGJiMHxBQ7JDU8jbaanUY975CdOW1YDrurjrM0vWHfjv9tLQsW9GSyEb/heSK1L5gaweRjzfUVBFoybQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.38.tgz", + "integrity": "sha512-cljBAApVwkpnJZfnRVThpRBGzCi+a+V9Ofb1fVkKhtrPLDYlHLrSYGtmnoTVWDQdU516qYI8+wOgcGZ4XIZh0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.38.tgz", + "integrity": "sha512-CDswYr2PWPGEPpLDUO50mL3WO/07EMjnZDNKpmaxUPsrW+kVM3LoAqr/CE8UbzugpEiflYqJsGPLirThRB18IQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.38.tgz", + "integrity": "sha512-2mfIoYW58gKcC3bck0j7lD3RZkqYA7MmujFYmSn9l6TiIcAMpuEvqksO+ntBgbLep/eyjpgdplF7b+4T9VJGOA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.38.tgz", + "integrity": "sha512-L2BmEeFZATAvU+FJzJiRLFUP+d9RHN+QXpgaOrs2klshoAm1AE6Us4X6fS9k33Uy5SzScn2TpcgecbqJza1Hjw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.38.tgz", + "integrity": "sha512-Khy4wVmebnzue8aeSXLC+6clo/hRYeNIm0DyikoEqX+3w3rcvrhzpoix0S+MF9vzh6JFskkIGD7Zx47ODJNyCw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.38.tgz", + "integrity": "sha512-k3FGCNmHBkqdJXuJszdWciAH77PukEyDsdIryEHn9cKLQFxzhT39dSumeTuggaQcXY57UlmLGIkklWZo2qzHpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/history": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", + "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", + "dependencies": { + "@babel/runtime": "^7.7.6" + } + }, + "node_modules/is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.4" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz", + "integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==", + "dev": true + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.13", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.13.tgz", + "integrity": "sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.3", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.1.0.tgz", + "integrity": "sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz", + "integrity": "sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.22.0" + }, + "peerDependencies": { + "react": "^18.1.0" + } + }, + "node_modules/react-refresh": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz", + "integrity": "sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", + "integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==", + "dependencies": { + "history": "^5.2.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz", + "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==", + "dependencies": { + "history": "^5.2.0", + "react-router": "6.3.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + }, + "node_modules/resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "2.72.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.72.1.tgz", + "integrity": "sha512-NTc5UGy/NWFGpSqF1lFY8z9Adri6uhyMLI6LvPAXdBKoPRFhIIiBUpt+Qg2awixqO3xvzSijjhnb4+QEZwJmxA==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/scheduler": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz", + "integrity": "sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz", + "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/vite": { + "version": "2.9.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.9.tgz", + "integrity": "sha512-ffaam+NgHfbEmfw/Vuh6BHKKlI/XIAhxE5QSS7gFLIngxg171mg1P3a4LSRME0z2ZU1ScxoKzphkipcYwSD5Ew==", + "dev": true, + "dependencies": { + "esbuild": "^0.14.27", + "postcss": "^8.4.13", + "resolve": "^1.22.0", + "rollup": "^2.59.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": ">=12.2.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "less": "*", + "sass": "*", + "stylus": "*" + }, + "peerDependenciesMeta": { + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + } + } + } + }, + "dependencies": { + "@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/code-frame": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.16.7" + } + }, + "@babel/compat-data": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.10.tgz", + "integrity": "sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw==", + "dev": true + }, + "@babel/core": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.10.tgz", + "integrity": "sha512-liKoppandF3ZcBnIYFjfSDHZLKdLHGJRkoWtG8zQyGJBQfIYobpnVGI5+pLBNtS6psFLDzyq8+h5HiVljW9PNA==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.17.10", + "@babel/helper-compilation-targets": "^7.17.10", + "@babel/helper-module-transforms": "^7.17.7", + "@babel/helpers": "^7.17.9", + "@babel/parser": "^7.17.10", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.10", + "@babel/types": "^7.17.10", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + } + }, + "@babel/generator": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", + "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", + "dev": true, + "requires": { + "@babel/types": "^7.17.10", + "@jridgewell/gen-mapping": "^0.1.0", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", + "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.10.tgz", + "integrity": "sha512-gh3RxjWbauw/dFiU/7whjd0qN9K6nPJMqe6+Er7rOavFh0CQUSwhAE3IcTho2rywPJFxej6TUUHDkWcYI6gGqQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.17.10", + "@babel/helper-validator-option": "^7.16.7", + "browserslist": "^4.20.2", + "semver": "^6.3.0" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz", + "integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-function-name": { + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", + "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", + "dev": true, + "requires": { + "@babel/template": "^7.16.7", + "@babel/types": "^7.17.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", + "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-module-imports": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", + "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-module-transforms": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", + "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-simple-access": "^7.17.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/helper-validator-identifier": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.3", + "@babel/types": "^7.17.0" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", + "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==", + "dev": true + }, + "@babel/helper-simple-access": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", + "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", + "dev": true, + "requires": { + "@babel/types": "^7.17.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", + "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", + "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", + "dev": true + }, + "@babel/helpers": { + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.9.tgz", + "integrity": "sha512-cPCt915ShDWUEzEp3+UNRktO2n6v49l5RSnG9M5pS24hA+2FAc5si+Pn1i4VVbQQ+jh+bIZhPFQOJOzbrOYY1Q==", + "dev": true, + "requires": { + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.9", + "@babel/types": "^7.17.0" + } + }, + "@babel/highlight": { + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", + "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.10.tgz", + "integrity": "sha512-n2Q6i+fnJqzOaq2VkdXxy2TCPCWQZHiCo0XqmrCvDWcZQKRyZzYi4Z0yxlBuN0w+r2ZHmre+Q087DSrw3pbJDQ==", + "dev": true + }, + "@babel/plugin-syntax-jsx": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.7.tgz", + "integrity": "sha512-Esxmk7YjA8QysKeT3VhTXvF6y77f/a91SIs4pWb4H2eWGQkCKFgQaG6hdoEVZtGsrAcb2K5BW66XsOErD4WU3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-react-jsx": { + "version": "7.17.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.17.3.tgz", + "integrity": "sha512-9tjBm4O07f7mzKSIlEmPdiE6ub7kfIe6Cd+w+oQebpATfTQMAgW+YOuWxogbKVTulA+MEO7byMeIUtQ1z+z+ZQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-jsx": "^7.16.7", + "@babel/types": "^7.17.0" + } + }, + "@babel/plugin-transform-react-jsx-development": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.16.7.tgz", + "integrity": "sha512-RMvQWvpla+xy6MlBpPlrKZCMRs2AGiHOGHY3xRwl0pEeim348dDyxeH4xBsMPbIMhujeq7ihE702eM2Ew0Wo+A==", + "dev": true, + "requires": { + "@babel/plugin-transform-react-jsx": "^7.16.7" + } + }, + "@babel/plugin-transform-react-jsx-self": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.16.7.tgz", + "integrity": "sha512-oe5VuWs7J9ilH3BCCApGoYjHoSO48vkjX2CbA5bFVhIuO2HKxA3vyF7rleA4o6/4rTDbk6r8hBW7Ul8E+UZrpA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-react-jsx-source": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.16.7.tgz", + "integrity": "sha512-rONFiQz9vgbsnaMtQlZCjIRwhJvlrPET8TabIUK2hzlXw9B9s2Ieaxte1SCOOXMbWRHodbKixNf3BLcWVOQ8Bw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/runtime": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz", + "integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@babel/template": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", + "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.16.7", + "@babel/parser": "^7.16.7", + "@babel/types": "^7.16.7" + } + }, + "@babel/traverse": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", + "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.17.10", + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-function-name": "^7.17.9", + "@babel/helper-hoist-variables": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/parser": "^7.17.10", + "@babel/types": "^7.17.10", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", + "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz", + "integrity": "sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.1.tgz", + "integrity": "sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", + "integrity": "sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz", + "integrity": "sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@rollup/plugin-replace": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-4.0.0.tgz", + "integrity": "sha512-+rumQFiaNac9y64OHtkHGmdjm7us9bo1PlbgQfdihQtuNxzjpaB064HbRnewUOggLQxVCCyINfStkgmBeQpv1g==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + } + }, + "@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + } + }, + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "@types/node": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.32.tgz", + "integrity": "sha512-eAIcfAvhf/BkHcf4pkLJ7ECpBAhh9kcxRBpip9cTiO+hf+aJrsxYxBeS6OXvOd9WqNAJmavXVpZvY1rBjNsXmw==", + "dev": true + }, + "@types/prop-types": { + "version": "15.7.4", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", + "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==", + "dev": true + }, + "@types/react": { + "version": "18.0.9", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.9.tgz", + "integrity": "sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.3.tgz", + "integrity": "sha512-1RRW9kst+67gveJRYPxGmVy8eVJ05O43hg77G2j5m76/RFJtMbcfAs2viQ2UNsvvDg8F7OfQZx8qQcl6ymygaQ==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", + "dev": true + }, + "@vitejs/plugin-react": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-1.3.2.tgz", + "integrity": "sha512-aurBNmMo0kz1O4qRoY+FM4epSA39y3ShWGuqfLRA/3z0oEJAdtoSfgA3aO98/PCCHAqMaduLxIxErWrVKIFzXA==", + "dev": true, + "requires": { + "@babel/core": "^7.17.10", + "@babel/plugin-transform-react-jsx": "^7.17.3", + "@babel/plugin-transform-react-jsx-development": "^7.16.7", + "@babel/plugin-transform-react-jsx-self": "^7.16.7", + "@babel/plugin-transform-react-jsx-source": "^7.16.7", + "@rollup/pluginutils": "^4.2.1", + "react-refresh": "^0.13.0", + "resolve": "^1.22.0" + }, + "dependencies": { + "@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "requires": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + } + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + } + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "browserslist": { + "version": "4.20.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", + "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001332", + "electron-to-chromium": "^1.4.118", + "escalade": "^3.1.1", + "node-releases": "^2.0.3", + "picocolors": "^1.0.0" + } + }, + "caniuse-lite": { + "version": "1.0.30001339", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001339.tgz", + "integrity": "sha512-Es8PiVqCe+uXdms0Gu5xP5PF2bxLR7OBp3wUzUnuO7OHzhOfCyg3hdiGWVPVxhiuniOzng+hTc1u3fEQ0TlkSQ==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "csstype": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz", + "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "electron-to-chromium": { + "version": "1.4.137", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz", + "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", + "dev": true + }, + "esbuild": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.38.tgz", + "integrity": "sha512-12fzJ0fsm7gVZX1YQ1InkOE5f9Tl7cgf6JPYXRJtPIoE0zkWAbHdPHVPPaLi9tYAcEBqheGzqLn/3RdTOyBfcA==", + "dev": true, + "requires": { + "esbuild-android-64": "0.14.38", + "esbuild-android-arm64": "0.14.38", + "esbuild-darwin-64": "0.14.38", + "esbuild-darwin-arm64": "0.14.38", + "esbuild-freebsd-64": "0.14.38", + "esbuild-freebsd-arm64": "0.14.38", + "esbuild-linux-32": "0.14.38", + "esbuild-linux-64": "0.14.38", + "esbuild-linux-arm": "0.14.38", + "esbuild-linux-arm64": "0.14.38", + "esbuild-linux-mips64le": "0.14.38", + "esbuild-linux-ppc64le": "0.14.38", + "esbuild-linux-riscv64": "0.14.38", + "esbuild-linux-s390x": "0.14.38", + "esbuild-netbsd-64": "0.14.38", + "esbuild-openbsd-64": "0.14.38", + "esbuild-sunos-64": "0.14.38", + "esbuild-windows-32": "0.14.38", + "esbuild-windows-64": "0.14.38", + "esbuild-windows-arm64": "0.14.38" + } + }, + "esbuild-android-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.38.tgz", + "integrity": "sha512-aRFxR3scRKkbmNuGAK+Gee3+yFxkTJO/cx83Dkyzo4CnQl/2zVSurtG6+G86EQIZ+w+VYngVyK7P3HyTBKu3nw==", + "dev": true, + "optional": true + }, + "esbuild-android-arm64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.38.tgz", + "integrity": "sha512-L2NgQRWuHFI89IIZIlpAcINy9FvBk6xFVZ7xGdOwIm8VyhX1vNCEqUJO3DPSSy945Gzdg98cxtNt8Grv1CsyhA==", + "dev": true, + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.38.tgz", + "integrity": "sha512-5JJvgXkX87Pd1Og0u/NJuO7TSqAikAcQQ74gyJ87bqWRVeouky84ICoV4sN6VV53aTW+NE87qLdGY4QA2S7KNA==", + "dev": true, + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.38.tgz", + "integrity": "sha512-eqF+OejMI3mC5Dlo9Kdq/Ilbki9sQBw3QlHW3wjLmsLh+quNfHmGMp3Ly1eWm981iGBMdbtSS9+LRvR2T8B3eQ==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.38.tgz", + "integrity": "sha512-epnPbhZUt93xV5cgeY36ZxPXDsQeO55DppzsIgWM8vgiG/Rz+qYDLmh5ts3e+Ln1wA9dQ+nZmVHw+RjaW3I5Ig==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.38.tgz", + "integrity": "sha512-/9icXUYJWherhk+y5fjPI5yNUdFPtXHQlwP7/K/zg8t8lQdHVj20SqU9/udQmeUo5pDFHMYzcEFfJqgOVeKNNQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-32": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.38.tgz", + "integrity": "sha512-QfgfeNHRFvr2XeHFzP8kOZVnal3QvST3A0cgq32ZrHjSMFTdgXhMhmWdKzRXP/PKcfv3e2OW9tT9PpcjNvaq6g==", + "dev": true, + "optional": true + }, + "esbuild-linux-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.38.tgz", + "integrity": "sha512-uuZHNmqcs+Bj1qiW9k/HZU3FtIHmYiuxZ/6Aa+/KHb/pFKr7R3aVqvxlAudYI9Fw3St0VCPfv7QBpUITSmBR1Q==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.38.tgz", + "integrity": "sha512-FiFvQe8J3VKTDXG01JbvoVRXQ0x6UZwyrU4IaLBZeq39Bsbatd94Fuc3F1RGqPF5RbIWW7RvkVQjn79ejzysnA==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.38.tgz", + "integrity": "sha512-HlMGZTEsBrXrivr64eZ/EO0NQM8H8DuSENRok9d+Jtvq8hOLzrxfsAT9U94K3KOGk2XgCmkaI2KD8hX7F97lvA==", + "dev": true, + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.38.tgz", + "integrity": "sha512-qd1dLf2v7QBiI5wwfil9j0HG/5YMFBAmMVmdeokbNAMbcg49p25t6IlJFXAeLzogv1AvgaXRXvgFNhScYEUXGQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.38.tgz", + "integrity": "sha512-mnbEm7o69gTl60jSuK+nn+pRsRHGtDPfzhrqEUXyCl7CTOCLtWN2bhK8bgsdp6J/2NyS/wHBjs1x8aBWwP2X9Q==", + "dev": true, + "optional": true + }, + "esbuild-linux-riscv64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.38.tgz", + "integrity": "sha512-+p6YKYbuV72uikChRk14FSyNJZ4WfYkffj6Af0/Tw63/6TJX6TnIKE+6D3xtEc7DeDth1fjUOEqm+ApKFXbbVQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-s390x": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.38.tgz", + "integrity": "sha512-0zUsiDkGJiMHxBQ7JDU8jbaanUY975CdOW1YDrurjrM0vWHfjv9tLQsW9GSyEb/heSK1L5gaweRjzfUVBFoybQ==", + "dev": true, + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.38.tgz", + "integrity": "sha512-cljBAApVwkpnJZfnRVThpRBGzCi+a+V9Ofb1fVkKhtrPLDYlHLrSYGtmnoTVWDQdU516qYI8+wOgcGZ4XIZh0Q==", + "dev": true, + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.38.tgz", + "integrity": "sha512-CDswYr2PWPGEPpLDUO50mL3WO/07EMjnZDNKpmaxUPsrW+kVM3LoAqr/CE8UbzugpEiflYqJsGPLirThRB18IQ==", + "dev": true, + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.38.tgz", + "integrity": "sha512-2mfIoYW58gKcC3bck0j7lD3RZkqYA7MmujFYmSn9l6TiIcAMpuEvqksO+ntBgbLep/eyjpgdplF7b+4T9VJGOA==", + "dev": true, + "optional": true + }, + "esbuild-windows-32": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.38.tgz", + "integrity": "sha512-L2BmEeFZATAvU+FJzJiRLFUP+d9RHN+QXpgaOrs2klshoAm1AE6Us4X6fS9k33Uy5SzScn2TpcgecbqJza1Hjw==", + "dev": true, + "optional": true + }, + "esbuild-windows-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.38.tgz", + "integrity": "sha512-Khy4wVmebnzue8aeSXLC+6clo/hRYeNIm0DyikoEqX+3w3rcvrhzpoix0S+MF9vzh6JFskkIGD7Zx47ODJNyCw==", + "dev": true, + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.38.tgz", + "integrity": "sha512-k3FGCNmHBkqdJXuJszdWciAH77PukEyDsdIryEHn9cKLQFxzhT39dSumeTuggaQcXY57UlmLGIkklWZo2qzHpw==", + "dev": true, + "optional": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "history": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", + "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", + "requires": { + "@babel/runtime": "^7.7.6" + } + }, + "is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true + }, + "node-releases": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz", + "integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true + }, + "postcss": { + "version": "8.4.13", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.13.tgz", + "integrity": "sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA==", + "dev": true, + "requires": { + "nanoid": "^3.3.3", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "react": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.1.0.tgz", + "integrity": "sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz", + "integrity": "sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==", + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.22.0" + } + }, + "react-refresh": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz", + "integrity": "sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==", + "dev": true + }, + "react-router": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", + "integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==", + "requires": { + "history": "^5.2.0" + } + }, + "react-router-dom": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz", + "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==", + "requires": { + "history": "^5.2.0", + "react-router": "6.3.0" + } + }, + "regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + }, + "resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dev": true, + "requires": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "rollup": { + "version": "2.72.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.72.1.tgz", + "integrity": "sha512-NTc5UGy/NWFGpSqF1lFY8z9Adri6uhyMLI6LvPAXdBKoPRFhIIiBUpt+Qg2awixqO3xvzSijjhnb4+QEZwJmxA==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "scheduler": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz", + "integrity": "sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "typescript": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz", + "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==", + "dev": true + }, + "vite": { + "version": "2.9.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.9.tgz", + "integrity": "sha512-ffaam+NgHfbEmfw/Vuh6BHKKlI/XIAhxE5QSS7gFLIngxg171mg1P3a4LSRME0z2ZU1ScxoKzphkipcYwSD5Ew==", + "dev": true, + "requires": { + "esbuild": "^0.14.27", + "fsevents": "~2.3.2", + "postcss": "^8.4.13", + "resolve": "^1.22.0", + "rollup": "^2.59.0" + } + } + } +} diff --git a/examples/data-router/package.json b/examples/data-router/package.json new file mode 100644 index 0000000000..d434e9b8a8 --- /dev/null +++ b/examples/data-router/package.json @@ -0,0 +1,24 @@ +{ + "name": "data-router", + "private": true, + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "serve": "vite preview" + }, + "dependencies": { + "react": "18.1.0", + "react-dom": "18.1.0", + "react-router": "6.3.0", + "react-router-dom": "6.3.0" + }, + "devDependencies": { + "@rollup/plugin-replace": "4.0.0", + "@types/node": "17.0.32", + "@types/react": "18.0.9", + "@types/react-dom": "18.0.3", + "@vitejs/plugin-react": "1.3.2", + "typescript": "4.6.4", + "vite": "2.9.9" + } +} diff --git a/examples/data-router/src/App.tsx b/examples/data-router/src/App.tsx new file mode 100644 index 0000000000..330d2868d2 --- /dev/null +++ b/examples/data-router/src/App.tsx @@ -0,0 +1,228 @@ +import React from "react"; +import type { ActionFunction, LoaderFunction } from "react-router-dom"; +import { + DataBrowserRouter, + useLoaderData, + useNavigation, + useFetcher, + useFetchers, + useRouteError, + Form, + Link, + Route, + Outlet, +} from "react-router-dom"; + +import type { Todos } from "./todos"; +import { addTodo, deleteTodo, getTodos } from "./todos"; + +let sleep = () => new Promise((r) => setTimeout(r, 500)); + +function Fallback() { + return

Performing initial data "load"

; +} + +// Layout +function Layout() { + let navigation = useNavigation(); + let fetchers = useFetchers(); + let fetcherInProgress = fetchers.some((f) => + ["loading", "submitting"].includes(f.state) + ); + return ( + <> + +
+ {navigation.state !== "idle" &&

Navigation in progress...

} + {fetcherInProgress &&

Fetcher in progress...

} +
+

+ Click on over to /todos and check out these + data loading APIs!{" "} +

+

+ We've introduced some fake async-aspects of routing here, so Keep an eye + on the top-right hand corner to see when we're actively navigating. +

+ + + ); +} + +// Home +const homeLoader: LoaderFunction = async () => { + await sleep(); + return { + date: new Date().toISOString(), + }; +}; + +function Home() { + let data = useLoaderData(); + return ( + <> +

Home

+

Last loaded at: {data.date}

+ + ); +} + +// Todos +const todosAction: ActionFunction = async ({ request }) => { + await sleep(); + + let formData = await request.formData(); + + // Deletion via fetcher + if (formData.get("action") === "delete") { + let id = formData.get("todoId"); + if (typeof id === "string") { + deleteTodo(id); + return { ok: true }; + } + } + + // Addition via
+ let todo = formData.get("todo"); + if (typeof todo === "string") { + addTodo(todo); + } + + return new Response(null, { + status: 302, + headers: { Location: "/todos" }, + }); +}; + +const todosLoader: LoaderFunction = async () => { + await sleep(); + return getTodos(); +}; + +function TodosList() { + let todos = useLoaderData() as Todos; + let navigation = useNavigation(); + let formRef = React.useRef(null); + + // If we add and then we delete - this will keep isAdding=true until the + // fetcher completes it's revalidation + let [isAdding, setIsAdding] = React.useState(false); + React.useEffect(() => { + if (navigation.formData?.get("action") === "add") { + setIsAdding(true); + } else if (navigation.state === "idle") { + setIsAdding(false); + formRef.current?.reset(); + } + }, [navigation]); + + return ( + <> +

Todos

+

+ This todo app uses a <Form> to submit new todos and a + <fetcher.form> to delete todos. Click on a todo item to navigate + to the /todos/:id route. +

+
    +
  • + + Click this link to force an error in the loader + +
  • + {Object.entries(todos).map(([id, todo]) => ( +
  • + +
  • + ))} +
+ + + + + + + + ); +} + +function TodosBoundary() { + let error = useRouteError(); + return ( + <> +

Error ๐Ÿ’ฅ

+

{error.message}

+ + ); +} + +interface TodoItemProps { + id: string; + todo: string; +} + +function TodoItem({ id, todo }: TodoItemProps) { + let fetcher = useFetcher(); + + let isDeleting = fetcher.formData != null; + return ( + <> + {todo} +   + + + + + + ); +} + +// Todo +const todoLoader: LoaderFunction = async ({ params }) => { + await sleep(); + let todos = getTodos(); + let todo = todos[params.id]; + if (!todo) { + throw new Error(`Uh oh, I couldn't find a todo with id "${params.id}"`); + } + return todo; +}; + +function Todo() { + let todo = useLoaderData(); + return ( + <> +

Error ๐Ÿ’ฅ

+

{todo}

+ + ); +} + +function App() { + return ( + }> + }> + } /> + } + errorElement={} + > + } /> + + + + ); +} + +export default App; diff --git a/examples/data-router/src/index.css b/examples/data-router/src/index.css new file mode 100644 index 0000000000..3e1f253f03 --- /dev/null +++ b/examples/data-router/src/index.css @@ -0,0 +1,12 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", + monospace; +} diff --git a/examples/data-router/src/main.tsx b/examples/data-router/src/main.tsx new file mode 100644 index 0000000000..201a661a90 --- /dev/null +++ b/examples/data-router/src/main.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; + +import "./index.css"; +import App from "./App"; + +createRoot(document.getElementById("root")).render( + + + +); diff --git a/examples/data-router/src/todos.ts b/examples/data-router/src/todos.ts new file mode 100644 index 0000000000..d7181b0574 --- /dev/null +++ b/examples/data-router/src/todos.ts @@ -0,0 +1,52 @@ +export interface Todos { + [key: string]: string; +} + +const TODOS_KEY = "todos"; + +export const uuid = () => Math.random().toString(36).substr(2, 9); + +export function saveTodos(todos: Todos): void { + return localStorage.setItem(TODOS_KEY, JSON.stringify(todos)); +} + +function initializeTodos(): Todos { + let todos: Todos = new Array(10) + .fill(null) + .reduce( + (acc, _, index) => + Object.assign(acc, { [uuid()]: `Seeded Todo #${index + 1}` }), + {} + ); + saveTodos(todos); + return todos; +} + +export function getTodos(): Todos { + let todos: Todos | null = null; + try { + // @ts-expect-error OK to throw here since we're catching + todos = JSON.parse(localStorage.getItem(TODOS_KEY)); + } catch (e) {} + if (!todos) { + todos = initializeTodos(); + } + return todos; +} + +export function addTodo(todo: string): void { + let newTodos = { ...getTodos() }; + newTodos[uuid()] = todo; + saveTodos(newTodos); +} + +export function deleteTodo(id: string): void { + let newTodos = { ...getTodos() }; + delete newTodos[id]; + saveTodos(newTodos); +} + +export function resetTodos(): void { + localStorage.removeItem(TODOS_KEY); + initializeTodos(); +} diff --git a/examples/data-router/src/vite-env.d.ts b/examples/data-router/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/data-router/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/data-router/tsconfig.json b/examples/data-router/tsconfig.json new file mode 100644 index 0000000000..8bdaabfe5d --- /dev/null +++ b/examples/data-router/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react", + "importsNotUsedAsValues": "error" + }, + "include": ["./src"] +} diff --git a/examples/data-router/vite.config.ts b/examples/data-router/vite.config.ts new file mode 100644 index 0000000000..b77eb48a30 --- /dev/null +++ b/examples/data-router/vite.config.ts @@ -0,0 +1,36 @@ +import * as path from "path"; +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import rollupReplace from "@rollup/plugin-replace"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + rollupReplace({ + preventAssignment: true, + values: { + __DEV__: JSON.stringify(true), + "process.env.NODE_ENV": JSON.stringify("development"), + }, + }), + react(), + ], + resolve: process.env.USE_SOURCE + ? { + alias: { + "@remix-run/router": path.resolve( + __dirname, + "../../packages/router/index.ts" + ), + "react-router": path.resolve( + __dirname, + "../../packages/react-router/index.ts" + ), + "react-router-dom": path.resolve( + __dirname, + "../../packages/react-router-dom/index.tsx" + ), + }, + } + : {}, +}); diff --git a/examples/scroll-restoration/.gitignore b/examples/scroll-restoration/.gitignore new file mode 100644 index 0000000000..d451ff16c1 --- /dev/null +++ b/examples/scroll-restoration/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local diff --git a/examples/scroll-restoration/.stackblitzrc b/examples/scroll-restoration/.stackblitzrc new file mode 100644 index 0000000000..d98146f4d0 --- /dev/null +++ b/examples/scroll-restoration/.stackblitzrc @@ -0,0 +1,4 @@ +{ + "installDependencies": true, + "startCommand": "npm run dev" +} diff --git a/examples/scroll-restoration/README.md b/examples/scroll-restoration/README.md new file mode 100644 index 0000000000..dd35c152d4 --- /dev/null +++ b/examples/scroll-restoration/README.md @@ -0,0 +1,21 @@ +--- +title: Basics +toc: false +order: 1 +--- + +# Basic Example + +This example demonstrates some of the basic features of React Router, including: + +- Layouts and nested ``s +- Index ``s +- Catch-all ``s +- Using `` as a placeholder for child routes +- Using ``s for navigation + +## Preview + +Open this example on [StackBlitz](https://stackblitz.com): + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/basic?file=src/App.tsx) diff --git a/examples/scroll-restoration/index.html b/examples/scroll-restoration/index.html new file mode 100644 index 0000000000..a8652d215a --- /dev/null +++ b/examples/scroll-restoration/index.html @@ -0,0 +1,12 @@ + + + + + + React Router - Basic Example + + +
+ + + diff --git a/examples/scroll-restoration/package-lock.json b/examples/scroll-restoration/package-lock.json new file mode 100644 index 0000000000..a0a35bb59f --- /dev/null +++ b/examples/scroll-restoration/package-lock.json @@ -0,0 +1,2483 @@ +{ + "name": "scroll-restoration", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "scroll-restoration", + "dependencies": { + "react": "18.1.0", + "react-dom": "18.1.0", + "react-router": "6.3.0", + "react-router-dom": "6.3.0" + }, + "devDependencies": { + "@rollup/plugin-replace": "4.0.0", + "@types/node": "17.0.32", + "@types/react": "18.0.9", + "@types/react-dom": "18.0.3", + "@vitejs/plugin-react": "1.3.2", + "typescript": "4.6.4", + "vite": "2.9.9" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.10.tgz", + "integrity": "sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.10.tgz", + "integrity": "sha512-liKoppandF3ZcBnIYFjfSDHZLKdLHGJRkoWtG8zQyGJBQfIYobpnVGI5+pLBNtS6psFLDzyq8+h5HiVljW9PNA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.17.10", + "@babel/helper-compilation-targets": "^7.17.10", + "@babel/helper-module-transforms": "^7.17.7", + "@babel/helpers": "^7.17.9", + "@babel/parser": "^7.17.10", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.10", + "@babel/types": "^7.17.10", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", + "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.17.10", + "@jridgewell/gen-mapping": "^0.1.0", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", + "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.10.tgz", + "integrity": "sha512-gh3RxjWbauw/dFiU/7whjd0qN9K6nPJMqe6+Er7rOavFh0CQUSwhAE3IcTho2rywPJFxej6TUUHDkWcYI6gGqQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.17.10", + "@babel/helper-validator-option": "^7.16.7", + "browserslist": "^4.20.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz", + "integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==", + "dev": true, + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", + "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.16.7", + "@babel/types": "^7.17.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", + "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", + "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", + "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-simple-access": "^7.17.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/helper-validator-identifier": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.3", + "@babel/types": "^7.17.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", + "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", + "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.17.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", + "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", + "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.9.tgz", + "integrity": "sha512-cPCt915ShDWUEzEp3+UNRktO2n6v49l5RSnG9M5pS24hA+2FAc5si+Pn1i4VVbQQ+jh+bIZhPFQOJOzbrOYY1Q==", + "dev": true, + "dependencies": { + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.9", + "@babel/types": "^7.17.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", + "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.16.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.10.tgz", + "integrity": "sha512-n2Q6i+fnJqzOaq2VkdXxy2TCPCWQZHiCo0XqmrCvDWcZQKRyZzYi4Z0yxlBuN0w+r2ZHmre+Q087DSrw3pbJDQ==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.7.tgz", + "integrity": "sha512-Esxmk7YjA8QysKeT3VhTXvF6y77f/a91SIs4pWb4H2eWGQkCKFgQaG6hdoEVZtGsrAcb2K5BW66XsOErD4WU3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.17.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.17.3.tgz", + "integrity": "sha512-9tjBm4O07f7mzKSIlEmPdiE6ub7kfIe6Cd+w+oQebpATfTQMAgW+YOuWxogbKVTulA+MEO7byMeIUtQ1z+z+ZQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-jsx": "^7.16.7", + "@babel/types": "^7.17.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.16.7.tgz", + "integrity": "sha512-RMvQWvpla+xy6MlBpPlrKZCMRs2AGiHOGHY3xRwl0pEeim348dDyxeH4xBsMPbIMhujeq7ihE702eM2Ew0Wo+A==", + "dev": true, + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.16.7.tgz", + "integrity": "sha512-oe5VuWs7J9ilH3BCCApGoYjHoSO48vkjX2CbA5bFVhIuO2HKxA3vyF7rleA4o6/4rTDbk6r8hBW7Ul8E+UZrpA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.16.7.tgz", + "integrity": "sha512-rONFiQz9vgbsnaMtQlZCjIRwhJvlrPET8TabIUK2hzlXw9B9s2Ieaxte1SCOOXMbWRHodbKixNf3BLcWVOQ8Bw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz", + "integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==", + "dependencies": { + "regenerator-runtime": "^0.13.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", + "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.16.7", + "@babel/parser": "^7.16.7", + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", + "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.17.10", + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-function-name": "^7.17.9", + "@babel/helper-hoist-variables": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/parser": "^7.17.10", + "@babel/types": "^7.17.10", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", + "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz", + "integrity": "sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.1.tgz", + "integrity": "sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", + "integrity": "sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz", + "integrity": "sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-4.0.0.tgz", + "integrity": "sha512-+rumQFiaNac9y64OHtkHGmdjm7us9bo1PlbgQfdihQtuNxzjpaB064HbRnewUOggLQxVCCyINfStkgmBeQpv1g==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + }, + "peerDependencies": { + "rollup": "^1.20.0 || ^2.0.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "node_modules/@types/node": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.32.tgz", + "integrity": "sha512-eAIcfAvhf/BkHcf4pkLJ7ECpBAhh9kcxRBpip9cTiO+hf+aJrsxYxBeS6OXvOd9WqNAJmavXVpZvY1rBjNsXmw==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.4", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", + "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.0.9", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.9.tgz", + "integrity": "sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.3.tgz", + "integrity": "sha512-1RRW9kst+67gveJRYPxGmVy8eVJ05O43hg77G2j5m76/RFJtMbcfAs2viQ2UNsvvDg8F7OfQZx8qQcl6ymygaQ==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", + "dev": true + }, + "node_modules/@vitejs/plugin-react": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-1.3.2.tgz", + "integrity": "sha512-aurBNmMo0kz1O4qRoY+FM4epSA39y3ShWGuqfLRA/3z0oEJAdtoSfgA3aO98/PCCHAqMaduLxIxErWrVKIFzXA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.17.10", + "@babel/plugin-transform-react-jsx": "^7.17.3", + "@babel/plugin-transform-react-jsx-development": "^7.16.7", + "@babel/plugin-transform-react-jsx-self": "^7.16.7", + "@babel/plugin-transform-react-jsx-source": "^7.16.7", + "@rollup/pluginutils": "^4.2.1", + "react-refresh": "^0.13.0", + "resolve": "^1.22.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@vitejs/plugin-react/node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@vitejs/plugin-react/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/browserslist": { + "version": "4.20.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", + "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001332", + "electron-to-chromium": "^1.4.118", + "escalade": "^3.1.1", + "node-releases": "^2.0.3", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001339", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001339.tgz", + "integrity": "sha512-Es8PiVqCe+uXdms0Gu5xP5PF2bxLR7OBp3wUzUnuO7OHzhOfCyg3hdiGWVPVxhiuniOzng+hTc1u3fEQ0TlkSQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.1" + } + }, + "node_modules/csstype": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz", + "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.137", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz", + "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.38.tgz", + "integrity": "sha512-12fzJ0fsm7gVZX1YQ1InkOE5f9Tl7cgf6JPYXRJtPIoE0zkWAbHdPHVPPaLi9tYAcEBqheGzqLn/3RdTOyBfcA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "esbuild-android-64": "0.14.38", + "esbuild-android-arm64": "0.14.38", + "esbuild-darwin-64": "0.14.38", + "esbuild-darwin-arm64": "0.14.38", + "esbuild-freebsd-64": "0.14.38", + "esbuild-freebsd-arm64": "0.14.38", + "esbuild-linux-32": "0.14.38", + "esbuild-linux-64": "0.14.38", + "esbuild-linux-arm": "0.14.38", + "esbuild-linux-arm64": "0.14.38", + "esbuild-linux-mips64le": "0.14.38", + "esbuild-linux-ppc64le": "0.14.38", + "esbuild-linux-riscv64": "0.14.38", + "esbuild-linux-s390x": "0.14.38", + "esbuild-netbsd-64": "0.14.38", + "esbuild-openbsd-64": "0.14.38", + "esbuild-sunos-64": "0.14.38", + "esbuild-windows-32": "0.14.38", + "esbuild-windows-64": "0.14.38", + "esbuild-windows-arm64": "0.14.38" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.38.tgz", + "integrity": "sha512-aRFxR3scRKkbmNuGAK+Gee3+yFxkTJO/cx83Dkyzo4CnQl/2zVSurtG6+G86EQIZ+w+VYngVyK7P3HyTBKu3nw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.38.tgz", + "integrity": "sha512-L2NgQRWuHFI89IIZIlpAcINy9FvBk6xFVZ7xGdOwIm8VyhX1vNCEqUJO3DPSSy945Gzdg98cxtNt8Grv1CsyhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.38.tgz", + "integrity": "sha512-5JJvgXkX87Pd1Og0u/NJuO7TSqAikAcQQ74gyJ87bqWRVeouky84ICoV4sN6VV53aTW+NE87qLdGY4QA2S7KNA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.38.tgz", + "integrity": "sha512-eqF+OejMI3mC5Dlo9Kdq/Ilbki9sQBw3QlHW3wjLmsLh+quNfHmGMp3Ly1eWm981iGBMdbtSS9+LRvR2T8B3eQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.38.tgz", + "integrity": "sha512-epnPbhZUt93xV5cgeY36ZxPXDsQeO55DppzsIgWM8vgiG/Rz+qYDLmh5ts3e+Ln1wA9dQ+nZmVHw+RjaW3I5Ig==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.38.tgz", + "integrity": "sha512-/9icXUYJWherhk+y5fjPI5yNUdFPtXHQlwP7/K/zg8t8lQdHVj20SqU9/udQmeUo5pDFHMYzcEFfJqgOVeKNNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.38.tgz", + "integrity": "sha512-QfgfeNHRFvr2XeHFzP8kOZVnal3QvST3A0cgq32ZrHjSMFTdgXhMhmWdKzRXP/PKcfv3e2OW9tT9PpcjNvaq6g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.38.tgz", + "integrity": "sha512-uuZHNmqcs+Bj1qiW9k/HZU3FtIHmYiuxZ/6Aa+/KHb/pFKr7R3aVqvxlAudYI9Fw3St0VCPfv7QBpUITSmBR1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.38.tgz", + "integrity": "sha512-FiFvQe8J3VKTDXG01JbvoVRXQ0x6UZwyrU4IaLBZeq39Bsbatd94Fuc3F1RGqPF5RbIWW7RvkVQjn79ejzysnA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.38.tgz", + "integrity": "sha512-HlMGZTEsBrXrivr64eZ/EO0NQM8H8DuSENRok9d+Jtvq8hOLzrxfsAT9U94K3KOGk2XgCmkaI2KD8hX7F97lvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.38.tgz", + "integrity": "sha512-qd1dLf2v7QBiI5wwfil9j0HG/5YMFBAmMVmdeokbNAMbcg49p25t6IlJFXAeLzogv1AvgaXRXvgFNhScYEUXGQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.38.tgz", + "integrity": "sha512-mnbEm7o69gTl60jSuK+nn+pRsRHGtDPfzhrqEUXyCl7CTOCLtWN2bhK8bgsdp6J/2NyS/wHBjs1x8aBWwP2X9Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.38.tgz", + "integrity": "sha512-+p6YKYbuV72uikChRk14FSyNJZ4WfYkffj6Af0/Tw63/6TJX6TnIKE+6D3xtEc7DeDth1fjUOEqm+ApKFXbbVQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.38.tgz", + "integrity": "sha512-0zUsiDkGJiMHxBQ7JDU8jbaanUY975CdOW1YDrurjrM0vWHfjv9tLQsW9GSyEb/heSK1L5gaweRjzfUVBFoybQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.38.tgz", + "integrity": "sha512-cljBAApVwkpnJZfnRVThpRBGzCi+a+V9Ofb1fVkKhtrPLDYlHLrSYGtmnoTVWDQdU516qYI8+wOgcGZ4XIZh0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.38.tgz", + "integrity": "sha512-CDswYr2PWPGEPpLDUO50mL3WO/07EMjnZDNKpmaxUPsrW+kVM3LoAqr/CE8UbzugpEiflYqJsGPLirThRB18IQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.38.tgz", + "integrity": "sha512-2mfIoYW58gKcC3bck0j7lD3RZkqYA7MmujFYmSn9l6TiIcAMpuEvqksO+ntBgbLep/eyjpgdplF7b+4T9VJGOA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.38.tgz", + "integrity": "sha512-L2BmEeFZATAvU+FJzJiRLFUP+d9RHN+QXpgaOrs2klshoAm1AE6Us4X6fS9k33Uy5SzScn2TpcgecbqJza1Hjw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.38.tgz", + "integrity": "sha512-Khy4wVmebnzue8aeSXLC+6clo/hRYeNIm0DyikoEqX+3w3rcvrhzpoix0S+MF9vzh6JFskkIGD7Zx47ODJNyCw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.38.tgz", + "integrity": "sha512-k3FGCNmHBkqdJXuJszdWciAH77PukEyDsdIryEHn9cKLQFxzhT39dSumeTuggaQcXY57UlmLGIkklWZo2qzHpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/history": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", + "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", + "dependencies": { + "@babel/runtime": "^7.7.6" + } + }, + "node_modules/is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.4" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz", + "integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==", + "dev": true + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.13", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.13.tgz", + "integrity": "sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.3", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.1.0.tgz", + "integrity": "sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz", + "integrity": "sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.22.0" + }, + "peerDependencies": { + "react": "^18.1.0" + } + }, + "node_modules/react-refresh": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz", + "integrity": "sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", + "integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==", + "dependencies": { + "history": "^5.2.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz", + "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==", + "dependencies": { + "history": "^5.2.0", + "react-router": "6.3.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + }, + "node_modules/resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "2.72.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.72.1.tgz", + "integrity": "sha512-NTc5UGy/NWFGpSqF1lFY8z9Adri6uhyMLI6LvPAXdBKoPRFhIIiBUpt+Qg2awixqO3xvzSijjhnb4+QEZwJmxA==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/scheduler": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz", + "integrity": "sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz", + "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/vite": { + "version": "2.9.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.9.tgz", + "integrity": "sha512-ffaam+NgHfbEmfw/Vuh6BHKKlI/XIAhxE5QSS7gFLIngxg171mg1P3a4LSRME0z2ZU1ScxoKzphkipcYwSD5Ew==", + "dev": true, + "dependencies": { + "esbuild": "^0.14.27", + "postcss": "^8.4.13", + "resolve": "^1.22.0", + "rollup": "^2.59.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": ">=12.2.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "less": "*", + "sass": "*", + "stylus": "*" + }, + "peerDependenciesMeta": { + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + } + } + } + }, + "dependencies": { + "@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/code-frame": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.16.7" + } + }, + "@babel/compat-data": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.10.tgz", + "integrity": "sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw==", + "dev": true + }, + "@babel/core": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.10.tgz", + "integrity": "sha512-liKoppandF3ZcBnIYFjfSDHZLKdLHGJRkoWtG8zQyGJBQfIYobpnVGI5+pLBNtS6psFLDzyq8+h5HiVljW9PNA==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.17.10", + "@babel/helper-compilation-targets": "^7.17.10", + "@babel/helper-module-transforms": "^7.17.7", + "@babel/helpers": "^7.17.9", + "@babel/parser": "^7.17.10", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.10", + "@babel/types": "^7.17.10", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + } + }, + "@babel/generator": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", + "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", + "dev": true, + "requires": { + "@babel/types": "^7.17.10", + "@jridgewell/gen-mapping": "^0.1.0", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", + "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.10.tgz", + "integrity": "sha512-gh3RxjWbauw/dFiU/7whjd0qN9K6nPJMqe6+Er7rOavFh0CQUSwhAE3IcTho2rywPJFxej6TUUHDkWcYI6gGqQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.17.10", + "@babel/helper-validator-option": "^7.16.7", + "browserslist": "^4.20.2", + "semver": "^6.3.0" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz", + "integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-function-name": { + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", + "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", + "dev": true, + "requires": { + "@babel/template": "^7.16.7", + "@babel/types": "^7.17.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", + "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-module-imports": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", + "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-module-transforms": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", + "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-simple-access": "^7.17.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/helper-validator-identifier": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.3", + "@babel/types": "^7.17.0" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", + "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==", + "dev": true + }, + "@babel/helper-simple-access": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", + "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", + "dev": true, + "requires": { + "@babel/types": "^7.17.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", + "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", + "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", + "dev": true + }, + "@babel/helpers": { + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.9.tgz", + "integrity": "sha512-cPCt915ShDWUEzEp3+UNRktO2n6v49l5RSnG9M5pS24hA+2FAc5si+Pn1i4VVbQQ+jh+bIZhPFQOJOzbrOYY1Q==", + "dev": true, + "requires": { + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.9", + "@babel/types": "^7.17.0" + } + }, + "@babel/highlight": { + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", + "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.10.tgz", + "integrity": "sha512-n2Q6i+fnJqzOaq2VkdXxy2TCPCWQZHiCo0XqmrCvDWcZQKRyZzYi4Z0yxlBuN0w+r2ZHmre+Q087DSrw3pbJDQ==", + "dev": true + }, + "@babel/plugin-syntax-jsx": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.7.tgz", + "integrity": "sha512-Esxmk7YjA8QysKeT3VhTXvF6y77f/a91SIs4pWb4H2eWGQkCKFgQaG6hdoEVZtGsrAcb2K5BW66XsOErD4WU3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-react-jsx": { + "version": "7.17.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.17.3.tgz", + "integrity": "sha512-9tjBm4O07f7mzKSIlEmPdiE6ub7kfIe6Cd+w+oQebpATfTQMAgW+YOuWxogbKVTulA+MEO7byMeIUtQ1z+z+ZQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-jsx": "^7.16.7", + "@babel/types": "^7.17.0" + } + }, + "@babel/plugin-transform-react-jsx-development": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.16.7.tgz", + "integrity": "sha512-RMvQWvpla+xy6MlBpPlrKZCMRs2AGiHOGHY3xRwl0pEeim348dDyxeH4xBsMPbIMhujeq7ihE702eM2Ew0Wo+A==", + "dev": true, + "requires": { + "@babel/plugin-transform-react-jsx": "^7.16.7" + } + }, + "@babel/plugin-transform-react-jsx-self": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.16.7.tgz", + "integrity": "sha512-oe5VuWs7J9ilH3BCCApGoYjHoSO48vkjX2CbA5bFVhIuO2HKxA3vyF7rleA4o6/4rTDbk6r8hBW7Ul8E+UZrpA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-react-jsx-source": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.16.7.tgz", + "integrity": "sha512-rONFiQz9vgbsnaMtQlZCjIRwhJvlrPET8TabIUK2hzlXw9B9s2Ieaxte1SCOOXMbWRHodbKixNf3BLcWVOQ8Bw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/runtime": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz", + "integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@babel/template": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", + "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.16.7", + "@babel/parser": "^7.16.7", + "@babel/types": "^7.16.7" + } + }, + "@babel/traverse": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", + "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.17.10", + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-function-name": "^7.17.9", + "@babel/helper-hoist-variables": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/parser": "^7.17.10", + "@babel/types": "^7.17.10", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", + "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz", + "integrity": "sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.1.tgz", + "integrity": "sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", + "integrity": "sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz", + "integrity": "sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@rollup/plugin-replace": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-4.0.0.tgz", + "integrity": "sha512-+rumQFiaNac9y64OHtkHGmdjm7us9bo1PlbgQfdihQtuNxzjpaB064HbRnewUOggLQxVCCyINfStkgmBeQpv1g==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + } + }, + "@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + } + }, + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "@types/node": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.32.tgz", + "integrity": "sha512-eAIcfAvhf/BkHcf4pkLJ7ECpBAhh9kcxRBpip9cTiO+hf+aJrsxYxBeS6OXvOd9WqNAJmavXVpZvY1rBjNsXmw==", + "dev": true + }, + "@types/prop-types": { + "version": "15.7.4", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", + "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==", + "dev": true + }, + "@types/react": { + "version": "18.0.9", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.9.tgz", + "integrity": "sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.3.tgz", + "integrity": "sha512-1RRW9kst+67gveJRYPxGmVy8eVJ05O43hg77G2j5m76/RFJtMbcfAs2viQ2UNsvvDg8F7OfQZx8qQcl6ymygaQ==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", + "dev": true + }, + "@vitejs/plugin-react": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-1.3.2.tgz", + "integrity": "sha512-aurBNmMo0kz1O4qRoY+FM4epSA39y3ShWGuqfLRA/3z0oEJAdtoSfgA3aO98/PCCHAqMaduLxIxErWrVKIFzXA==", + "dev": true, + "requires": { + "@babel/core": "^7.17.10", + "@babel/plugin-transform-react-jsx": "^7.17.3", + "@babel/plugin-transform-react-jsx-development": "^7.16.7", + "@babel/plugin-transform-react-jsx-self": "^7.16.7", + "@babel/plugin-transform-react-jsx-source": "^7.16.7", + "@rollup/pluginutils": "^4.2.1", + "react-refresh": "^0.13.0", + "resolve": "^1.22.0" + }, + "dependencies": { + "@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "requires": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + } + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + } + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "browserslist": { + "version": "4.20.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", + "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001332", + "electron-to-chromium": "^1.4.118", + "escalade": "^3.1.1", + "node-releases": "^2.0.3", + "picocolors": "^1.0.0" + } + }, + "caniuse-lite": { + "version": "1.0.30001339", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001339.tgz", + "integrity": "sha512-Es8PiVqCe+uXdms0Gu5xP5PF2bxLR7OBp3wUzUnuO7OHzhOfCyg3hdiGWVPVxhiuniOzng+hTc1u3fEQ0TlkSQ==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "csstype": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz", + "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "electron-to-chromium": { + "version": "1.4.137", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz", + "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", + "dev": true + }, + "esbuild": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.38.tgz", + "integrity": "sha512-12fzJ0fsm7gVZX1YQ1InkOE5f9Tl7cgf6JPYXRJtPIoE0zkWAbHdPHVPPaLi9tYAcEBqheGzqLn/3RdTOyBfcA==", + "dev": true, + "requires": { + "esbuild-android-64": "0.14.38", + "esbuild-android-arm64": "0.14.38", + "esbuild-darwin-64": "0.14.38", + "esbuild-darwin-arm64": "0.14.38", + "esbuild-freebsd-64": "0.14.38", + "esbuild-freebsd-arm64": "0.14.38", + "esbuild-linux-32": "0.14.38", + "esbuild-linux-64": "0.14.38", + "esbuild-linux-arm": "0.14.38", + "esbuild-linux-arm64": "0.14.38", + "esbuild-linux-mips64le": "0.14.38", + "esbuild-linux-ppc64le": "0.14.38", + "esbuild-linux-riscv64": "0.14.38", + "esbuild-linux-s390x": "0.14.38", + "esbuild-netbsd-64": "0.14.38", + "esbuild-openbsd-64": "0.14.38", + "esbuild-sunos-64": "0.14.38", + "esbuild-windows-32": "0.14.38", + "esbuild-windows-64": "0.14.38", + "esbuild-windows-arm64": "0.14.38" + } + }, + "esbuild-android-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.38.tgz", + "integrity": "sha512-aRFxR3scRKkbmNuGAK+Gee3+yFxkTJO/cx83Dkyzo4CnQl/2zVSurtG6+G86EQIZ+w+VYngVyK7P3HyTBKu3nw==", + "dev": true, + "optional": true + }, + "esbuild-android-arm64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.38.tgz", + "integrity": "sha512-L2NgQRWuHFI89IIZIlpAcINy9FvBk6xFVZ7xGdOwIm8VyhX1vNCEqUJO3DPSSy945Gzdg98cxtNt8Grv1CsyhA==", + "dev": true, + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.38.tgz", + "integrity": "sha512-5JJvgXkX87Pd1Og0u/NJuO7TSqAikAcQQ74gyJ87bqWRVeouky84ICoV4sN6VV53aTW+NE87qLdGY4QA2S7KNA==", + "dev": true, + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.38.tgz", + "integrity": "sha512-eqF+OejMI3mC5Dlo9Kdq/Ilbki9sQBw3QlHW3wjLmsLh+quNfHmGMp3Ly1eWm981iGBMdbtSS9+LRvR2T8B3eQ==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.38.tgz", + "integrity": "sha512-epnPbhZUt93xV5cgeY36ZxPXDsQeO55DppzsIgWM8vgiG/Rz+qYDLmh5ts3e+Ln1wA9dQ+nZmVHw+RjaW3I5Ig==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.38.tgz", + "integrity": "sha512-/9icXUYJWherhk+y5fjPI5yNUdFPtXHQlwP7/K/zg8t8lQdHVj20SqU9/udQmeUo5pDFHMYzcEFfJqgOVeKNNQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-32": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.38.tgz", + "integrity": "sha512-QfgfeNHRFvr2XeHFzP8kOZVnal3QvST3A0cgq32ZrHjSMFTdgXhMhmWdKzRXP/PKcfv3e2OW9tT9PpcjNvaq6g==", + "dev": true, + "optional": true + }, + "esbuild-linux-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.38.tgz", + "integrity": "sha512-uuZHNmqcs+Bj1qiW9k/HZU3FtIHmYiuxZ/6Aa+/KHb/pFKr7R3aVqvxlAudYI9Fw3St0VCPfv7QBpUITSmBR1Q==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.38.tgz", + "integrity": "sha512-FiFvQe8J3VKTDXG01JbvoVRXQ0x6UZwyrU4IaLBZeq39Bsbatd94Fuc3F1RGqPF5RbIWW7RvkVQjn79ejzysnA==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.38.tgz", + "integrity": "sha512-HlMGZTEsBrXrivr64eZ/EO0NQM8H8DuSENRok9d+Jtvq8hOLzrxfsAT9U94K3KOGk2XgCmkaI2KD8hX7F97lvA==", + "dev": true, + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.38.tgz", + "integrity": "sha512-qd1dLf2v7QBiI5wwfil9j0HG/5YMFBAmMVmdeokbNAMbcg49p25t6IlJFXAeLzogv1AvgaXRXvgFNhScYEUXGQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.38.tgz", + "integrity": "sha512-mnbEm7o69gTl60jSuK+nn+pRsRHGtDPfzhrqEUXyCl7CTOCLtWN2bhK8bgsdp6J/2NyS/wHBjs1x8aBWwP2X9Q==", + "dev": true, + "optional": true + }, + "esbuild-linux-riscv64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.38.tgz", + "integrity": "sha512-+p6YKYbuV72uikChRk14FSyNJZ4WfYkffj6Af0/Tw63/6TJX6TnIKE+6D3xtEc7DeDth1fjUOEqm+ApKFXbbVQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-s390x": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.38.tgz", + "integrity": "sha512-0zUsiDkGJiMHxBQ7JDU8jbaanUY975CdOW1YDrurjrM0vWHfjv9tLQsW9GSyEb/heSK1L5gaweRjzfUVBFoybQ==", + "dev": true, + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.38.tgz", + "integrity": "sha512-cljBAApVwkpnJZfnRVThpRBGzCi+a+V9Ofb1fVkKhtrPLDYlHLrSYGtmnoTVWDQdU516qYI8+wOgcGZ4XIZh0Q==", + "dev": true, + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.38.tgz", + "integrity": "sha512-CDswYr2PWPGEPpLDUO50mL3WO/07EMjnZDNKpmaxUPsrW+kVM3LoAqr/CE8UbzugpEiflYqJsGPLirThRB18IQ==", + "dev": true, + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.38.tgz", + "integrity": "sha512-2mfIoYW58gKcC3bck0j7lD3RZkqYA7MmujFYmSn9l6TiIcAMpuEvqksO+ntBgbLep/eyjpgdplF7b+4T9VJGOA==", + "dev": true, + "optional": true + }, + "esbuild-windows-32": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.38.tgz", + "integrity": "sha512-L2BmEeFZATAvU+FJzJiRLFUP+d9RHN+QXpgaOrs2klshoAm1AE6Us4X6fS9k33Uy5SzScn2TpcgecbqJza1Hjw==", + "dev": true, + "optional": true + }, + "esbuild-windows-64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.38.tgz", + "integrity": "sha512-Khy4wVmebnzue8aeSXLC+6clo/hRYeNIm0DyikoEqX+3w3rcvrhzpoix0S+MF9vzh6JFskkIGD7Zx47ODJNyCw==", + "dev": true, + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.14.38", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.38.tgz", + "integrity": "sha512-k3FGCNmHBkqdJXuJszdWciAH77PukEyDsdIryEHn9cKLQFxzhT39dSumeTuggaQcXY57UlmLGIkklWZo2qzHpw==", + "dev": true, + "optional": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "history": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", + "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", + "requires": { + "@babel/runtime": "^7.7.6" + } + }, + "is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true + }, + "node-releases": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz", + "integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true + }, + "postcss": { + "version": "8.4.13", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.13.tgz", + "integrity": "sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA==", + "dev": true, + "requires": { + "nanoid": "^3.3.3", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "react": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.1.0.tgz", + "integrity": "sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz", + "integrity": "sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==", + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.22.0" + } + }, + "react-refresh": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz", + "integrity": "sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==", + "dev": true + }, + "react-router": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", + "integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==", + "requires": { + "history": "^5.2.0" + } + }, + "react-router-dom": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz", + "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==", + "requires": { + "history": "^5.2.0", + "react-router": "6.3.0" + } + }, + "regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + }, + "resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dev": true, + "requires": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "rollup": { + "version": "2.72.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.72.1.tgz", + "integrity": "sha512-NTc5UGy/NWFGpSqF1lFY8z9Adri6uhyMLI6LvPAXdBKoPRFhIIiBUpt+Qg2awixqO3xvzSijjhnb4+QEZwJmxA==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "scheduler": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz", + "integrity": "sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "typescript": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz", + "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==", + "dev": true + }, + "vite": { + "version": "2.9.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.9.tgz", + "integrity": "sha512-ffaam+NgHfbEmfw/Vuh6BHKKlI/XIAhxE5QSS7gFLIngxg171mg1P3a4LSRME0z2ZU1ScxoKzphkipcYwSD5Ew==", + "dev": true, + "requires": { + "esbuild": "^0.14.27", + "fsevents": "~2.3.2", + "postcss": "^8.4.13", + "resolve": "^1.22.0", + "rollup": "^2.59.0" + } + } + } +} diff --git a/examples/scroll-restoration/package.json b/examples/scroll-restoration/package.json new file mode 100644 index 0000000000..84b76628b4 --- /dev/null +++ b/examples/scroll-restoration/package.json @@ -0,0 +1,24 @@ +{ + "name": "scroll-restoration", + "private": true, + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "serve": "vite preview" + }, + "dependencies": { + "react": "18.1.0", + "react-dom": "18.1.0", + "react-router": "6.3.0", + "react-router-dom": "6.3.0" + }, + "devDependencies": { + "@rollup/plugin-replace": "4.0.0", + "@types/node": "17.0.32", + "@types/react": "18.0.9", + "@types/react-dom": "18.0.3", + "@vitejs/plugin-react": "1.3.2", + "typescript": "4.6.4", + "vite": "2.9.9" + } +} diff --git a/examples/scroll-restoration/src/App.tsx b/examples/scroll-restoration/src/App.tsx new file mode 100644 index 0000000000..e7ae5e0cca --- /dev/null +++ b/examples/scroll-restoration/src/App.tsx @@ -0,0 +1,135 @@ +import React from "react"; +import type { DataRouteMatch, Location } from "react-router-dom"; +import { + DataBrowserRouter, + ScrollRestoration, + useLocation, + Link, + Route, + Outlet, +} from "react-router-dom"; + +function Layout() { + // You can provide a custom implementation of what "key" should be used to + // cache scroll positions for a given location. Using the location.key will + // provide standard browser behavior and only restore on back/forward + // navigations. Using location.pathname will provide more aggressive + // restoration and will also restore on normal link navigations to a + // previously-accessed path. Or - go nuts and lump many pages into a + // single key (i.e., anything /wizard/* uses the same key)! + let getKey = React.useCallback( + (location: Location, matches: DataRouteMatch[]) => { + let match = matches.find((m) => m.route.handle?.scrollMode); + if (match?.route.handle?.scrollMode === "pathname") { + return location.pathname; + } + + return location.key; + }, + [] + ); + + return ( + <> + +
+
+
+ +
+
+
+ +
+
+ {/* + Including this component inside a DataRouter component tree is what + enables restoration + */} + + + ); +} + +function LongPage() { + let location = useLocation(); + return ( + <> +

Long Page

+ {new Array(100).fill(null).map((n, i) => ( +

+ Item {i} on {location.pathname} +

+ ))} +

This is a linkable heading

+ {new Array(100).fill(null).map((n, i) => ( +

+ Item {i + 100} on {location.pathname} +

+ ))} + + ); +} + +function App() { + return ( + + }> + Home} /> + } /> + } + handle={{ scrollMode: "pathname" }} + /> + } /> + + + ); +} + +export default App; diff --git a/examples/scroll-restoration/src/index.css b/examples/scroll-restoration/src/index.css new file mode 100644 index 0000000000..3e1f253f03 --- /dev/null +++ b/examples/scroll-restoration/src/index.css @@ -0,0 +1,12 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", + monospace; +} diff --git a/examples/scroll-restoration/src/main.tsx b/examples/scroll-restoration/src/main.tsx new file mode 100644 index 0000000000..201a661a90 --- /dev/null +++ b/examples/scroll-restoration/src/main.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; + +import "./index.css"; +import App from "./App"; + +createRoot(document.getElementById("root")).render( + + + +); diff --git a/examples/scroll-restoration/src/vite-env.d.ts b/examples/scroll-restoration/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/scroll-restoration/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/scroll-restoration/tsconfig.json b/examples/scroll-restoration/tsconfig.json new file mode 100644 index 0000000000..8bdaabfe5d --- /dev/null +++ b/examples/scroll-restoration/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react", + "importsNotUsedAsValues": "error" + }, + "include": ["./src"] +} diff --git a/examples/scroll-restoration/vite.config.ts b/examples/scroll-restoration/vite.config.ts new file mode 100644 index 0000000000..b77eb48a30 --- /dev/null +++ b/examples/scroll-restoration/vite.config.ts @@ -0,0 +1,36 @@ +import * as path from "path"; +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import rollupReplace from "@rollup/plugin-replace"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + rollupReplace({ + preventAssignment: true, + values: { + __DEV__: JSON.stringify(true), + "process.env.NODE_ENV": JSON.stringify("development"), + }, + }), + react(), + ], + resolve: process.env.USE_SOURCE + ? { + alias: { + "@remix-run/router": path.resolve( + __dirname, + "../../packages/router/index.ts" + ), + "react-router": path.resolve( + __dirname, + "../../packages/react-router/index.ts" + ), + "react-router-dom": path.resolve( + __dirname, + "../../packages/react-router-dom/index.tsx" + ), + }, + } + : {}, +}); From a44fb5f5bbaa23585c8066c186d3a3ab4f343ee3 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 16 May 2022 12:44:47 -0400 Subject: [PATCH 075/119] chore: switch from @web-std/fetch to @remix-run/web-fetch for tests --- package.json | 2 +- packages/react-router-dom/__tests__/setup.ts | 9 ++- packages/react-router/__tests__/setup.ts | 9 ++- packages/router/__tests__/setup.ts | 9 ++- yarn.lock | 75 +++++++++----------- 5 files changed, 56 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index 0faf5de68d..261c996baa 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@babel/preset-modules": "^0.1.4", "@babel/preset-react": "^7.14.5", "@babel/preset-typescript": "^7.15.0", + "@remix-run/web-fetch": "4.1.3", "@rollup/plugin-replace": "^2.2.1", "@testing-library/jest-dom": "5.16.3", "@testing-library/react": "12.1.4", @@ -34,7 +35,6 @@ "@types/use-sync-external-store": "0.0.3", "@typescript-eslint/eslint-plugin": "^4.28.3", "@typescript-eslint/parser": "^4.28.3", - "@web-std/fetch": "4.1.0", "babel-eslint": "^10.1.0", "babel-plugin-dev-expression": "^0.2.2", "chalk": "^4.1.1", diff --git a/packages/react-router-dom/__tests__/setup.ts b/packages/react-router-dom/__tests__/setup.ts index 7cb92582bf..6d9d8d4a16 100644 --- a/packages/react-router-dom/__tests__/setup.ts +++ b/packages/react-router-dom/__tests__/setup.ts @@ -1,10 +1,15 @@ -import fetch, { Request, Response, FormData } from "@web-std/fetch"; +import { fetch, Request, Response } from "@remix-run/web-fetch"; if (!globalThis.fetch) { + // Built-in lib.dom.d.ts expects `fetch(Request | string, ...)` but the web + // fetch API allows a URL so @remix-run/web-fetch defines + // `fetch(string | URL | Request, ...)` + // @ts-expect-error globalThis.fetch = fetch; + // Same as above, lib.dom.d.ts doesn't allow a URL to the Request constructor + // @ts-expect-error globalThis.Request = Request; // web-std/fetch Response does not currently implement Response.error() // @ts-expect-error globalThis.Response = Response; - globalThis.FormData = FormData; } diff --git a/packages/react-router/__tests__/setup.ts b/packages/react-router/__tests__/setup.ts index 7cb92582bf..6d9d8d4a16 100644 --- a/packages/react-router/__tests__/setup.ts +++ b/packages/react-router/__tests__/setup.ts @@ -1,10 +1,15 @@ -import fetch, { Request, Response, FormData } from "@web-std/fetch"; +import { fetch, Request, Response } from "@remix-run/web-fetch"; if (!globalThis.fetch) { + // Built-in lib.dom.d.ts expects `fetch(Request | string, ...)` but the web + // fetch API allows a URL so @remix-run/web-fetch defines + // `fetch(string | URL | Request, ...)` + // @ts-expect-error globalThis.fetch = fetch; + // Same as above, lib.dom.d.ts doesn't allow a URL to the Request constructor + // @ts-expect-error globalThis.Request = Request; // web-std/fetch Response does not currently implement Response.error() // @ts-expect-error globalThis.Response = Response; - globalThis.FormData = FormData; } diff --git a/packages/router/__tests__/setup.ts b/packages/router/__tests__/setup.ts index 7cb92582bf..6d9d8d4a16 100644 --- a/packages/router/__tests__/setup.ts +++ b/packages/router/__tests__/setup.ts @@ -1,10 +1,15 @@ -import fetch, { Request, Response, FormData } from "@web-std/fetch"; +import { fetch, Request, Response } from "@remix-run/web-fetch"; if (!globalThis.fetch) { + // Built-in lib.dom.d.ts expects `fetch(Request | string, ...)` but the web + // fetch API allows a URL so @remix-run/web-fetch defines + // `fetch(string | URL | Request, ...)` + // @ts-expect-error globalThis.fetch = fetch; + // Same as above, lib.dom.d.ts doesn't allow a URL to the Request constructor + // @ts-expect-error globalThis.Request = Request; // web-std/fetch Response does not currently implement Response.error() // @ts-expect-error globalThis.Response = Response; - globalThis.FormData = FormData; } diff --git a/yarn.lock b/yarn.lock index 79c7b9865a..25db8a2329 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1535,6 +1535,40 @@ wcwidth "^1.0.1" ws "^1.1.0" +"@remix-run/web-blob@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@remix-run/web-blob/-/web-blob-3.0.4.tgz#99c67b9d0fb641bd0c07d267fd218ae5aa4ae5ed" + integrity sha512-AfegzZvSSDc+LwnXV+SwROTrDtoLiPxeFW+jxgvtDAnkuCX1rrzmVJ6CzqZ1Ai0bVfmJadkG5GxtAfYclpPmgw== + dependencies: + "@remix-run/web-stream" "^1.0.0" + web-encoding "1.1.5" + +"@remix-run/web-fetch@4.1.3": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@remix-run/web-fetch/-/web-fetch-4.1.3.tgz#8ad3077c1b5bd9fe2a8813d0ad3c84970a495c04" + integrity sha512-D3KXAEkzhR248mu7wCHReQrMrIo3Y9pDDa7TrlISnsOEvqkfWkJJF+PQWmOIKpOSHAhDg7TCb2tzvW8lc/MfHw== + dependencies: + "@remix-run/web-blob" "^3.0.4" + "@remix-run/web-form-data" "^3.0.2" + "@remix-run/web-stream" "^1.0.3" + "@web3-storage/multipart-parser" "^1.0.0" + data-uri-to-buffer "^3.0.1" + mrmime "^1.0.0" + +"@remix-run/web-form-data@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@remix-run/web-form-data/-/web-form-data-3.0.2.tgz#733a4c8f8176523b7b60a8bd0dc6704fd4d498f3" + integrity sha512-F8tm3iB1sPxMpysK6Js7lV3gvLfTNKGmIW38t/e6dtPEB5L1WdbRG1cmLyhsonFc7rT1x1JKdz+2jCtoSdnIUw== + dependencies: + web-encoding "1.1.5" + +"@remix-run/web-stream@^1.0.0", "@remix-run/web-stream@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@remix-run/web-stream/-/web-stream-1.0.3.tgz#3284a6a45675d1455c4d9c8f31b89225c9006438" + integrity sha512-wlezlJaA5NF6SsNMiwQnnAW6tnPzQ5I8qk0Y0pSohm0eHKa2FQ1QhEKLVVcDDu02TmkfHgnux0igNfeYhDOXiA== + dependencies: + web-streams-polyfill "^3.1.1" + "@rollup/plugin-replace@^2.2.1": version "2.4.2" resolved "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz" @@ -1955,47 +1989,6 @@ resolved "https://registry.npmjs.org/@ungap/url-search-params/-/url-search-params-0.1.4.tgz" integrity sha512-RLwrxCTDNiNev9hpr9rDq8NyeQ8Nn0X1we4Wu7Tlf368I8r+7hBj3uObhifhuLk74egaYaSX5nUsBlWz6kjj+A== -"@web-std/blob@^3.0.3": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@web-std/blob/-/blob-3.0.4.tgz#dd67a685547331915428d69e723c7da2015c3fc5" - integrity sha512-+dibyiw+uHYK4dX5cJ7HA+gtDAaUUe6JsOryp2ZpAC7h4ICsh49E34JwHoEKPlPvP0llCrNzz45vvD+xX5QDBg== - dependencies: - "@web-std/stream" "1.0.0" - web-encoding "1.1.5" - -"@web-std/fetch@4.1.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@web-std/fetch/-/fetch-4.1.0.tgz#db1eb659198376dad692421896b7119fb13e6e52" - integrity sha512-ZRizMcP8YyuRlhIsRYNFD9x/w28K7kbUhNGmKM9hDy4qeQ5xMTk//wA89EF+Clbl6EP4ksmCcN+4TqBMSRL8Zw== - dependencies: - "@web-std/blob" "^3.0.3" - "@web-std/form-data" "^3.0.2" - "@web-std/stream" "^1.0.1" - "@web3-storage/multipart-parser" "^1.0.0" - data-uri-to-buffer "^3.0.1" - mrmime "^1.0.0" - -"@web-std/form-data@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@web-std/form-data/-/form-data-3.0.2.tgz#c71d9def6a593138ea92fe3d1ffbce19f43e869c" - integrity sha512-rhc8IRw66sJ0FHcnC84kT3mTN6eACTuNftkt1XSl1Ef6WRKq4Pz65xixxqZymAZl1K3USpwhLci4SKNn4PYxWQ== - dependencies: - web-encoding "1.1.5" - -"@web-std/stream@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@web-std/stream/-/stream-1.0.0.tgz#01066f40f536e4329d9b696dc29872f3a14b93c1" - integrity sha512-jyIbdVl+0ZJyKGTV0Ohb9E6UnxP+t7ZzX4Do3AHjZKxUXKMs9EmqnBDQgHF7bEw0EzbQygOjtt/7gvtmi//iCQ== - dependencies: - web-streams-polyfill "^3.1.1" - -"@web-std/stream@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@web-std/stream/-/stream-1.0.1.tgz#af2972654848e20a683781b0a50bef2ce3f011a0" - integrity sha512-tsz4Y0WNDgFA5jwLSeV7/UV5rfMIlj0cPsSLVfTihjaVW0OJPd5NxJ3le1B3yLyqqzRpeG5OAfJAADLc4VoGTA== - dependencies: - web-streams-polyfill "^3.1.1" - "@web3-storage/multipart-parser@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@web3-storage/multipart-parser/-/multipart-parser-1.0.0.tgz#6b69dc2a32a5b207ba43e556c25cc136a56659c4" From 99bf102c3665570864bb506f33f93faa5dc2093f Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 16 May 2022 16:21:19 -0400 Subject: [PATCH 076/119] feat: add pending logic to NavLink (#8875) * feat: add pending logic to NavLink * chore: fixup type imports * chore: add useMemo to nextMatch calculation --- examples/custom-link/src/App.tsx | 2 +- .../__tests__/nav-link-active-test.tsx | 294 +++++++++++++++++- packages/react-router-dom/index.tsx | 70 +++-- 3 files changed, 340 insertions(+), 26 deletions(-) diff --git a/examples/custom-link/src/App.tsx b/examples/custom-link/src/App.tsx index aee8e0bf1d..8bb61adde3 100644 --- a/examples/custom-link/src/App.tsx +++ b/examples/custom-link/src/App.tsx @@ -17,7 +17,7 @@ export default function App() {

This example demonstrates how to create a custom{" "} <Link> component that knows whether or not it is - "active" using the low-level useResolvedPath() and + "active" using the low-level useResolvedPath() and{" "} useMatch() hooks.

diff --git a/packages/react-router-dom/__tests__/nav-link-active-test.tsx b/packages/react-router-dom/__tests__/nav-link-active-test.tsx index 458ba92d2c..c8e133e21e 100644 --- a/packages/react-router-dom/__tests__/nav-link-active-test.tsx +++ b/packages/react-router-dom/__tests__/nav-link-active-test.tsx @@ -1,6 +1,21 @@ +/** + * @jest-environment ./__tests__/custom-environment.js + */ + +import { render, fireEvent, waitFor, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { JSDOM } from "jsdom"; import * as React from "react"; import * as TestRenderer from "react-test-renderer"; -import { MemoryRouter, Routes, Route, NavLink, Outlet } from "react-router-dom"; +import { + MemoryRouter, + Routes, + Route, + NavLink, + Outlet, + DataBrowserRouter, +} from "react-router-dom"; +import { _resetModuleScope } from "react-router/lib/components"; describe("NavLink", () => { describe("when it does not match", () => { @@ -310,6 +325,250 @@ describe("NavLink", () => { }); }); +describe("NavLink using a data router", () => { + afterEach(() => { + _resetModuleScope(); + }); + + it("applies the default 'active'/'pending' classNames to the underlying ", async () => { + let deferred = defer(); + render( + } + > + }> + Foo page

} /> + deferred.promise} + element={

Bar page

} + /> +
+
+ ); + + function Layout() { + return ( + <> + Link to Foo + Link to Bar + + + ); + } + + expect(screen.getByText("Link to Bar").className).toBe(""); + + fireEvent.click(screen.getByText("Link to Bar")); + expect(screen.getByText("Link to Bar").className).toBe("pending"); + + deferred.resolve(); + await waitFor(() => screen.getByText("Bar page")); + expect(screen.getByText("Link to Bar").className).toBe("active"); + }); + + it("applies its className correctly when provided as a function", async () => { + let deferred = defer(); + render( + } + > + }> + Foo page

} /> + deferred.promise} + element={

Bar page

} + /> +
+
+ ); + + function Layout() { + return ( + <> + Link to Foo + + isPending + ? "some-pending-classname" + : isActive + ? "some-active-classname" + : undefined + } + > + Link to Bar + + + + + ); + } + + expect(screen.getByText("Link to Bar").className).toBe(""); + + fireEvent.click(screen.getByText("Link to Bar")); + expect(screen.getByText("Link to Bar").className).toBe( + "some-pending-classname" + ); + + deferred.resolve(); + await waitFor(() => screen.getByText("Bar page")); + expect(screen.getByText("Link to Bar").className).toBe( + "some-active-classname" + ); + }); + + it("applies its style correctly when provided as a function", async () => { + let deferred = defer(); + render( + } + > + }> + Foo page

} /> + deferred.promise} + element={

Bar page

} + /> +
+
+ ); + + function Layout() { + return ( + <> + Link to Foo + + isPending + ? { textTransform: "underline" } + : isActive + ? { textTransform: "uppercase" } + : undefined + } + > + Link to Bar + + + + + ); + } + + expect(screen.getByText("Link to Bar").style.textTransform).toBe(""); + + fireEvent.click(screen.getByText("Link to Bar")); + expect(screen.getByText("Link to Bar").style.textTransform).toBe( + "underline" + ); + + deferred.resolve(); + await waitFor(() => screen.getByText("Bar page")); + expect(screen.getByText("Link to Bar").style.textTransform).toBe( + "uppercase" + ); + }); + + it("applies its children correctly when provided as a function", async () => { + let deferred = defer(); + render( + } + > + }> + Foo page

} /> + deferred.promise} + element={

Bar page

} + /> +
+
+ ); + + function Layout() { + return ( + <> + Link to Foo + + {({ isActive, isPending }) => + isPending + ? "Link to Bar (loading...)" + : isActive + ? "Link to Bar (current)" + : "Link to Bar (idle)" + } + + + + + ); + } + + expect(screen.getByText("Link to Bar (idle)")).toBeDefined(); + + fireEvent.click(screen.getByText("Link to Bar (idle)")); + expect(screen.getByText("Link to Bar (loading...)")).toBeDefined(); + + deferred.resolve(); + await waitFor(() => screen.getByText("Bar page")); + expect(screen.getByText("Link to Bar (current)")).toBeDefined(); + }); + + it("does not apply during transitions to non-matching locations", async () => { + let deferred = defer(); + render( + } + > + }> + Foo page

} /> + Bar page

} /> + deferred.promise} + element={

Baz page

} + /> +
+
+ ); + + function Layout() { + return ( + <> + Link to Foo + Link to Bar + Link to Baz + + + ); + } + + expect(screen.getByText("Link to Bar").className).toBe(""); + + fireEvent.click(screen.getByText("Link to Baz")); + expect(screen.getByText("Link to Bar").className).toBe(""); + + deferred.resolve(); + await waitFor(() => screen.getByText("Baz page")); + expect(screen.getByText("Link to Bar").className).toBe(""); + }); +}); + describe("NavLink under a Routes with a basename", () => { describe("when it does not match", () => { it("does not apply the default 'active' className to the underlying
", () => { @@ -352,3 +611,36 @@ describe("NavLink under a Routes with a basename", () => { }); }); }); + +function defer() { + let resolve: (val?: any) => Promise; + let reject: (error?: Error) => Promise; + let promise = new Promise((res, rej) => { + resolve = async (val: any) => { + 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, + }; +} + +function getWindow(initialUrl: string): Window { + // Need to use our own custom DOM in order to get a working history + const dom = new JSDOM(``, { url: "https://remix.run/" }); + dom.window.history.replaceState(null, "", initialUrl); + return dom.window as unknown as Window; +} diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index 38236d1fc7..8afc38bf8f 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -8,6 +8,7 @@ import { createPath, useHref, useLocation, + useMatch, useNavigate, useResolvedPath, UNSAFE_useRenderDataRouter, @@ -19,20 +20,21 @@ import type { To } from "react-router"; import type { BrowserHistory, Fetcher, + FormEncType, + FormMethod, HashHistory, History, + HydrationState, GetScrollRestorationKeyFunction, + RouteObject, } from "@remix-run/router"; import { createBrowserHistory, createHashHistory, createBrowserRouter, createHashRouter, - FormEncType, - FormMethod, - HydrationState, invariant, - RouteObject, + matchPath, } from "@remix-run/router"; import type { @@ -377,13 +379,21 @@ export interface NavLinkProps extends Omit { children?: | React.ReactNode - | ((props: { isActive: boolean }) => React.ReactNode); + | ((props: { isActive: boolean; isPending: boolean }) => React.ReactNode); caseSensitive?: boolean; - className?: string | ((props: { isActive: boolean }) => string | undefined); + className?: + | string + | ((props: { + isActive: boolean; + isPending: boolean; + }) => string | undefined); end?: boolean; style?: | React.CSSProperties - | ((props: { isActive: boolean }) => React.CSSProperties); + | ((props: { + isActive: boolean; + isPending: boolean; + }) => React.CSSProperties); } /** @@ -403,40 +413,50 @@ export const NavLink = React.forwardRef( }, ref ) { - let location = useLocation(); let path = useResolvedPath(to); + let match = useMatch({ path: path.pathname, end, caseSensitive }); + + let routerState = React.useContext(UNSAFE_DataRouterStateContext); + let nextLocation = routerState?.navigation.location; + let nextPath = useResolvedPath(nextLocation || ""); + let nextMatch = React.useMemo( + () => + nextLocation + ? matchPath( + { path: path.pathname, end, caseSensitive }, + nextPath.pathname + ) + : null, + [nextLocation, path.pathname, caseSensitive, end, nextPath.pathname] + ); - let locationPathname = location.pathname; - let toPathname = path.pathname; - if (!caseSensitive) { - locationPathname = locationPathname.toLowerCase(); - toPathname = toPathname.toLowerCase(); - } - - let isActive = - locationPathname === toPathname || - (!end && - locationPathname.startsWith(toPathname) && - locationPathname.charAt(toPathname.length) === "/"); + let isPending = nextMatch != null; + let isActive = match != null; let ariaCurrent = isActive ? ariaCurrentProp : undefined; let className: string | undefined; if (typeof classNameProp === "function") { - className = classNameProp({ isActive }); + className = classNameProp({ isActive, isPending }); } else { // If the className prop is not a function, we use a default `active` // class for s that are active. In v5 `active` was the default // value for `activeClassName`, but we are removing that API and can still // use the old default behavior for a cleaner upgrade path and keep the // simple styling rules working as they currently do. - className = [classNameProp, isActive ? "active" : null] + className = [ + classNameProp, + isActive ? "active" : null, + isPending ? "pending" : null, + ] .filter(Boolean) .join(" "); } let style = - typeof styleProp === "function" ? styleProp({ isActive }) : styleProp; + typeof styleProp === "function" + ? styleProp({ isActive, isPending }) + : styleProp; return ( ( style={style} to={to} > - {typeof children === "function" ? children({ isActive }) : children} + {typeof children === "function" + ? children({ isActive, isPending }) + : children} ); } From 70cab7472f80f86fde0f249872656887b65d8c59 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 16 May 2022 16:29:25 -0400 Subject: [PATCH 077/119] chore: inline react use-sync-external-store/shim for UMD builds --- examples/data-router/src/App.tsx | 17 +- packages/react-router/lib/components.tsx | 2 +- .../lib/use-sync-external-store-shim/index.ts | 31 ++++ .../useSyncExternalStoreShimClient.ts | 153 ++++++++++++++++++ .../useSyncExternalStoreShimServer.ts | 20 +++ 5 files changed, 215 insertions(+), 8 deletions(-) create mode 100644 packages/react-router/lib/use-sync-external-store-shim/index.ts create mode 100644 packages/react-router/lib/use-sync-external-store-shim/useSyncExternalStoreShimClient.ts create mode 100644 packages/react-router/lib/use-sync-external-store-shim/useSyncExternalStoreShimServer.ts diff --git a/examples/data-router/src/App.tsx b/examples/data-router/src/App.tsx index 330d2868d2..c5fbee1bb4 100644 --- a/examples/data-router/src/App.tsx +++ b/examples/data-router/src/App.tsx @@ -2,15 +2,16 @@ import React from "react"; import type { ActionFunction, LoaderFunction } from "react-router-dom"; import { DataBrowserRouter, - useLoaderData, - useNavigation, - useFetcher, - useFetchers, - useRouteError, Form, Link, Route, Outlet, + useFetcher, + useFetchers, + useLoaderData, + useNavigation, + useParams, + useRouteError, } from "react-router-dom"; import type { Todos } from "./todos"; @@ -197,11 +198,13 @@ const todoLoader: LoaderFunction = async ({ params }) => { }; function Todo() { + let params = useParams(); let todo = useLoaderData(); return ( <> -

Error ๐Ÿ’ฅ

-

{todo}

+

Nested Todo Route:

+

id: {params.id}

+

todo: {todo}

); } diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index 93dd7fe5a7..b9fb2e8c62 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -20,7 +20,7 @@ import { stripBasename, warning, } from "@remix-run/router"; -import { useSyncExternalStore as useSyncExternalStoreShim } from "use-sync-external-store/shim"; +import { useSyncExternalStore as useSyncExternalStoreShim } from "./use-sync-external-store-shim"; import { LocationContext, diff --git a/packages/react-router/lib/use-sync-external-store-shim/index.ts b/packages/react-router/lib/use-sync-external-store-shim/index.ts new file mode 100644 index 0000000000..8eaa777d98 --- /dev/null +++ b/packages/react-router/lib/use-sync-external-store-shim/index.ts @@ -0,0 +1,31 @@ +/** + * Inlined into the react-router repo since use-sync-external-store does not + * provide a UMD-compatible package, so we need this to be able to distribute + * UMD react-router bundles + */ + +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import * as React from "react"; + +import { useSyncExternalStore as client } from "./useSyncExternalStoreShimClient"; +import { useSyncExternalStore as server } from "./useSyncExternalStoreShimServer"; + +const canUseDOM: boolean = !!( + typeof window !== "undefined" && + typeof window.document !== "undefined" && + typeof window.document.createElement !== "undefined" +); +const isServerEnvironment = !canUseDOM; +const shim = isServerEnvironment ? server : client; + +export const useSyncExternalStore = + // @ts-expect-error + React.useSyncExternalStore !== undefined ? React.useSyncExternalStore : shim; diff --git a/packages/react-router/lib/use-sync-external-store-shim/useSyncExternalStoreShimClient.ts b/packages/react-router/lib/use-sync-external-store-shim/useSyncExternalStoreShimClient.ts new file mode 100644 index 0000000000..5f58f9bbf6 --- /dev/null +++ b/packages/react-router/lib/use-sync-external-store-shim/useSyncExternalStoreShimClient.ts @@ -0,0 +1,153 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as React from "react"; + +/** + * inlined Object.is polyfill to avoid requiring consumers ship their own + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is + */ +function isPolyfill(x: any, y: any) { + return ( + (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare + ); +} + +const is: (x: any, y: any) => boolean = + typeof Object.is === "function" ? Object.is : isPolyfill; + +// Intentionally not using named imports because Rollup uses dynamic +// dispatch for CommonJS interop named imports. +const { useState, useEffect, useLayoutEffect, useDebugValue } = React; + +let didWarnOld18Alpha = false; +let didWarnUncachedGetSnapshot = false; + +// Disclaimer: This shim breaks many of the rules of React, and only works +// because of a very particular set of implementation details and assumptions +// -- change any one of them and it will break. The most important assumption +// is that updates are always synchronous, because concurrent rendering is +// only available in versions of React that also have a built-in +// useSyncExternalStore API. And we only use this shim when the built-in API +// does not exist. +// +// Do not assume that the clever hacks used by this hook also work in general. +// The point of this shim is to replace the need for hacks by other libraries. +export function useSyncExternalStore( + subscribe: (fn: () => void) => () => void, + getSnapshot: () => T, + // Note: The shim does not use getServerSnapshot, because pre-18 versions of + // React do not expose a way to check if we're hydrating. So users of the shim + // will need to track that themselves and return the correct value + // from `getSnapshot`. + getServerSnapshot?: () => T +): T { + if (__DEV__) { + if (!didWarnOld18Alpha) { + // @ts-expect-error + if (React.startTransition !== undefined) { + didWarnOld18Alpha = true; + console.error( + "You are using an outdated, pre-release alpha of React 18 that " + + "does not support useSyncExternalStore. The " + + "use-sync-external-store shim will not work correctly. Upgrade " + + "to a newer pre-release." + ); + } + } + } + + // Read the current snapshot from the store on every render. Again, this + // breaks the rules of React, and only works here because of specific + // implementation details, most importantly that updates are + // always synchronous. + const value = getSnapshot(); + if (__DEV__) { + if (!didWarnUncachedGetSnapshot) { + const cachedValue = getSnapshot(); + if (!is(value, cachedValue)) { + console.error( + "The result of getSnapshot should be cached to avoid an infinite loop" + ); + didWarnUncachedGetSnapshot = true; + } + } + } + + // Because updates are synchronous, we don't queue them. Instead we force a + // re-render whenever the subscribed state changes by updating an some + // arbitrary useState hook. Then, during render, we call getSnapshot to read + // the current value. + // + // Because we don't actually use the state returned by the useState hook, we + // can save a bit of memory by storing other stuff in that slot. + // + // To implement the early bailout, we need to track some things on a mutable + // object. Usually, we would put that in a useRef hook, but we can stash it in + // our useState hook instead. + // + // To force a re-render, we call forceUpdate({inst}). That works because the + // new object always fails an equality check. + const [{ inst }, forceUpdate] = useState({ inst: { value, getSnapshot } }); + + // Track the latest getSnapshot function with a ref. This needs to be updated + // in the layout phase so we can access it during the tearing check that + // happens on subscribe. + useLayoutEffect(() => { + inst.value = value; + inst.getSnapshot = getSnapshot; + + // Whenever getSnapshot or subscribe changes, we need to check in the + // commit phase if there was an interleaved mutation. In concurrent mode + // this can happen all the time, but even in synchronous mode, an earlier + // effect may have mutated the store. + if (checkIfSnapshotChanged(inst)) { + // Force a re-render. + forceUpdate({ inst }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [subscribe, value, getSnapshot]); + + useEffect(() => { + // Check for changes right before subscribing. Subsequent changes will be + // detected in the subscription handler. + if (checkIfSnapshotChanged(inst)) { + // Force a re-render. + forceUpdate({ inst }); + } + const handleStoreChange = () => { + // TODO: Because there is no cross-renderer API for batching updates, it's + // up to the consumer of this library to wrap their subscription event + // with unstable_batchedUpdates. Should we try to detect when this isn't + // the case and print a warning in development? + + // The store changed. Check if the snapshot changed since the last time we + // read from the store. + if (checkIfSnapshotChanged(inst)) { + // Force a re-render. + forceUpdate({ inst }); + } + }; + // Subscribe to the store and return a clean-up function. + return subscribe(handleStoreChange); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [subscribe]); + + useDebugValue(value); + return value; +} + +function checkIfSnapshotChanged(inst: any) { + const latestGetSnapshot = inst.getSnapshot; + const prevValue = inst.value; + try { + const nextValue = latestGetSnapshot(); + return !is(prevValue, nextValue); + } catch (error) { + return true; + } +} diff --git a/packages/react-router/lib/use-sync-external-store-shim/useSyncExternalStoreShimServer.ts b/packages/react-router/lib/use-sync-external-store-shim/useSyncExternalStoreShimServer.ts new file mode 100644 index 0000000000..9a8050d23d --- /dev/null +++ b/packages/react-router/lib/use-sync-external-store-shim/useSyncExternalStoreShimServer.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export function useSyncExternalStore( + subscribe: (fn: () => void) => () => void, + getSnapshot: () => T, + getServerSnapshot?: () => T +): T { + // Note: The shim does not use getServerSnapshot, because pre-18 versions of + // React do not expose a way to check if we're hydrating. So users of the shim + // will need to track that themselves and return the correct value + // from `getSnapshot`. + return getSnapshot(); +} From 9b2f174bb1bc75fd4f2c01d785305a560475d6cb Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 17 May 2022 17:08:50 -0400 Subject: [PATCH 078/119] chore: clean up some exports and add some jsdocs --- .../__tests__/nav-link-active-test.tsx | 4 +- .../__tests__/DataMemoryRouter-test.tsx | 59 +++++++- packages/react-router/lib/components.tsx | 1 + packages/react-router/lib/hooks.tsx | 37 ++++- packages/router/index.ts | 134 +++++------------- packages/router/router.ts | 24 ++++ packages/router/utils.ts | 38 +++-- 7 files changed, 179 insertions(+), 118 deletions(-) diff --git a/packages/react-router-dom/__tests__/nav-link-active-test.tsx b/packages/react-router-dom/__tests__/nav-link-active-test.tsx index c8e133e21e..ae18b9643b 100644 --- a/packages/react-router-dom/__tests__/nav-link-active-test.tsx +++ b/packages/react-router-dom/__tests__/nav-link-active-test.tsx @@ -451,7 +451,7 @@ describe("NavLink using a data router", () => { to="/bar" style={({ isActive, isPending }) => isPending - ? { textTransform: "underline" } + ? { textTransform: "lowercase" } : isActive ? { textTransform: "uppercase" } : undefined @@ -469,7 +469,7 @@ describe("NavLink using a data router", () => { fireEvent.click(screen.getByText("Link to Bar")); expect(screen.getByText("Link to Bar").style.textTransform).toBe( - "underline" + "lowercase" ); deferred.resolve(); diff --git a/packages/react-router/__tests__/DataMemoryRouter-test.tsx b/packages/react-router/__tests__/DataMemoryRouter-test.tsx index ebbf6cb0a6..963992bcb0 100644 --- a/packages/react-router/__tests__/DataMemoryRouter-test.tsx +++ b/packages/react-router/__tests__/DataMemoryRouter-test.tsx @@ -46,7 +46,11 @@ describe("", () => { it("renders the first route that matches the URL", () => { let { container } = render( - + } + initialEntries={["/"]} + hydrationData={{}} + > Home} /> ); @@ -64,6 +68,7 @@ describe("", () => { // In data routers there is no basename and you should instead use a root route let { container } = render( } initialEntries={["/my/base/path/thing"]} hydrationData={{}} > @@ -87,6 +92,7 @@ describe("", () => { it("renders with hydration data", async () => { let { container } = render( } initialEntries={["/child"]} hydrationData={{ loaderData: { @@ -138,8 +144,8 @@ describe("", () => { let fooDefer = defer(); let { container } = render( } + initialEntries={["/foo"]} > }> fooDefer.promise} element={} /> @@ -184,6 +190,7 @@ describe("", () => { it("renders a null fallbackElement if none is provided", async () => { let fooDefer = defer(); let { container } = render( + // @ts-expect-error }> fooDefer.promise} element={} /> @@ -219,8 +226,8 @@ describe("", () => { let fooDefer = defer(); let { container } = render( } + initialEntries={["/bar"]} > }> fooDefer.promise} element={} /> @@ -253,7 +260,11 @@ describe("", () => { it("handles link navigations", async () => { render( - + } + initialEntries={["/foo"]} + hydrationData={{}} + > }> } /> } /> @@ -291,7 +302,11 @@ describe("", () => { let barDefer = defer(); let { container } = render( - + } + initialEntries={["/foo"]} + hydrationData={{}} + > }> } /> barDefer.promise} element={} /> @@ -383,7 +398,11 @@ describe("", () => { formData.append("test", "value"); let { container } = render( - + } + initialEntries={["/foo"]} + hydrationData={{}} + > }> } /> ", () => { let spy = jest.fn(); render( - + } + initialEntries={["/"]} + hydrationData={{}} + > }> ", () => { path="bar" loader={async () => "BAR LOADER"} element={} + handle={{ key: "value" }} /> @@ -552,6 +576,7 @@ describe("", () => { "useMatches": Array [ Object { "data": undefined, + "handle": undefined, "id": "0", "params": Object {}, "pathname": "/", @@ -570,6 +595,7 @@ describe("", () => { "useMatches": Array [ Object { "data": undefined, + "handle": undefined, "id": "0", "params": Object {}, "pathname": "/", @@ -588,12 +614,16 @@ describe("", () => { "useMatches": Array [ Object { "data": undefined, + "handle": undefined, "id": "0", "params": Object {}, "pathname": "/", }, Object { "data": "BAR LOADER", + "handle": Object { + "key": "value", + }, "id": "0-1", "params": Object {}, "pathname": "/bar", @@ -610,12 +640,16 @@ describe("", () => { "useMatches": Array [ Object { "data": undefined, + "handle": undefined, "id": "0", "params": Object {}, "pathname": "/", }, Object { "data": "BAR LOADER", + "handle": Object { + "key": "value", + }, "id": "0-1", "params": Object {}, "pathname": "/bar", @@ -633,6 +667,7 @@ describe("", () => { let { container } = render( } initialEntries={["/foo"]} hydrationData={{ loaderData: { @@ -732,6 +767,7 @@ describe("", () => { it("renders hydration errors on leaf elements", async () => { let { container } = render( } initialEntries={["/child"]} hydrationData={{ loaderData: { @@ -791,6 +827,7 @@ describe("", () => { it("renders hydration errors on parent elements", async () => { let { container } = render( } initialEntries={["/child"]} hydrationData={{ loaderData: {}, @@ -840,6 +877,7 @@ describe("", () => { let { container } = render( } initialEntries={["/foo"]} hydrationData={{ loaderData: { @@ -980,6 +1018,7 @@ describe("", () => { let { container } = render( } initialEntries={["/foo"]} hydrationData={{ loaderData: { @@ -1076,6 +1115,7 @@ describe("", () => { let { container } = render( } initialEntries={["/foo"]} hydrationData={{ loaderData: { @@ -1188,6 +1228,7 @@ describe("", () => { it("handles render errors in parent errorElement", async () => { let { container } = render( } initialEntries={["/child"]} hydrationData={{ loaderData: {}, @@ -1230,6 +1271,7 @@ describe("", () => { it("handles render errors in child errorElement", async () => { let { container } = render( } initialEntries={["/child"]} hydrationData={{ loaderData: {}, @@ -1281,6 +1323,7 @@ describe("", () => { it("handles render errors in default errorElement", async () => { let { container } = render( } initialEntries={["/child"]} hydrationData={{ loaderData: {}, @@ -1400,6 +1443,7 @@ describe("", () => { let { container } = render(
} initialEntries={["/"]} hydrationData={{ loaderData: {} }} > @@ -1506,6 +1550,7 @@ describe("", () => { let { container } = render(
} initialEntries={["/"]} hydrationData={{ loaderData: {} }} > diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index b9fb2e8c62..f096da5765 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -233,6 +233,7 @@ interface DataRouteProps { action?: RouteObject["action"]; errorElement?: RouteObject["errorElement"]; shouldRevalidate?: RouteObject["shouldRevalidate"]; + handle?: RouteObject["handle"]; } export interface RouteProps extends DataRouteProps { diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index a04ad03399..03ad8786d9 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -21,7 +21,6 @@ import { parsePath, resolveTo, warning, - warningOnce, } from "@remix-run/router"; import { @@ -563,11 +562,19 @@ function useDataRouterState(hookName: DataRouterHook) { return state; } +/** + * Returns the current navigation, defaulting to an "idle" navigation when + * no navigation is in progress + */ export function useNavigation() { let state = useDataRouterState(DataRouterHook.UseNavigation); return state.navigation; } +/** + * Returns a revalidate function for manually triggering revalidation, as well + * as the current state of any manual revalidations + */ export function useRevalidator() { let router = React.useContext(DataRouterContext); invariant(router, `useRevalidator must be used within a DataRouter`); @@ -575,6 +582,10 @@ export function useRevalidator() { return { revalidate: router.revalidate, state: state.revalidation }; } +/** + * Returns the active route matches, useful for accessing loaderData for + * parent/child routes or the route "handle" property + */ export function useMatches() { let { matches, loaderData } = useDataRouterState(DataRouterHook.UseMatches); return React.useMemo( @@ -586,12 +597,16 @@ export function useMatches() { pathname, params, data: loaderData[match.route.id], + handle: match.route.handle, }; }), [matches, loaderData] ); } +/** + * Returns the loader data for the nearest ancestor Route loader + */ export function useLoaderData() { let state = useDataRouterState(DataRouterHook.UseLoaderData); @@ -607,11 +622,17 @@ export function useLoaderData() { return state.loaderData?.[thisRoute.route.id]; } +/** + * Returns the loaderData for the given routeId + */ export function useRouteLoaderData(routeId: string): any { let state = useDataRouterState(DataRouterHook.UseRouteLoaderData); return state.loaderData?.[routeId]; } +/** + * Returns the action data for the nearest ancestor Route action + */ export function useActionData() { let state = useDataRouterState(DataRouterHook.UseRouteError); @@ -621,6 +642,11 @@ export function useActionData() { return Object.values(state?.actionData || {})[0]; } +/** + * Returns the nearest ancestor Route error, which could be a loader/action + * error or a render error. This is intended to be called from your + * errorElement to display a proper error message. + */ export function useRouteError() { let error = React.useContext(RouteErrorContext); let state = useDataRouterState(DataRouterHook.UseRouteError); @@ -642,3 +668,12 @@ export function useRouteError() { // Otherwise look for errors from our data router state return state.errors?.[thisRoute.route.id]; } + +const alreadyWarned: Record = {}; + +function warningOnce(key: string, cond: boolean, message: string) { + if (!cond && !alreadyWarned[key]) { + alreadyWarned[key] = true; + warning(false, message); + } +} diff --git a/packages/router/index.ts b/packages/router/index.ts index 05e24ed6ee..0f6253b31c 100644 --- a/packages/router/index.ts +++ b/packages/router/index.ts @@ -1,157 +1,91 @@ import type { - BrowserHistory, BrowserHistoryOptions, - HashHistory, HashHistoryOptions, - History, - InitialEntry, - Location, - MemoryHistory, MemoryHistoryOptions, - Path, - To, } from "./history"; import { - Action, createBrowserHistory, createHashHistory, createMemoryHistory, - createPath, - parsePath, } from "./history"; -import type { - DataRouteMatch, - Fetcher, - GetScrollRestorationKeyFunction, - HydrationState, - NavigateOptions, - Navigation, - Router, - RouterState, - RouteData, - RouterInit, -} from "./router"; -import { IDLE_NAVIGATION, createRouter } from "./router"; -import type { - ActionFunction, - DataRouteObject, - FormEncType, - FormMethod, - LoaderFunction, - ParamParseKey, - Params, - PathMatch, - PathPattern, - RouteMatch, - RouteObject, - ShouldRevalidateFunction, - Submission, -} from "./utils"; -import { - generatePath, - getToPathname, - invariant, - joinPaths, - matchPath, - matchRoutes, - normalizePathname, - normalizeSearch, - normalizeHash, - resolvePath, - resolveTo, - stripBasename, - warning, - warningOnce, -} from "./utils"; +import type { Router, RouterInit } from "./router"; +import { createRouter } from "./router"; -type MemoryRouterInit = MemoryHistoryOptions & Omit; function createMemoryRouter({ initialEntries, initialIndex, ...routerInit -}: MemoryRouterInit): Router { +}: MemoryHistoryOptions & Omit): Router { let history = createMemoryHistory({ initialEntries, initialIndex }); return createRouter({ history, ...routerInit }); } -type BrowserRouterInit = BrowserHistoryOptions & Omit; function createBrowserRouter({ window, ...routerInit -}: BrowserRouterInit): Router { +}: BrowserHistoryOptions & Omit): Router { let history = createBrowserHistory({ window }); return createRouter({ history, ...routerInit }); } -type HashRouterInit = HashHistoryOptions & Omit; -function createHashRouter({ window, ...routerInit }: HashRouterInit): Router { +function createHashRouter({ + window, + ...routerInit +}: HashHistoryOptions & Omit): Router { let history = createHashHistory({ window }); return createRouter({ history, ...routerInit }); } -// @remix-run/router public Type API +export * from "./router"; + export type { ActionFunction, - BrowserHistory, - BrowserRouterInit, - DataRouteMatch, DataRouteObject, - Fetcher, FormEncType, FormMethod, - GetScrollRestorationKeyFunction, - HashHistory, - HashRouterInit, - History, - HydrationState, - InitialEntry, LoaderFunction, - Location, - MemoryHistory, - MemoryRouterInit, - NavigateOptions, ParamParseKey, Params, - Path, PathMatch, PathPattern, - RouteData, RouteMatch, RouteObject, - Router, - RouterInit, - RouterState, ShouldRevalidateFunction, Submission, - To, - Navigation, -}; +} from "./utils"; -// @remix-run/router public API export { - Action, - IDLE_NAVIGATION, - createBrowserHistory, - createBrowserRouter, - createHashHistory, - createHashRouter, - createMemoryRouter, - createMemoryHistory, - createPath, - createRouter, generatePath, getToPathname, invariant, joinPaths, matchPath, matchRoutes, - normalizeHash, normalizePathname, - normalizeSearch, - parsePath, resolvePath, resolveTo, stripBasename, warning, - warningOnce, -}; +} from "./utils"; + +export type { + BrowserHistory, + HashHistory, + History, + InitialEntry, + Location, + MemoryHistory, + Path, + To, +} from "./history"; + +export { + Action, + createBrowserHistory, + createPath, + createHashHistory, + createMemoryHistory, + parsePath, +} from "./history"; + +export { createBrowserRouter, createHashRouter, createMemoryRouter }; diff --git a/packages/router/router.ts b/packages/router/router.ts index c4142e69d2..2a7b2625d8 100644 --- a/packages/router/router.ts +++ b/packages/router/router.ts @@ -327,6 +327,9 @@ export type Navigation = NavigationStates[keyof NavigationStates]; export type RevalidationState = "idle" | "loading"; +/** + * Potential states for fetchers + */ type FetcherStates = { Idle: { state: "idle"; @@ -595,6 +598,9 @@ export function createRouter(init: RouterInit): Router { // Most recent href/match for fetcher.load calls for fetchers let fetchLoadMatches = new Map(); + // Initialize the router, all side effects should be kicked off from here. + // Implemented as a Fluent API for ease of: + // let router = createRouter(init).initialize(); function initialize() { // If history informs us of a POP navigation, start the navigation but do not update // state. We'll update our own state once the navigation completes @@ -611,6 +617,7 @@ export function createRouter(init: RouterInit): Router { return router; } + // Clean up a router and it's side effects function dispose() { if (unlistenHistory) { unlistenHistory(); @@ -622,6 +629,7 @@ export function createRouter(init: RouterInit): Router { } } + // Subscribe to state updates for the router function subscribe(fn: RouterSubscriber) { if (subscriber) { throw new Error("A router only accepts one active subscriber"); @@ -690,6 +698,8 @@ export function createRouter(init: RouterInit): Router { isRevalidationRequired = false; } + // Trigger a navigation event, which can either be a numerical POP or a PUSH + // replace with an optional submission async function navigate( path: number | To, opts?: NavigateOptions @@ -718,6 +728,9 @@ export function createRouter(init: RouterInit): Router { return await startNavigation(historyAction, location); } + // Revalidate all current loaders. If a navigation is in progress or if this + // is interrupted by a navigation, allow this to "succeed" by calling all + // loaders during the next loader round async function revalidate(): Promise { let { state: navigationState, type } = state.navigation; @@ -844,6 +857,8 @@ export function createRouter(init: RouterInit): Router { }); } + // Call the action matched by the leaf route for this navigation and handle + // redirects/errors async function handleAction( historyAction: HistoryAction, location: Location, @@ -935,6 +950,8 @@ export function createRouter(init: RouterInit): Router { }; } + // Call all applicable loaders for the given matches, handling redirects, + // errors, etc. async function handleLoaders( historyAction: HistoryAction, location: Location, @@ -1083,6 +1100,7 @@ export function createRouter(init: RouterInit): Router { return state.fetchers.get(key) || IDLE_FETCHER; } + // Trigger a fetcher load/submit for the given fetcher key async function fetch(key: string, href: string, opts?: NavigateOptions) { if (typeof AbortController === "undefined") { throw new Error( @@ -1137,6 +1155,8 @@ export function createRouter(init: RouterInit): Router { } } + // Call the action for the matched fetcher.submit(), and then handle redirects, + // errors, and revalidation async function handleFetcherAction( key: string, href: string, @@ -1339,6 +1359,7 @@ export function createRouter(init: RouterInit): Router { } } + // Call the matched loader for fetcher.load(), handling redirects, errors, etc. async function handleFetcherLoader( key: string, href: string, @@ -1401,6 +1422,7 @@ export function createRouter(init: RouterInit): Router { updateState({ fetchers: new Map(state.fetchers) }); } + // Utility function to handle redirects returned from an action or loader async function startRedirectNavigation( redirect: RedirectResult, navigation: Navigation @@ -1478,6 +1500,8 @@ export function createRouter(init: RouterInit): Router { return yeetedKeys.length > 0; } + // Opt in to capturing and reporting scroll positions during navigations, + // used by the component function enableScrollRestoration( positions: Record, getPosition: GetScrollPositionFunction, diff --git a/packages/router/utils.ts b/packages/router/utils.ts index cf0cbb3383..4b35f04b19 100644 --- a/packages/router/utils.ts +++ b/packages/router/utils.ts @@ -17,6 +17,9 @@ export interface Submission { formData: FormData; } +/** + * Narrowed type enforcing a non-GET method + */ export interface ActionSubmission extends Submission { formMethod: ActionFormMethod; } @@ -557,6 +560,9 @@ function safelyDecodeURIComponent(value: string, paramName: string) { } } +/** + * @private + */ export function stripBasename( pathname: string, basename: string @@ -576,6 +582,9 @@ export function stripBasename( return pathname.slice(basename.length) || "/"; } +/** + * @private + */ export function invariant(value: boolean, message?: string): asserts value; export function invariant( value: T | null | undefined, @@ -587,6 +596,9 @@ export function invariant(value: any, message?: string) { } } +/** + * @private + */ export function warning(cond: any, message: string): void { if (!cond) { // eslint-disable-next-line no-console @@ -604,14 +616,6 @@ export function warning(cond: any, message: string): void { } } -const alreadyWarned: Record = {}; -export function warningOnce(key: string, cond: boolean, message: string) { - if (!cond && !alreadyWarned[key]) { - alreadyWarned[key] = true; - warning(false, message); - } -} - /** * Returns a resolved path object relative to the given pathname. * @@ -653,6 +657,9 @@ function resolvePathname(relativePath: string, fromPathname: string): string { return segments.length > 1 ? segments.join("/") : "/"; } +/** + * @private + */ export function resolveTo( toArg: To, routePathnames: string[], @@ -708,6 +715,9 @@ export function resolveTo( return path; } +/** + * @private + */ export function getToPathname(to: To): string | undefined { // Empty strings should be treated the same as / paths return to === "" || (to as Path).pathname === "" @@ -717,12 +727,21 @@ export function getToPathname(to: To): string | undefined { : to.pathname; } +/** + * @private + */ export const joinPaths = (paths: string[]): string => paths.join("/").replace(/\/\/+/g, "/"); +/** + * @private + */ export const normalizePathname = (pathname: string): string => pathname.replace(/\/+$/, "").replace(/^\/*/, "/"); +/** + * @private + */ export const normalizeSearch = (search: string): string => !search || search === "?" ? "" @@ -730,5 +749,8 @@ export const normalizeSearch = (search: string): string => ? search : "?" + search; +/** + * @private + */ export const normalizeHash = (hash: string): string => !hash || hash === "#" ? "" : hash.startsWith("#") ? hash : "#" + hash; From 9f40c3381bc55d71cfcbd1dc6205b24f82781387 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 19 May 2022 11:15:05 -0400 Subject: [PATCH 079/119] feat: add DataStaticRouter for SSR --- .../__tests__/data-static-router-test.tsx | 194 ++++++++++++++++++ .../__tests__/static-location-test.tsx | 8 +- packages/react-router-dom/index.tsx | 8 +- packages/react-router-dom/server.tsx | 93 +++++++-- packages/react-router-native/index.tsx | 2 +- .../__tests__/DataMemoryRouter-test.tsx | 6 +- packages/react-router/index.ts | 2 +- packages/react-router/lib/components.tsx | 3 + 8 files changed, 289 insertions(+), 27 deletions(-) create mode 100644 packages/react-router-dom/__tests__/data-static-router-test.tsx diff --git a/packages/react-router-dom/__tests__/data-static-router-test.tsx b/packages/react-router-dom/__tests__/data-static-router-test.tsx new file mode 100644 index 0000000000..c36809c2fc --- /dev/null +++ b/packages/react-router-dom/__tests__/data-static-router-test.tsx @@ -0,0 +1,194 @@ +import * as React from "react"; +import * as ReactDOMServer from "react-dom/server"; +import { Route, useLocation } from "react-router-dom"; +import { DataStaticRouter } from "react-router-dom/server"; +import { Outlet } from "react-router/lib/components"; +import { useLoaderData, useMatches } from "react-router/lib/hooks"; + +beforeEach(() => { + jest.spyOn(console, "warn").mockImplementation(() => {}); +}); + +describe("A ", () => { + it("renders with provided hydration data", () => { + let loaderSpy = jest.fn(); + + let hooksData1: { + location: ReturnType; + loaderData: ReturnType; + matches: ReturnType; + }; + let hooksData2: { + location: ReturnType; + loaderData: ReturnType; + matches: ReturnType; + }; + + function HooksChecker1() { + hooksData1 = { + location: useLocation(), + loaderData: useLoaderData(), + matches: useMatches(), + }; + return ; + } + + function HooksChecker2() { + hooksData2 = { + location: useLocation(), + loaderData: useLoaderData(), + matches: useMatches(), + }; + return null; + } + + ReactDOMServer.renderToStaticMarkup( + + } + loader={loaderSpy} + handle="1" + > + } + loader={loaderSpy} + handle="2" + /> + + + ); + + expect(loaderSpy).not.toHaveBeenCalled(); + + expect(hooksData1.location).toEqual({ + pathname: "/the/path", + search: "?the=query", + hash: "#the-hash", + state: null, + key: expect.any(String), + }); + expect(hooksData1.loaderData).toEqual({ + key1: "value1", + }); + expect(hooksData1.matches).toEqual([ + { + data: { + key1: "value1", + }, + handle: "1", + id: "0", + params: {}, + pathname: "/the", + }, + { + data: { + key2: "value2", + }, + handle: "2", + id: "0-0", + params: {}, + pathname: "/the/path", + }, + ]); + + expect(hooksData2.location).toEqual({ + pathname: "/the/path", + search: "?the=query", + hash: "#the-hash", + state: null, + key: expect.any(String), + }); + expect(hooksData2.loaderData).toEqual({ + key2: "value2", + }); + expect(hooksData2.matches).toEqual([ + { + data: { + key1: "value1", + }, + handle: "1", + id: "0", + params: {}, + pathname: "/the", + }, + { + data: { + key2: "value2", + }, + handle: "2", + id: "0-0", + params: {}, + pathname: "/the/path", + }, + ]); + }); + + it("defaults to the root location", () => { + let loaderSpy = jest.fn(); + + let markup = ReactDOMServer.renderToStaticMarkup( + // @ts-expect-error + + index} loader={loaderSpy} /> + } loader={loaderSpy}> + ๐Ÿ‘ถ} loader={loaderSpy} /> + + + ); + expect(markup).toBe("

index

"); + }); + + it("throws an error if no data is provided", () => { + let loaderSpy = jest.fn(); + + expect(() => + ReactDOMServer.renderToStaticMarkup( + // @ts-expect-error + + } loader={loaderSpy}> + ๐Ÿ‘ถ} loader={loaderSpy} /> + + + ) + ).toThrow("You must provide a complete `data` prop for "); + }); + + it("throws an error if partial data is provided", () => { + let loaderSpy = jest.fn(); + + expect(() => + ReactDOMServer.renderToStaticMarkup( + + } loader={loaderSpy}> + ๐Ÿ‘ถ} loader={loaderSpy} /> + + + ) + ).toThrow("You must provide a complete `data` prop for "); + }); +}); diff --git a/packages/react-router-dom/__tests__/static-location-test.tsx b/packages/react-router-dom/__tests__/static-location-test.tsx index d8186e4a49..102e35da6e 100644 --- a/packages/react-router-dom/__tests__/static-location-test.tsx +++ b/packages/react-router-dom/__tests__/static-location-test.tsx @@ -20,11 +20,11 @@ describe("A ", () => { ); - expect(location).toMatchObject({ + expect(location).toEqual({ pathname: "/the/path", search: "?the=query", hash: "#the-hash", - state: {}, + state: null, key: expect.any(String), }); }); @@ -48,11 +48,11 @@ describe("A ", () => { ); - expect(location).toMatchObject({ + expect(location).toEqual({ pathname: "/the/path", search: "?the=query", hash: "", - state: {}, + state: null, key: expect.any(String), }); }); diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index 8afc38bf8f..fe4e366776 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -10,8 +10,8 @@ import { useLocation, useMatch, useNavigate, + useRenderDataRouter, useResolvedPath, - UNSAFE_useRenderDataRouter, UNSAFE_RouteContext, UNSAFE_DataRouterContext, UNSAFE_DataRouterStateContext, @@ -148,7 +148,7 @@ export { UNSAFE_RouteContext, UNSAFE_DataRouterContext, UNSAFE_DataRouterStateContext, - UNSAFE_useRenderDataRouter, + useRenderDataRouter, } from "react-router"; //#endregion @@ -169,7 +169,7 @@ export function DataBrowserRouter({ hydrationData, window, }: DataBrowserRouterProps): React.ReactElement { - return UNSAFE_useRenderDataRouter({ + return useRenderDataRouter({ children, fallbackElement, createRouter: (routes: RouteObject[]) => @@ -194,7 +194,7 @@ export function DataHashRouter({ fallbackElement, window, }: DataBrowserRouterProps): React.ReactElement { - return UNSAFE_useRenderDataRouter({ + return useRenderDataRouter({ children, fallbackElement, createRouter: (routes: RouteObject[]) => diff --git a/packages/react-router-dom/server.tsx b/packages/react-router-dom/server.tsx index 9f8f5f01ec..c8a312b378 100644 --- a/packages/react-router-dom/server.tsx +++ b/packages/react-router-dom/server.tsx @@ -1,6 +1,22 @@ import * as React from "react"; -import { Action } from "@remix-run/router"; -import { Location, To, createPath, parsePath, Router } from "react-router-dom"; +import type { RouterInit } from "@remix-run/router"; +import { + Action, + createMemoryHistory, + createRouter, + invariant, +} from "@remix-run/router"; +import { + Location, + To, + createPath, + parsePath, + Router, + createRoutesFromChildren, + Routes, + UNSAFE_DataRouterContext as DataRouterContext, + UNSAFE_DataRouterStateContext as DataRouterStateContext, +} from "react-router-dom"; export interface StaticRouterProps { basename?: string; @@ -30,7 +46,67 @@ export function StaticRouter({ key: locationProp.key || "default", }; - let staticNavigator = { + let staticNavigator = getStatelessNavigator(); + return ( + + ); +} + +export interface DataStaticRouterProps { + data: RouterInit["hydrationData"]; + location: Partial | string; + children?: React.ReactNode; +} + +/** + * A Data Router that may not navigate to any other location. This is useful + * on the server where there is no stateful UI. + */ +export function DataStaticRouter({ + data, + location = "/", + children, +}: DataStaticRouterProps) { + // Create a router but do not call initialize() so it has no side effects + // and performs no data fetching + let staticRouter = createRouter({ + history: createMemoryHistory({ initialEntries: [location] }), + routes: createRoutesFromChildren(children), + hydrationData: data, + }); + + invariant( + staticRouter.state.initialized, + "You must provide a complete `data` prop for " + ); + + let staticNavigator = getStatelessNavigator(); + + return ( + + + + + + + + ); +} + +function getStatelessNavigator() { + return { createHref(to: To) { return typeof to === "string" ? to : createPath(to); }, @@ -69,15 +145,4 @@ export function StaticRouter({ ); }, }; - - return ( - - ); } diff --git a/packages/react-router-native/index.tsx b/packages/react-router-native/index.tsx index 840828f169..94c380dc77 100644 --- a/packages/react-router-native/index.tsx +++ b/packages/react-router-native/index.tsx @@ -111,7 +111,7 @@ export { UNSAFE_RouteContext, UNSAFE_DataRouterContext, UNSAFE_DataRouterStateContext, - UNSAFE_useRenderDataRouter, + useRenderDataRouter, } from "react-router"; //////////////////////////////////////////////////////////////////////////////// diff --git a/packages/react-router/__tests__/DataMemoryRouter-test.tsx b/packages/react-router/__tests__/DataMemoryRouter-test.tsx index 963992bcb0..425658e8b6 100644 --- a/packages/react-router/__tests__/DataMemoryRouter-test.tsx +++ b/packages/react-router/__tests__/DataMemoryRouter-test.tsx @@ -22,11 +22,11 @@ import { useRouteLoaderData, useRouteError, useNavigation, + useRenderDataRouter, useRevalidator, UNSAFE_DataRouterContext, MemoryRouter, Routes, - UNSAFE_useRenderDataRouter, } from "../index"; import { _resetModuleScope } from "../lib/components"; @@ -1425,7 +1425,7 @@ describe("", () => { hydrationData, fallbackElement, }: DataMemoryRouterProps): React.ReactElement { - return UNSAFE_useRenderDataRouter({ + return useRenderDataRouter({ children, fallbackElement, createRouter: (routes) => { @@ -1532,7 +1532,7 @@ describe("", () => { hydrationData, fallbackElement, }: DataMemoryRouterProps): React.ReactElement { - return UNSAFE_useRenderDataRouter({ + return useRenderDataRouter({ children, fallbackElement, createRouter: (routes) => { diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts index 3bfe384b5b..06b1dc37b6 100644 --- a/packages/react-router/index.ts +++ b/packages/react-router/index.ts @@ -173,5 +173,5 @@ export { RouteContext as UNSAFE_RouteContext, DataRouterContext as UNSAFE_DataRouterContext, DataRouterStateContext as UNSAFE_DataRouterStateContext, - useRenderDataRouter as UNSAFE_useRenderDataRouter, + useRenderDataRouter, }; diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index f096da5765..d6b661a4fb 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -52,6 +52,9 @@ export function _resetModuleScope() { routerSingleton = null; } +/** + * @private + */ export function useRenderDataRouter({ children, fallbackElement, From b13865e706beca06ff9a267bc543ed2620d27976 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 19 May 2022 12:39:47 -0400 Subject: [PATCH 080/119] chore: fix link --- packages/router/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/router/utils.ts b/packages/router/utils.ts index 4b35f04b19..e5947f5f7e 100644 --- a/packages/router/utils.ts +++ b/packages/router/utils.ts @@ -380,7 +380,7 @@ function matchRouteBranch< /** * Returns a path with params interpolated. * - * @see https://reactrouter.com/docs/en/v6/api#generatepath + * @see https://reactrouter.com/docs/en/v6/utils/generate-path */ export function generatePath(path: string, params: Params = {}): string { return path From 8c7b2bf2a3c42d1b332573ae2dd1961d3e7da193 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 19 May 2022 13:42:28 -0400 Subject: [PATCH 081/119] fix: default replace=false only for GET submissions --- .../__tests__/DataBrowserRouter-test.tsx | 310 ++++++++++++++++++ packages/react-router-dom/index.tsx | 7 +- 2 files changed, 315 insertions(+), 2 deletions(-) diff --git a/packages/react-router-dom/__tests__/DataBrowserRouter-test.tsx b/packages/react-router-dom/__tests__/DataBrowserRouter-test.tsx index fe058239b4..325f1ec7d8 100644 --- a/packages/react-router-dom/__tests__/DataBrowserRouter-test.tsx +++ b/packages/react-router-dom/__tests__/DataBrowserRouter-test.tsx @@ -29,6 +29,7 @@ import { useFetchers, } from "../index"; import { _resetModuleScope } from "react-router/lib/components"; +import { useNavigate } from "react-router/lib/hooks"; testDomRouter("", DataBrowserRouter, (url) => getWindowImpl(url, false) @@ -656,6 +657,315 @@ function testDomRouter(name, TestDataRouter, getWindow) { `); }); + it('defaults
to be a PUSH navigation', async () => { + let { container } = render( + + }> + "index"} element={

index

} /> + "1"} element={

Page 1

} /> + "2"} element={

Page 2

} /> +
+
+ ); + + function Layout() { + let navigate = useNavigate(); + return ( + <> + + + +
+ +
+ +
+ + ); + } + + expect(getHtml(container.querySelector(".output"))) + .toMatchInlineSnapshot(` + "
+

+ index +

+
" + `); + + fireEvent.click(screen.getByText("Submit Form")); + await waitFor(() => screen.getByText("Page 1")); + expect(getHtml(container.querySelector(".output"))) + .toMatchInlineSnapshot(` + "
+

+ Page 1 +

+
" + `); + + fireEvent.click(screen.getByText("Go back")); + await waitFor(() => screen.getByText("index")); + expect(getHtml(container.querySelector(".output"))) + .toMatchInlineSnapshot(` + "
+

+ index +

+
" + `); + }); + + it('defaults
to be a REPLACE navigation', async () => { + let { container } = render( + + }> + "index"} element={

index

} /> + "1"} element={

Page 1

} /> + "action"} + loader={() => "2"} + element={

Page 2

} + /> +
+
+ ); + + function Layout() { + let navigate = useNavigate(); + return ( + <> + Go to 1 + + + +
+ +
+ +
+ + ); + } + + expect(getHtml(container.querySelector(".output"))) + .toMatchInlineSnapshot(` + "
+

+ index +

+
" + `); + + fireEvent.click(screen.getByText("Go to 1")); + await waitFor(() => screen.getByText("Page 1")); + expect(getHtml(container.querySelector(".output"))) + .toMatchInlineSnapshot(` + "
+

+ Page 1 +

+
" + `); + + fireEvent.click(screen.getByText("Submit Form")); + await waitFor(() => screen.getByText("Page 2")); + expect(getHtml(container.querySelector(".output"))) + .toMatchInlineSnapshot(` + "
+

+ Page 2 +

+
" + `); + + fireEvent.click(screen.getByText("Go back")); + await waitFor(() => screen.getByText("index")); + expect(getHtml(container.querySelector(".output"))) + .toMatchInlineSnapshot(` + "
+

+ index +

+
" + `); + }); + + it('defaults useSubmit({ method: "get" }) to be a PUSH navigation', async () => { + let { container } = render( + + }> + "index"} element={

index

} /> + "1"} element={

Page 1

} /> + "2"} element={

Page 2

} /> +
+
+ ); + + function Layout() { + let navigate = useNavigate(); + let submit = useSubmit(); + let formData = new FormData(); + formData.append("test", "value"); + return ( + <> + + +
+ +
+ + ); + } + + expect(getHtml(container.querySelector(".output"))) + .toMatchInlineSnapshot(` + "
+

+ index +

+
" + `); + + fireEvent.click(screen.getByText("Submit")); + await waitFor(() => screen.getByText("Page 1")); + expect(getHtml(container.querySelector(".output"))) + .toMatchInlineSnapshot(` + "
+

+ Page 1 +

+
" + `); + + fireEvent.click(screen.getByText("Go back")); + await waitFor(() => screen.getByText("index")); + expect(getHtml(container.querySelector(".output"))) + .toMatchInlineSnapshot(` + "
+

+ index +

+
" + `); + }); + + it('defaults useSubmit({ method: "post" }) to be a REPLACE navigation', async () => { + let { container } = render( + + }> + "index"} element={

index

} /> + "1"} element={

Page 1

} /> + "action"} + loader={() => "2"} + element={

Page 2

} + /> +
+
+ ); + + function Layout() { + let navigate = useNavigate(); + let submit = useSubmit(); + let formData = new FormData(); + formData.append("test", "value"); + return ( + <> + Go to 1 + + +
+ +
+ + ); + } + + expect(getHtml(container.querySelector(".output"))) + .toMatchInlineSnapshot(` + "
+

+ index +

+
" + `); + + fireEvent.click(screen.getByText("Go to 1")); + await waitFor(() => screen.getByText("Page 1")); + expect(getHtml(container.querySelector(".output"))) + .toMatchInlineSnapshot(` + "
+

+ Page 1 +

+
" + `); + + fireEvent.click(screen.getByText("Submit")); + await waitFor(() => screen.getByText("Page 2")); + expect(getHtml(container.querySelector(".output"))) + .toMatchInlineSnapshot(` + "
+

+ Page 2 +

+
" + `); + + fireEvent.click(screen.getByText("Go back")); + await waitFor(() => screen.getByText("index")); + expect(getHtml(container.querySelector(".output"))) + .toMatchInlineSnapshot(` + "
+

+ index +

+
" + `); + }); + describe("useFetcher(s)", () => { it("handles fetcher.load and fetcher.submit", async () => { let count = 0; diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index fe4e366776..7db6a10d60 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -541,7 +541,7 @@ interface FormImplProps extends FormProps { const FormImpl = React.forwardRef( ( { - replace = false, + replace, method = defaultMethod, action = ".", encType = defaultEncType, @@ -764,7 +764,10 @@ function useSubmitImpl(fetcherKey?: string): SubmitFunction { let href = url.pathname + url.search; let opts = { - replace: options.replace, + // If replace is not specified, we'll default to false for GET and + // true otherwise + replace: + options.replace != null ? options.replace === true : method !== "get", formData, formMethod: method as FormMethod, formEncType: encType as FormEncType, From a895bcab240a43486e12045f922ee41d750cde5e Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 19 May 2022 14:32:40 -0400 Subject: [PATCH 082/119] chore: update scroll restoration example to include data loading --- examples/scroll-restoration/src/App.tsx | 71 +++++++++++++++++++------ 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/examples/scroll-restoration/src/App.tsx b/examples/scroll-restoration/src/App.tsx index e7ae5e0cca..bbc1382283 100644 --- a/examples/scroll-restoration/src/App.tsx +++ b/examples/scroll-restoration/src/App.tsx @@ -2,14 +2,18 @@ import React from "react"; import type { DataRouteMatch, Location } from "react-router-dom"; import { DataBrowserRouter, - ScrollRestoration, - useLocation, Link, - Route, Outlet, + Route, + ScrollRestoration, + useLoaderData, + useLocation, + useNavigation, } from "react-router-dom"; function Layout() { + let navigation = useNavigation(); + // You can provide a custom implementation of what "key" should be used to // cache scroll positions for a given location. Using the location.key will // provide standard browser behavior and only restore on back/forward @@ -48,32 +52,48 @@ function Layout() { .navitem { margin: 1rem 0; } + + .spinner { + position: fixed; + top: 0; + right: 0; + padding: 5px; + background-color: lightgreen; + } `} +
+ Navigating... +
" `); - // Resolve Comp2 loader and complete navigation - Comp2 fetcher is still + // Resolve Comp2 loader and complete navigation - Comp1 fetcher is still // reflected here since deleteFetcher doesn't updateState // TODO: Is this expected? - // TODO: Should getFetcher reflect the Comp2 idle fetcher in useFetchers? dfd2.resolve("data 2"); await waitFor(() => screen.getByText(/2.*idle/)); expect(getHtml(container.querySelector("#output"))) @@ -1660,7 +1631,7 @@ function testDomRouter(name, TestDataRouter, getWindow) { id=\\"output\\" >

- [\\"idle-done\\"] + [\\"idle\\"]

2 @@ -1682,7 +1653,7 @@ function testDomRouter(name, TestDataRouter, getWindow) { id=\\"output\\" >

- [\\"loading-normalLoad\\"] + [\\"loading\\"]

2 @@ -1703,7 +1674,7 @@ function testDomRouter(name, TestDataRouter, getWindow) { id=\\"output\\" >

- [\\"idle-done\\"] + [\\"idle\\"]

2 @@ -1743,12 +1714,11 @@ function testDomRouter(name, TestDataRouter, getWindow) { ); function Comp() { - let fetcher = useFetcher({ revalidate: true }); + let fetcher = useFetcher(); return ( <>

{fetcher.state} - {fetcher.type} {fetcher.data ? JSON.stringify(fetcher.data) : null}

+ + ); +} +``` + +Make sure your inputs have names or else the `FormData` will not include that field's value. + +All of this will trigger state updates to any rendered [`useNavigation`][usenavigation] hooks so you can build pending indicators and optimistic UI while the async operations are in-flight. + +If the form doesn't _feel_ like navigation, you probably want [`useFetcher`][usefetcher]. + +## `action` + +The url to which the form will be submitted, just like [HTML form action][htmlformaction]. The only difference is the default action. With HTML forms, it defaults to the full URL. With `
`, it defaults to the relative URL of the closest route in context. + +Consider the following routes and components: + +```jsx +function ProjectsLayout() { + return ( + <> + + + + ); +} + +function ProjectsPage() { + return ; +} + + + } + action={ProjectsLayout.action} + > + } + action={ProjectsPage.action} + /> + +; +``` + +If the the current URL is `"/projects/123"`, the form inside the child +route, `ProjectsPage`, will have a default action as you might expect: `"/projects/123"`. In this case, where the route is the deepest matching route, both `` and plain HTML forms have the same result. + +But the form inside of `ProjectsLayout` will point to `"/projects"`, not the full URL. In other words, it points to the matching segment of the URL for the route in which the form is rendered. + +This helps with portability as well as co-location of forms and their action handlers when if you add some convention around your route modules. + +If you need to post to a different route, then add an action prop: + +```tsx + +``` + +**See also:** + +- [Index Search Param][indexsearchparam] (index vs parent route disambiguation) + +## `method` + +This determines the [HTTP verb](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) to be used. The same as plain HTML [form method][htmlform-method], except it also supports "put", "patch", and "delete" in addition to "get" and "post". The default is "get". + +### GET submissions + +The default method is "get". Get submissions _will not call an action_. Get submissions are the same as a normal navigation (user clicks a link) except the user gets to supply the search params that go to the URL from the form. + +```tsx + + + + +``` + +Let's say the user types in "running shoes" and submits the form. React Router emulates the browser and will serialize the form into [URLSearchParams][urlsearchparams] and then navigate the user to `"/products?q=running+shoes"`. It's as if you rendered a `` as the developer, but instead you let the user supply the query string dynamically. + +Your route loader can access these values most conveniently by creating a new [`URL`][url] from the `request.url` and then load the data. + +```tsx + { + let url = new URL(request.url); + let searchTerm = url.searchParams.get("q"); + return fakeSearchProducts(searchTerm); + }} +/> +``` + +### Mutation Submissions + +All other methods are "mutation submissions", meaning you intend to change something about your data with POST, PUT, PATCH, or DELETE. Note that plain HTML forms only support "post" and "get", we tend to stick to those two as well. + +When the user submits the form, React Router will match the `action` to the app's routes and call the `` with the serialized [`FormData`][formdata]. When the action completes, all of the loader data on the page will automatically revalidate to keep your UI in sync with your data. + +The method will be available on [`request.method`][requestmethod] inside the route action that is called. You can use this to instruct your data abstractions about the intent of the submission. + +```tsx +} + loader={async ({ params }) => { + return fakeLoadProject(params.id) + }} + action={async ({ request, params }) => { + switch (request.method) { + case "put": { + let formData = await request.formData(); + let name = formData.get("projectName"); + return fakeUpdateProject(name); + } + case "delete": { + return fakeDeleteProject(params.id); + } + default { + throw new Response("", { status: 405 }) + } + } + }} +/>; + +function Project() { + let project = useLoaderData(); + + return ( + <> +
+ + +
+ +
+ +
+ + ); +} +``` + +As you can see, both forms submit to the same route but you can use the `request.method` to branch on what you intend to do. After the actions completes, the `loader` will be revalidated and the UI will automatically synchronize with the new data. + +## `replace` + +Instructs the form to replace the current entry in the history stack, instead of pushing the new entry. + +```tsx +
+``` + +The default behavior is conditional on the form `method`: + +- `get` defaults to `false` +- every other method defaults to `true` + +We've found with `get` you often want the user to be able to click "back" to see the previous search results/filters, etc. But with the other methods the default is `true` to avoid the "are you sure you want to resubmit the form?" prompt. Note that even if `replace={false}` React Router _will not_ resubmit the form when the back button is clicked and the method is post, put, patch, or delete. + +In other words, this is really only useful for GET submissions and you want to avoid the back button showing the previous results. + +## `reloadDocument` + +Instructs the form to skip React Router and submit the form with the browser's built in behavior. + +```tsx + +``` + +This is recommended over `` so you can get the benefits of default and relative `action`, but otherwise is the same as a plain HTML form. + +Without a framework like [Remix][remix], or your own server handling of posts to routes, this isn't very useful. + +See also: + +- [`useTransition`][usetransition] +- [`useActionData`][useactiondata] +- [`useSubmit`][usesubmit] + +# Examples + +TODO: More examples + +## Large List Filtering + +A common use case for GET submissions is filtering a large list, like ecommerce and travel booking sites. + +```tsx +function FilterForm() { + return ( + + + +
+ Star Rating + + + + + +
+ +
+ Amenities + + +
+ +
+ ); +} +``` + +When the user submits this form, the form will be serialized to the URL with something like this, depending on the user's selections: + +``` +/slc/hotels?sort=price&stars=4&amenities=pool&amenities=exercise +``` + +You can access those values from the `request.url` + +```tsx + { + let url = new URL(request.url); + let sort = url.searchParams.get("sort"); + let stars = url.searchParams.get("stars"); + let amenities = url.searchParams.getAll("amenities"); + return fakeGetHotels({ sort, stars, amenities }); + }} +/> +``` + +**See also:** + +- [useSubmit][usesubmit] + +[usenavigation]: ../hooks/use-navigation +[formdata]: https://developer.mozilla.org/en-US/docs/Web/API/FormData +[usefetcher]: ../hooks/use-fetcher +[htmlform]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form +[htmlformaction]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-action +[htmlform-method]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-method +[urlsearchparams]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams +[url]: https://developer.mozilla.org/en-US/docs/Web/API/URL +[usesubmit]: ../hooks/use-submit +[requestmethod]: https://developer.mozilla.org/en-US/docs/Web/API/Request/method +[remix]: https://remix.run +[formvalidation]: https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation +[indexsearchparam]: ../guides/index-search-param diff --git a/docs/components/link.md b/docs/components/link.md index edad76f64c..5e7f0f7f00 100644 --- a/docs/components/link.md +++ b/docs/components/link.md @@ -63,3 +63,42 @@ A relative `` value (that does not begin with `/`) resolves relative to > differently when the current URL ends with `/` vs when it does not. [link-native]: ./link-native + +## `resetScroll` + +If you are using [``][scrollrestoration], this lets you prevent the scroll position from being reset to the top of the window when the link is clicked. + +```tsx + +``` + +This does not prevent the scroll position from being restored when the user comes back to the location with the back/forward buttons, it just prevents the reset when the user clicks the link. + +An example when you might want this behavior is a list of tabs that manipulate the url search params that aren't at the top of the page. You wouldn't want the scroll position to jump up to the top because it might scroll the toggled content out of the viewport! + +``` + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ โ”œโ”€โ”€โ” + โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ”‚ scrolled + โ”‚ โ”‚ โ”‚ out of view + โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ—„โ”˜ + โ”Œโ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ” + โ”‚ โ”œโ”€โ” + โ”‚ โ”‚ โ”‚ viewport + โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ + โ”‚ โ”‚ tab tab tab โ”‚ โ”‚ โ”‚ + โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ + โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ content โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ + โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ + โ”‚ โ”‚โ—„โ”˜ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +``` + +[scrollrestoration]: ./scroll-restoration diff --git a/docs/components/route.md b/docs/components/route.md index f083dbe900..58fe68db21 100644 --- a/docs/components/route.md +++ b/docs/components/route.md @@ -1,5 +1,6 @@ --- title: Route +new: true --- # `` and `` diff --git a/docs/components/scroll-restoration.md b/docs/components/scroll-restoration.md new file mode 100644 index 0000000000..ee20e7c3c5 --- /dev/null +++ b/docs/components/scroll-restoration.md @@ -0,0 +1,111 @@ +--- +title: ScrollRestoration +new: true +--- + +# `` + +This component will emulate the browser's scroll restoration on location changes after loaders have completed to ensure the scroll position is restored to the right spot, even across domains. + +You should only render one of these and it's recommended you render it in the root route of your app: + +```tsx +import * as React from "react"; +import * as ReactDOM from "react-dom"; +import { + DataBrowserRouter, + ScrollRestoration, +} from "react-router-dom"; + +function Root() { + return ( +
+ {/* ... */} + +
+ ); +} + +ReactDOM.render( + + }>{/* child routes */} + , + root +); +``` + +## `getKey` + +Optional prop that defines the key React Router should use to restore scroll positions. + +```tsx + { + // default behavior + return location.key; + }} +/> +``` + +By default it uses `location.key`, emulating the browser's default behavior without client side routing. The user can navigate to the same URL multiple times in the stack and each entry gets its own scroll position to restore. + +Some apps may want to override this behavior and restore position based on something else. Consider a social app that has four primary pages: + +- "/home" +- "/messages" +- "/notifications" +- "/search" + +If the user starts at "/home", scrolls down a bit, clicks "messages" in the navigation menu, then clicks "home" in the navigation menu (not the back button!) there will be three entries in the history stack: + +``` +1. /home +2. /messages +3. /home +``` + +By default, React Router (and the browser) will have two different scroll positions stored for `1` and `3` even though they have the same URL. That means as the user navigated from `2` โ†’ `3` the scroll position goes to the top instead of restoring to where it was in `1`. + +A solid product decision here is to keep the users scroll position on the home feed no matter how they got there (back button or new link clicks). For this, you'd want to use the `location.pathname` as the key. + +```tsx + { + return location.pathname; + }} +/> +``` + +Or you may want to only use the pathname for some paths, and use the normal behavior for everything else: + +```tsx + { + const paths = ["/home", "/notifications"]; + return paths.includes(location.pathname) + ? // home and notifications restore by pathname + location.pathname + : // everything else by location like the browser + location.key; + }} +/> +``` + +## Preventing Scroll Reset + +When navigation creates new scroll keys, the scroll position is reset to the top of the page. You can prevent the "scroll to top" behavior from your links: + +```tsx + +``` + +See also: [``][resetscroll] + +## Scroll Flashing + +Without a server side rendering framework like [Remix][remix], you may experience some scroll flashing on initial page loads. This is because React Router can't restore scroll position until your JS bundles have downloaded (because React Router doesn't exist yet). It also has to wait for the data to load and the the page to render completely before it can accurately restore scroll (if you're rendering a spinner, the viewport is likely not the size it was when the scroll position was saved). + +With server rendering in Remix, the document comes to the browser fully formed and Remix actually lets the browser restore the scroll position with the browser's own default behavior. + +[remix]: https://remix.run +[resetscroll]: ../components/link#resetscroll diff --git a/docs/guides/index-search-param.md b/docs/guides/index-search-param.md new file mode 100644 index 0000000000..0eaa62a7f1 --- /dev/null +++ b/docs/guides/index-search-param.md @@ -0,0 +1,60 @@ +--- +title: Index Query Param +new: true +--- + +# Index Query Param + +You may find a wild `?index` appear in the URL of your app when submitting forms. + +Because of nested routes, multiple routes in your route hierarchy can match the URL. Unlike navigations where all matching route loaders are called to build up the UI, when a form is submitted _only one action is called_. + +Because index routes share the same URL as their parent, the `?index` param lets you disambiguate between the two. + +For example, consider the following route config and forms: + +```jsx + + } + action={ProjectsLayout.action} + > + } + action={ProjectsPage.action} + /> + +; + +
; +; +``` + +The `?index` param will submit to the index route, the action without the index param will submit to the parent route. + +When a `` is rendered in an index route without an `action`, the `?index` param will automatically be appended so that the form posts to the index route. The following form, when submitted, will post to `/projects?index` because it is rendered in the context of the projects index route: + +```tsx +function ProjectsIndex() { + return ; +} +``` + +If you moved the code to the `ProjectsLayout` route, it would instead post to `/projects`. + +This applies to `` and all of its cousins: + +```tsx +let submit = useSubmit(); +submit({}, { action: "/projects" }); +submit({}, { action: "/projects?index" }); + +let fetcher = useFetcher(); +fetcher.submit({}, { action: "/projects" }); +fetcher.submit({}, { action: "/projects?index" }); +; +; +; // defaults to the route in context +``` diff --git a/docs/hooks/use-action-data.md b/docs/hooks/use-action-data.md new file mode 100644 index 0000000000..0d26bdb57a --- /dev/null +++ b/docs/hooks/use-action-data.md @@ -0,0 +1,84 @@ +--- +title: useActionData +new: true +--- + +# `useActionData` + +This hook provides the returned value from the previous navigation's `action` result, or `undefined` if there was no submission. + +```tsx +import { useActionData } from "react-router-dom"; + +function SomeComponent() { + let actionData = useActionData(); + // ... +} +``` + +The most common use-case for this hook is form validation errors. If the form isn't right, you can return the errors and let the user try again: + +```tsx lines=[2,8,47] +import { + useActionData, + Form, + redirect, +} from "react-router-dom"; + +export default function SignUp() { + const errors = useActionData(); + + return ( + +

+ + {errors?.email && {errors.email}} +

+ +

+ + {errors?.password && {errors.password}} +

+ +

+ +

+ + ); +} + +SignUp.action = ({ request }) => { + const formData = await request.formData(); + const email = formData.get("email"); + const password = formData.get("password"); + const errors = {}; + + // validate the fields + if (typeof email !== "string" || !email.includes("@")) { + errors.email = + "That doesn't look like an email address"; + } + + if (typeof password !== "string" || password.length < 6) { + errors.password = "Password must be > 6 characters"; + } + + // return data if we have errors + if (Object.keys(errors).length) { + return errors; + } + + // otherwise create the user and redirect + await createUser(email, password); + return redirect("/dashboard"); +}; + +// at the top of your app: + + } + action={SignUp.action} + /> +; +``` diff --git a/docs/hooks/use-fetcher.md b/docs/hooks/use-fetcher.md new file mode 100644 index 0000000000..32f2a813c0 --- /dev/null +++ b/docs/hooks/use-fetcher.md @@ -0,0 +1,219 @@ +--- +title: useFetcher +new: true +--- + +# `useFetcher` + +In HTML/HTTP, data mutations and loads are modeled with navigation: `
` and `
`. Both cause a navigation in the browser. The React Router equivalents are `` and ``. + +But sometimes you want to call a loader outside of navigation, or call an action (and get the data on the page to revalidate) without changing the URL. Or you need to have multiple mutations in-flight at the same time. + +Many interactions with the server aren't navigation events. This hook lets you plug your UI into your actions and loaders without navigating. + +This is useful when you need to: + +- fetch data not associated with UI routes (popovers, dynamic forms, etc.) +- submit data to actions without navigating (shared components like a newsletter sign ups) +- handle multiple concurrent submissions in a list (typical "todo app" list where you can click multiple buttons and all should be pending at the same time) +- infinite scroll containers +- and more! + +If you're building a highly interactive, "app like" user interface, you will `useFetcher` often. + +```tsx +import { useFetcher } from "react-router-dom"; + +function SomeComponent() { + const fetcher = useFetcher(); + + // call submit or load in a useEffect + React.useEffect(() => { + fetcher.submit(data, options); + fetcher.load(href); + }, [fetcher]); + + // build your UI with these properties + fetcher.state; + fetcher.formData; + fetcher.formMethod; + fetcher.formAction; + fetcher.data; + + // render a form that doesn't cause navigation + return ; +} +``` + +Fetchers have a lot of built-in behavior: + +- Automatically handles cancellation on interruptions of the fetch +- When submitting with POST, PUT, PATCH, DELETE, the action is called first + - After the action completes, the data on the page is revalidated to capture any mutations that may have happened, automatically keeping your UI in sync with your server state +- When multiple fetchers are inflight at once, it will + - commit the freshest available data as they each land + - ensure no stale loads override fresher data, no matter which order the responses return +- Handles uncaught errors by rendering the nearest `errorElement` (just like a normal navigation from `` or ``) +- Will redirect the app if your action/loader being called returns a redirect (just like a normal navigation from `` or ``) + +## `fetcher.state` + +You can know the state of the fetcher with `fetcher.state`. It will be one of: + +- **idle** - nothing is being fetched. +- **submitting** - A form has been submitted. If the method is GET, then the route loader is being called. If POST, PUT, PATCH, or DELETE, then the route action is being called. +- **loading** - The data on the page is being revalidated after an action submission + +## `fetcher.Form` + +Just like `` except it doesn't cause a navigation. (You'll get over the dot in JSX ... we hope!) + +```tsx +function SomeComponent() { + const fetcher = useFetcher(); + return ( + + + + ); +} +``` + +## `fetcher.load()` + +Loads data from a route loader. + +```tsx lines=[8] +import { useFetcher } from "react-router-dom"; + +function SomeComponent() { + const fetcher = useFetcher(); + + useEffect(() => { + if (fetcher.state === "idle" && !fetcher.data) { + fetcher.load("/some/route"); + } + }, [fetcher]); + + return
{fetcher.data || "Loading..."}
; +} +``` + +Although a URL might match multiple nested routes, a `fetcher.load()` call will only call the loader on the leaf match (or parent of [index routes][indexsearchparam]). + +If you find yourself calling this function inside of click handlers, you can probably simplify your code by using `` instead. + +## `fetcher.submit()` + +The imperative version of ``. If a user interaction should initiate the fetch, you should use ``. But if you, the programmer are initiating the fetch (not in response to a user clicking a button, etc.), then use this function. + +For example, you may want to log the user out after a certain amount of idle time: + +```tsx lines=[1,5,10-13] +import { useFetcher } from "react-router-dom"; +import { useFakeUserIsIdle } from "./fake/hooks"; + +export function useIdleLogout() { + const fetcher = useFetcher(); + const userIsIdle = useFakeUserIsIdle(); + + useEffect(() => { + if (userIsIdle) { + fetcher.submit( + { idle: true }, + { method: "post", action: "/logout" } + ); + } + }, [userIsIdle]); +} +``` + +If you want to submit to an index route, use the [`?index` param][indexsearchparam]. + +If you find yourself calling this function inside of click handlers, you can probably simplify your code by using `` instead. + +## `fetcher.data` + +The returned data from the loader or action is stored here. Once the data is set, it persists on the fetcher even through reloads and resubmissions. + +```tsx +function ProductDetails({ product }) { + const fetcher = useFetcher(); + + return ( +
{ + if ( + event.currentTarget.open && + fetcher.state === "idle" && + !fetcher.data + ) { + fetcher.load(`/product/${product.id}/details`); + } + }} + > + {product.name} + {fetcher.data ? ( +
{fetcher.data}
+ ) : ( +
Loading product details...
+ )} +
+ ); +} +``` + +## `fetcher.formData` + +When using `` or `fetcher.submit()`, the form data is available to build optimistic UI. + +```tsx +function TaskCheckbox({ task }) { + let fetcher = useFetcher(); + + // while data is in flight, use that to immediately render + // the state you expect the task to be in when the form + // submission completes, instead of waiting for the + // network to respond. When the network responds, the + // formData will no longer be available and the UI will + // use the value in `task.status` from the revalidation + let status = + fetcher.formData?.get("status") || task.status; + + let isComplete = status === "complete"; + + return ( + + + + ); +} +``` + +## `fetcher.formAction` + +Tells you the action url the form is being submitted to. + +```tsx +; + +// when the form is submitting +fetcher.formAction; // "mark-as-read" +``` + +## `fetcher.formMethod` + +Tells you the method of the form being submitted: get, post, put, patch, or delete. + +```tsx +; + +// when the form is submitting +fetcher.formMethod; // "post" +``` diff --git a/docs/hooks/use-fetchers.md b/docs/hooks/use-fetchers.md new file mode 100644 index 0000000000..b0e9e9edad --- /dev/null +++ b/docs/hooks/use-fetchers.md @@ -0,0 +1,136 @@ +--- +title: useFetchers +new: true +--- + +# `useFetchers` + +Returns an array of all inflight [fetchers][usefetcher] without their `load`, `submit`, or `Form` properties (can't have parent components trying to control the behavior of their children! We know from IRL experience that this is a fool's errand.) + +```tsx +import { useFetchers } from "react-router-dom"; + +function SomeComp() { + const fetchers = useFetchers(); + // array of inflight fetchers +} +``` + +This is useful for components throughout the app that didn't create the fetchers but want to use their submissions to participate in optimistic UI. + +For example, imagine a UI where the sidebar lists projects, and the main view displays a list of checkboxes for the current project. The sidebar could display the number of completed and total tasks for each project. + +``` ++-----------------+----------------------------+ +| | | +| Soccer (8/9) | [x] Do the dishes | +| | | +| > Home (2/4) | [x] Fold laundry | +| | | +| | [ ] Replace battery in the | +| | smoke alarm | +| | | +| | [ ] Change lights in kids | +| | bathroom | +| | | ++-----------------+----------------------------โ”˜ +``` + +When the user clicks a checkbox, the submission goes to the action to change the state of the task. Instead of creating a "loading state" we want to create an "optimistic UI" that will **immediately** update the checkbox to appear checked even though the server hasn't processed it yet. In the checkbox component, we can use `fetcher.submission`: + +```tsx +function Task({ task }) { + const { projectId, id } = task; + const toggle = useFetcher(); + const checked = + toggle.formData?.get("complete") || task.complete; + + return ( + + + + ); +} +``` + +This awesome for the checkbox, but the sidebar will say 2/4 while the checkboxes show 3/4 when the user clicks on of them! + +``` ++-----------------+----------------------------+ +| | | +| Soccer (8/9) | [x] Do the dishes | +| | | +| > Home (2/4) | [x] Fold laundry | +| WRONG! ^ | | +| CLICK!-->[x] Replace battery in the | +| | smoke alarm | +| | | +| | [ ] Change lights in kids | +| | bathroom | +| | | ++-----------------+----------------------------โ”˜ +``` + +Because routes are automatically revalidated, the sidebar will quickly update and be correct. But for a moment, it's gonna feel a little funny. + +This is where `useFetchers` comes in. Up in the sidebar, we can access all the inflight fetcher states from the checkboxes - even though it's not the component that created them. + +The strategy has three steps: + +1. Find the submissions for tasks in a specific project +2. Use the `fetcher.formData` to immediately update the count +3. Use the normal task's state if it's not inflight + +```tsx +function ProjectTaskCount({ project }) { + let completedTasks = 0; + const fetchers = useFetchers(); + + // Find this project's fetchers + let projectFetchers = fetchers.filter((fetcher) => { + return fetcher.formAction?.startsWith( + `/projects/${project.id}/task` + ); + }); + + // Store in a map for easy lookup + const myFetchers = new Map( + fetchers.map(({ formData }) => [ + formData.get("id"), + formData.get("complete") === "on", + ]) + ); + + // Increment the count + for (const task of project.tasks) { + if (myFetchers.has(task.id)) { + if (myFetchers.get(task.id)) { + // if it's being submitted, increment optimistically + completedTasks++; + } + } else if (task.complete) { + // otherwise use the real task's data + completedTasks++; + } + } + + return ( + + {completedTasks}/{project.tasks.length} + + ); +} +``` + +It's a little bit of work, but it's mostly just asking React Router for the state it's tracking and doing an optimistic calculation based on it. + +[usefetcher]: ./use-fetcher diff --git a/docs/hooks/use-form-action.md b/docs/hooks/use-form-action.md new file mode 100644 index 0000000000..fd0ab71166 --- /dev/null +++ b/docs/hooks/use-form-action.md @@ -0,0 +1,34 @@ +--- +title: useFormAction +new: true +--- + +# `useFormAction` + +This hook is used internally in [Form] to automatically resolve default and relative actions to the current route in context. While uncommon, you can use it directly to do things like compute the correct action for a ` + ); +} +``` + +It's also useful for automatically resolving the action for [`submit`][usesubmit] and [`fetcher.submit`][usefetchersubmit]. + +```tsx +let submit = useSubmit(); +let action = useResolvedAction(); +submit(formData, { action }); +``` + +[usesubmit]: ./use-submit +[usefetchersubmit]: ./use-fetcher#fetchersubmit diff --git a/docs/hooks/use-loader-data.md b/docs/hooks/use-loader-data.md new file mode 100644 index 0000000000..6bd8f3bc06 --- /dev/null +++ b/docs/hooks/use-loader-data.md @@ -0,0 +1,41 @@ +--- +title: useLoaderData +new: true +--- + +# `useLoaderData` + +This hook provides the value returned from your route loader. + +```tsx lines=[8] +import { useLoaderData } from "react-router-dom"; + +function loader() { + return fetchFakeAlbums(); +} + +export function Albums() { + const albums = useLoaderData(); + // ... +} + +ReactDOM.render( + + } loader={loader} /> + , + root +); +``` + +After route [actions][actions] are called, the data will be revalidated automatically and return the latest result from your loader. + +Note that `useLoaderData` _does not initiate a fetch_. It simply reads the result of a fetch React Router manages internally, so you don't need to worry about it refetching when it re-renders for reasons outside of routing. + +This also means data returned is stable between renders, so you can safely pass it to dependency arrays in React hooks like `useEffect`. It only changes when the loader is called again after actions or certain navigations. In these cases the identity will change (even if the values don't). + +You can use this hook in any component or any custom hook, not just the Route element. It will return the data from the nearest route on context. + +To get data from any active route on the page, see [`useRouteLoaderData`][routeloaderdata]. + +[actions]: ../components/route#action +[routeloaderdata]: ./use-route-loader-data diff --git a/docs/hooks/use-matches.md b/docs/hooks/use-matches.md new file mode 100644 index 0000000000..6520515b80 --- /dev/null +++ b/docs/hooks/use-matches.md @@ -0,0 +1,103 @@ +--- +title: useMatches +new: true +--- + +# `useMatches` + +Returns the current route matches on the page. This is most useful for creating abstractions in parent layouts to get access to their child route's data. + +```js +import { useMatches } from "react-router-dom"; + +function SomeComponent() { + const matches = useMatches(); + // [match1, match2, ...] +} +``` + +A `match` has the following shape: + +```js +{ + // route id + id, + + // the portion of the URL the route matched + pathname, + + // the data from the loader + data, + + // the parsed params from the URL + params, + + // the with any app specific data + handle, +}; +``` + +Pairing `` with `useMatches` gets very powerful since you can put whatever you want on a route `handle` and have access to `useMatches` anywhere. + +## Breadcrumbs + +The proverbial use case here is adding breadcrumbs to a parent layout that uses data from the child routes. + +```jsx filename=app.jsx +ReactDOM.render( + + }> + } + loader={loadMessages} + handle={{ + // you can put whatever you want on a route handle + // here we use "crumb" and return some elements, + // this is what we'll render in the breadcrumbs + // for this route + crumb: () => Messages, + }} + > + } + loader={loadThread} + handle={{ + // `crumb` is your own abstraction, we decided + // to make this one a function so we can pass + // the data from the loader to it so that our + // breadcrumb is made up of dynamic content + crumb: (data) => {data.threadName}, + }} + /> + + + , + root +); +``` + +Now we can create a `Breadcrumbs` component that takes advantage of our home-grown `crumb` abstraction with `useMatches` and `handle`. + +```tsx filename=components/breadcrumbs.jsx +function Breadcrumbs() { + let matches = useMatches(); + let crumbs = matches + // first get rid of any matches that don't have handle and crumb + .filter((match) => Boolean(match.handle?.crumb)) + // now map them into an array of elements, passing the loader + // data to each one + .map((match) => match.handle.crumb(match.data)); + + return ( +
    + {crumbs.map((crumb, index) => ( +
  1. {crumb}
  2. + ))} +
+ ); +} +``` + +Now you can render `` anywhere you want, probably in the root component. diff --git a/docs/hooks/use-navigation.md b/docs/hooks/use-navigation.md new file mode 100644 index 0000000000..b31685aac1 --- /dev/null +++ b/docs/hooks/use-navigation.md @@ -0,0 +1,80 @@ +--- +title: useNavigation +new: true +--- + +# `useNavigation` + +This hook tells you everything you need to know about a page navigation to build pending navigation indicators and optimistic UI on data mutations. Things like: + +- Global loading indicators +- Disabling forms while a mutation is happening +- Adding busy indicators to submit buttons +- Optimistically showing a new record while it's being created on the server +- Optimistically showing the new state of a record while it's being updated + +```js +import { useNavigation } from "react-router-dom"; + +function SomeComponent() { + const navigation = useNavigation(); + navigation.state; + navigation.location; + navigation.formData; + navigation.formAction; + navigation.formMethod; +} +``` + +## `navigation.state` + +- **idle** - There is no navigation pending. +- **submitting** - A form has been submitted. If GET, then route loaders are being called. If POST, PUT, PATCH, DELETE, then a route action is being called. +- **loading** - The loaders for the next routes are being called to render the next page. + +Normal navigation transitions through these states: + +``` +idle โ†’ loading โ†’ idle +``` + +GET form submissions: + +``` +idle โ†’ submitting โ†’ idle +``` + +Form submissions with POST, PUT, PATCH, or DELETE navigation: + +``` +idle โ†’ submitting โ†’ loading โ†’ idle +``` + +Here's a simple submit button that changes its text when the navigation state is changing: + +```tsx +function SubmitButton() { + const navigation = useNavigation(); + + const text = + navigation.state === "submitting" + ? "Saving..." + : navigation.state === "loading" + ? "Saved!" + : "Go"; + + return ; +} +``` + +## `navigation.formData` + +Any navigation that started from a `` or `useSubmit` will have your form's submission data attached to it. This is primarily useful to build "Optimistic UI" with the `submission.formData` [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object. + +## `navigation.location` + +This tells you what the next [location][location] is going to be. + +Note that this link will not appear "pending" if a form is being submitted to the URL the link points to, because we only do this for "loading" states. The form will contain the pending UI for when the state is "submitting", once the action is complete, then the link will go pending. + +[location]: ../utils/location diff --git a/docs/hooks/use-revalidator.md b/docs/hooks/use-revalidator.md new file mode 100644 index 0000000000..093755c87c --- /dev/null +++ b/docs/hooks/use-revalidator.md @@ -0,0 +1,63 @@ +--- +title: useRevalidator +new: true +--- + +# `useRevalidator` + +This hook allows you to revalidate the data for any reason. React Router automatically revalidates the data after actions are called, but you may want to revalidate for other reasons like when focus returns to the window. + +```tsx +import { useRevalidator } from "react-router-dom"; + +function WindowFocusRevalidator() { + let revalidator = useRevalidator(); + + useFakeWindowFocus(() => { + revalidator.revalidate(); + }); + + return ( + + ); +} +``` + +Again, React Router already revalidates the data on the page automatically in the vast majority of cases so this should rarely be needed. If you find yourself using this for normal CRUD operations on your data in response to user interactions, you're probably not taking advantage of the other APIs like [``][form], [`useSubmit`][usesubmit], or [`useFetcher`][usefetcher] that do this automatically. + +## `revalidator.state` + +Tells you the state the revalidation is in, either `"idle"` or `"loading"`. + +This is useful for creating loading indicators and spinners to let the user know the app is thinking. + +## `revalidator.revalidate()` + +This initiates a revalidation. + +```tsx +function useLivePageData() { + let revalidator = useRevalidator(); + let interval = useInterval(5000); + + useEffect(() => { + if (revalidate.state === "idle") { + revalidator.revalidate(); + } + }, [interval]); +} +``` + +## Notes + +While you can render multiple occurrences of `useRevalidator` at the same time, underneath it is a singleton. This means when one `revalidator.revalidate()` is called, all instances go into the `"loading"` state together (or rather, they all update to report the singleton state). + +Race conditions are automatically handled when calling `revalidate()` when a revalidation is already in progress. + +If a navigation happens while a revalidation is in flight, the revalidation will be cancelled and fresh data will be requested from all loaders for the next page. + +[form]: ../components/form +[usefetcher]: ./use-fetcher +[usesubmit]: ./use-submit diff --git a/docs/hooks/use-route-error.md b/docs/hooks/use-route-error.md new file mode 100644 index 0000000000..3ead622d5b --- /dev/null +++ b/docs/hooks/use-route-error.md @@ -0,0 +1,8 @@ +--- +title: useRouteError +new: true +--- + +# `useRouteError` + +This hooks returns the thrown ... uh ... we gotta revisit this. diff --git a/docs/hooks/use-route-loader-data.md b/docs/hooks/use-route-loader-data.md new file mode 100644 index 0000000000..f03d1ac226 --- /dev/null +++ b/docs/hooks/use-route-loader-data.md @@ -0,0 +1,44 @@ +--- +title: useRouteLoaderData +new: true +--- + +# `useRouterLoaderData` + +This hook makes the data at any currently rendered route available anywhere in the tree. This is useful for components deep in the tree needing data from routes much farther up, as well as parent routes needing the data of child routes deeper in the tree. + +```tsx +import { useRouteLoaderData } from "react-router-dom"; + +function SomeComp() { + const user = useRouteLoaderData("root"); + // ... +} +``` + +React Router stores data internally with deterministic, auto-generated route ids, but you can supply your own route id to make this hook much easier to work with. + +```tsx [6] + + fetchUser()} + element={} + id="root" + > + fetchJob(params.jobId)} + element={} + /> + + +``` + +Now the user is available anywhere in the app. + +```tsx +const user = useRouteLoaderData("root"); +``` + +The only data available is the routes that are currently rendered. If you ask for data from a route that is not currently rendered, the hook will return `undefined`. diff --git a/docs/hooks/use-submit.md b/docs/hooks/use-submit.md new file mode 100644 index 0000000000..cc1738b374 --- /dev/null +++ b/docs/hooks/use-submit.md @@ -0,0 +1,89 @@ +--- +title: useSubmit +new: true +--- + +# `useSubmit` + +The imperative version of `` that let's you, the programmer, submit a form instead of the user. For example, submitting the form every time a value changes inside the form: + +```tsx [8] +import { useSubmit, Form } from "react-router-dom"; + +function SearchField() { + let submit = useSubmit(); + return ( + { + submit(event.currentTarget); + }} + > + + + + ); +} +``` + +This can also be useful if you'd like to automatically sign someone out of your website after a period of inactivity. In this case, we've defined inactivity as the user hasn't navigated to any other pages after 5 minutes. + +```tsx lines=[1,10,15] +import { useSubmit, useLocation } from "react-router-dom"; +import { useEffect } from "react"; + +function AdminPage() { + useSessionTimeout(); + return
{/* ... */}
; +} + +function useSessionTimeout() { + const submit = useSubmit(); + const location = useLocation(); + + useEffect(() => { + const timer = setTimeout(() => { + submit(null, { method: "post", action: "/logout" }); + }, 5 * 60_000); + + return () => clearTimeout(timer); + }, [submit, location]); +} +``` + +## Submit target + +The first argument to submit accepts many different values. + +You can submit any form or form input element: + +```tsx +// input element events + submit(event.currentTarget)} />; + +// React refs +let ref = useRef(); + +
+ ); +} + +const projectLoader: LoaderFunction = ({ params }) => { + if (params.projectId === "unauthorized") { + throw json({ contactEmail: "administrator@fake.com" }, { status: 401 }); + } + + if (params.projectId === "broken") { + // Uh oh - in this flow we somehow didn't get our data nested under `project` + // and instead got it at the root - this will cause a render error! + return json({ + id: params.projectId, + name: "Break Some Stuff", + owner: "The Joker", + deadline: "June 2022", + cost: "FREE", + }); + } + + return json({ + project: { + id: params.projectId, + name: "Build Some Stuff", + owner: "Joe", + deadline: "June 2022", + cost: "$5,000 USD", + }, + }); +}; + +function Project() { + let { project } = useLoaderData(); + + return ( + <> +

Project Name: {project.name}

+

Owner: {project.owner}

+

Deadline: {project.deadline}

+

Cost: {project.cost}

+ + ); +} + +function ProjectErrorBoundary() { + let error = useRouteError(); + + // We only care to handle 401's at this level, so if this is not a 401 + // ErrorResponse, re-throw to let the RootErrorBoundary handle it + if (!isRouteErrorResponse(error) || error.status !== 401) { + throw error; + } + + return ( + <> +

You do not have access to this project

+

+ Please reach out to{" "} + + {error.data.contactEmail} + {" "} + to obtain access. +

+ + ); +} + +function App() { + return ( + }> + }> + } + errorElement={} + > + } + errorElement={} + loader={projectLoader} + /> + + + + ); +} + +export default App; diff --git a/examples/error-boundaries/src/index.css b/examples/error-boundaries/src/index.css new file mode 100644 index 0000000000..b2983c71dc --- /dev/null +++ b/examples/error-boundaries/src/index.css @@ -0,0 +1,19 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + max-width: 600px; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", + monospace; + background-color: #eee; + padding: 1px 3px; +} + +li { + padding: 10px 0; +} diff --git a/examples/error-boundaries/src/main.tsx b/examples/error-boundaries/src/main.tsx new file mode 100644 index 0000000000..201a661a90 --- /dev/null +++ b/examples/error-boundaries/src/main.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; + +import "./index.css"; +import App from "./App"; + +createRoot(document.getElementById("root")).render( + + + +); diff --git a/examples/error-boundaries/src/vite-env.d.ts b/examples/error-boundaries/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/error-boundaries/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/error-boundaries/tsconfig.json b/examples/error-boundaries/tsconfig.json new file mode 100644 index 0000000000..8bdaabfe5d --- /dev/null +++ b/examples/error-boundaries/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react", + "importsNotUsedAsValues": "error" + }, + "include": ["./src"] +} diff --git a/examples/error-boundaries/vite.config.ts b/examples/error-boundaries/vite.config.ts new file mode 100644 index 0000000000..b77eb48a30 --- /dev/null +++ b/examples/error-boundaries/vite.config.ts @@ -0,0 +1,36 @@ +import * as path from "path"; +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import rollupReplace from "@rollup/plugin-replace"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + rollupReplace({ + preventAssignment: true, + values: { + __DEV__: JSON.stringify(true), + "process.env.NODE_ENV": JSON.stringify("development"), + }, + }), + react(), + ], + resolve: process.env.USE_SOURCE + ? { + alias: { + "@remix-run/router": path.resolve( + __dirname, + "../../packages/router/index.ts" + ), + "react-router": path.resolve( + __dirname, + "../../packages/react-router/index.ts" + ), + "react-router-dom": path.resolve( + __dirname, + "../../packages/react-router-dom/index.tsx" + ), + }, + } + : {}, +}); diff --git a/examples/scroll-restoration/README.md b/examples/scroll-restoration/README.md index dd35c152d4..742bdd4458 100644 --- a/examples/scroll-restoration/README.md +++ b/examples/scroll-restoration/README.md @@ -1,21 +1,19 @@ --- -title: Basics +title: Scroll Restoration toc: false order: 1 --- -# Basic Example +# Scroll Restoration -This example demonstrates some of the basic features of React Router, including: +This example demonstrates the basic usage of the `` component, including: -- Layouts and nested ``s -- Index ``s -- Catch-all ``s -- Using `` as a placeholder for child routes -- Using ``s for navigation +- Restoring scroll position via `location.key` +- Restoring scroll position via `location.pathname` +- Preventing scroll resetting via `` ## Preview Open this example on [StackBlitz](https://stackblitz.com): -[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/basic?file=src/App.tsx) +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/scroll-restoration?file=src/App.tsx) From e2e434118bb1e81a87f88f5b3085600b914256f4 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 20 May 2022 17:31:12 -0400 Subject: [PATCH 089/119] feat: support routes prop on data routers --- packages/react-router-dom/index.tsx | 10 +++++-- .../__tests__/DataMemoryRouter-test.tsx | 25 +++++++++++++++++ packages/react-router/lib/components.tsx | 27 +++++++++++++++++-- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index 686e67ff79..6b15585bff 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -164,6 +164,7 @@ export interface DataBrowserRouterProps { children?: React.ReactNode; hydrationData?: HydrationState; fallbackElement: React.ReactElement; + routes?: RouteObject[]; window?: Window; } @@ -171,12 +172,14 @@ export function DataBrowserRouter({ children, fallbackElement, hydrationData, + routes, window, }: DataBrowserRouterProps): React.ReactElement { return useRenderDataRouter({ children, fallbackElement, - createRouter: (routes: RouteObject[]) => + routes, + createRouter: (routes) => createBrowserRouter({ routes, hydrationData, @@ -189,6 +192,7 @@ export interface DataHashRouterProps { children?: React.ReactNode; hydrationData?: HydrationState; fallbackElement: React.ReactElement; + routes?: RouteObject[]; window?: Window; } @@ -196,12 +200,14 @@ export function DataHashRouter({ children, hydrationData, fallbackElement, + routes, window, }: DataBrowserRouterProps): React.ReactElement { return useRenderDataRouter({ children, fallbackElement, - createRouter: (routes: RouteObject[]) => + routes, + createRouter: (routes) => createHashRouter({ routes, hydrationData, diff --git a/packages/react-router/__tests__/DataMemoryRouter-test.tsx b/packages/react-router/__tests__/DataMemoryRouter-test.tsx index 425658e8b6..b4d3b1c54e 100644 --- a/packages/react-router/__tests__/DataMemoryRouter-test.tsx +++ b/packages/react-router/__tests__/DataMemoryRouter-test.tsx @@ -64,6 +64,31 @@ describe("", () => { `); }); + it("supports a `routes` prop instead of children", () => { + let routes = [ + { + path: "/", + element:

Home

, + }, + ]; + let { container } = render( + } + initialEntries={["/"]} + hydrationData={{}} + routes={routes} + /> + ); + + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+

+ Home +

+
" + `); + }); + it("renders the first route that matches the URL when wrapped in a 'basename' route", () => { // In data routers there is no basename and you should instead use a root route let { container } = render( diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index d6b661a4fb..d5d44c7b68 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -58,15 +58,17 @@ export function _resetModuleScope() { export function useRenderDataRouter({ children, fallbackElement, + routes, createRouter, }: { children?: React.ReactNode; fallbackElement: React.ReactElement; + routes?: RouteObject[]; createRouter: (routes: RouteObject[]) => DataRouter; }): React.ReactElement { if (!routerSingleton) { routerSingleton = createRouter( - createRoutesFromChildren(children) + routes || createRoutesFromChildren(children) ).initialize(); } let router = routerSingleton; @@ -98,7 +100,7 @@ export function useRenderDataRouter({ navigationType={state.historyAction} navigator={navigator} > - + @@ -111,6 +113,7 @@ export interface DataMemoryRouterProps { initialIndex?: number; hydrationData?: HydrationState; fallbackElement: React.ReactElement; + routes?: RouteObject[]; } export function DataMemoryRouter({ @@ -119,10 +122,12 @@ export function DataMemoryRouter({ initialIndex, hydrationData, fallbackElement, + routes, }: DataMemoryRouterProps): React.ReactElement { return useRenderDataRouter({ children, fallbackElement, + routes, createRouter: (routes) => createMemoryRouter({ initialEntries, @@ -385,6 +390,24 @@ export function Routes({ return useRoutes(createRoutesFromChildren(children), location); } +interface DataRoutesProps extends RoutesProps { + routes?: RouteObject[]; +} + +/** + * @private + * Used as an extension to and accepts a manual `routes` array to be + * instead of using JSX children. Extracted to it's own component to avoid + * conditional usage of `useRoutes` if we have to render a `fallbackElement` + */ +function DataRoutes({ + children, + location, + routes, +}: DataRoutesProps): React.ReactElement | null { + return useRoutes(routes || createRoutesFromChildren(children), location); +} + /////////////////////////////////////////////////////////////////////////////// // UTILS /////////////////////////////////////////////////////////////////////////////// From ebca0ffacf6972bc9d45e28ebdb004a8a59b3fb8 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Fri, 20 May 2022 21:18:55 -0600 Subject: [PATCH 090/119] docs: moar docs --- docs/components/index.md | 2 +- docs/components/route.md | 54 ++++-- docs/fetch/index.md | 4 + docs/fetch/is-route-error-response.md | 3 + docs/fetch/json.md | 3 + docs/fetch/redirect.md | 3 + docs/guides/index-route.md | 7 + docs/guides/migrating-to-remix.md | 47 +++++ docs/route/action.md | 6 + docs/route/error-element.md | 139 +++++++++++++ docs/route/index.md | 4 + docs/route/loader.md | 181 +++++++++++++++++ docs/route/route.md | 269 ++++++++++++++++++++++++++ docs/routers/index.md | 2 +- docs/routers/picking-a-router.md | 56 ++++++ docs/upgrading/index.md | 2 +- docs/utils/index.md | 2 +- 17 files changed, 765 insertions(+), 19 deletions(-) create mode 100644 docs/fetch/index.md create mode 100644 docs/fetch/is-route-error-response.md create mode 100644 docs/fetch/json.md create mode 100644 docs/fetch/redirect.md create mode 100644 docs/guides/index-route.md create mode 100644 docs/guides/migrating-to-remix.md create mode 100644 docs/route/action.md create mode 100644 docs/route/error-element.md create mode 100644 docs/route/index.md create mode 100644 docs/route/loader.md create mode 100644 docs/route/route.md diff --git a/docs/components/index.md b/docs/components/index.md index 2499554bbb..ee3eba1746 100644 --- a/docs/components/index.md +++ b/docs/components/index.md @@ -1,4 +1,4 @@ --- title: Components -order: 4 +order: 3 --- diff --git a/docs/components/route.md b/docs/components/route.md index 58fe68db21..4efa4d7226 100644 --- a/docs/components/route.md +++ b/docs/components/route.md @@ -3,36 +3,60 @@ title: Route new: true --- -# `` and `` +# `` -
- Type declaration +Routes are perhaps the most important part of a React Router app. They couple URL segments to components, data loading, and data mutations. ```tsx -declare function Routes( - props: RoutesProps -): React.ReactElement | null; +} + // when the URL matches this segment + path="teams/:teamId" + // with this data before rendering + loader={async ({ params }) => { + return fetch(`/fake/api/teams/${params.teamId}.json`); + }} + // performs this mutation when data is submitted to it + action={async ({ request }) => { + return updateFakeTeam(await request.formData()); + }} + // and renders this element in case something went wrong + errorElement={} +/> +``` -interface RoutesProps { - children?: React.ReactNode; - location?: Partial | string; -} +Routes can also exist to simply be submitted to: + +```tsx + { + await fakeDeleteProject(params.projectId) + return redirect("/projects"); + }} +> +``` + +## Type declaration +```tsx declare function Route( props: RouteProps ): React.ReactElement | null; interface RouteProps { + path?: string; + index?: boolean; caseSensitive?: boolean; - children?: React.ReactNode; + loader?: DataFunction; + action?: DataFunction; element?: React.ReactNode | null; - index?: boolean; - path?: string; + errorElement?: React.Node | null; + children?: React.ReactNode; } ``` -
- `` and `` are the primary ways to render something in React Router based on the current [`location`][location]. You can think about a `` kind of like an `if` statement; if its `path` matches the current URL, it renders its `element`! The `` prop determines if the matching should be done in a case-sensitive manner (defaults to `false`). Whenever the location changes, `` looks through all its `children` `` elements to find the best match and renders that branch of the UI. `` elements may be nested to indicate nested UI, which also correspond to nested URL paths. Parent routes render their child routes by rendering an [``][outlet]. diff --git a/docs/fetch/index.md b/docs/fetch/index.md new file mode 100644 index 0000000000..31ad0db371 --- /dev/null +++ b/docs/fetch/index.md @@ -0,0 +1,4 @@ +--- +title: Fetch Utilities +order: 5 +--- diff --git a/docs/fetch/is-route-error-response.md b/docs/fetch/is-route-error-response.md new file mode 100644 index 0000000000..f008b766cf --- /dev/null +++ b/docs/fetch/is-route-error-response.md @@ -0,0 +1,3 @@ +--- +title: isRouteErrorResponse +--- diff --git a/docs/fetch/json.md b/docs/fetch/json.md new file mode 100644 index 0000000000..a649361274 --- /dev/null +++ b/docs/fetch/json.md @@ -0,0 +1,3 @@ +--- +title: json +--- diff --git a/docs/fetch/redirect.md b/docs/fetch/redirect.md new file mode 100644 index 0000000000..8358272f4a --- /dev/null +++ b/docs/fetch/redirect.md @@ -0,0 +1,3 @@ +--- +title: redirect +--- diff --git a/docs/guides/index-route.md b/docs/guides/index-route.md new file mode 100644 index 0000000000..11e3bf2c89 --- /dev/null +++ b/docs/guides/index-route.md @@ -0,0 +1,7 @@ +--- +title: Index Route +--- + +# Index Route + +TODO diff --git a/docs/guides/migrating-to-remix.md b/docs/guides/migrating-to-remix.md new file mode 100644 index 0000000000..4ed29fcbe5 --- /dev/null +++ b/docs/guides/migrating-to-remix.md @@ -0,0 +1,47 @@ +--- +title: Migrating to Remix +--- + +# Migrating to Remix + +This doc is a stub + +With an entry like this: + +```tsx filename=app.js +import { DataBrowserRouter, Route } from "react-router-dom"; +import Teams, { + loader as teamLoader, +} from "./routes/teams"; + +ReactDOM.render( + + } + path="/teams" + loader={teamsLoader} + /> + , + root +); +``` + +And routes modules that look like this, where loaders return responses: + +```tsx filename=routes/teams.jsx +import { json } from "react-router-dom"; + +export function loader() { + const teams = await someIsomorphicDatabase + .from("teams") + .select("*"); + return json(teams); +} + +export default function Teams() { + const data = useLoaderData(); + // ... +} +``` + +You can quite literally copy/paste them into a Remix app. diff --git a/docs/route/action.md b/docs/route/action.md new file mode 100644 index 0000000000..444c5e56d5 --- /dev/null +++ b/docs/route/action.md @@ -0,0 +1,6 @@ +--- +title: action +new: true +--- + +# Route Action diff --git a/docs/route/error-element.md b/docs/route/error-element.md new file mode 100644 index 0000000000..02a247e4de --- /dev/null +++ b/docs/route/error-element.md @@ -0,0 +1,139 @@ +--- +title: errorElement +new: true +--- + +# `errorElement` + +When dealing with external data, you can't always plan on it being found, the user having access to it, or the service even being up! + +Here's a simple "not found" case: + +```tsx [4,7-9] +} + errorElement={} + loader={async ({ params }) => { + const res = await fetch(`/api/properties/${params.id}`); + if (res.status === 404) { + throw new Response("Not Found", { status: 404 }); + } + const home = res.json(); + const descriptionHtml = parseMarkdown( + data.descriptionMarkdown + ); + return { home, descriptionHtml }; + }} +/> +``` + +As soon as you know you can't render the route with the data your'e loading, you can throw to break the call stack. You don't have to worry about the rest of the work in the loader like parsing the user's markdown bio when it doesn't exist. + +React Router will catch the response and render the `errorElement` instead. This keeps your route components super clean because they don't have to worry about exceptions. + +Here is a full example showing how you can create utility functions that throw responses to stop code execution in the loader and move over to an alternative UI. + +```ts filename=app/db.ts +import { json } from "@remix-run/{runtime}"; +import type { ThrownResponse } from "@remix-run/react"; + +export type InvoiceNotFoundResponse = ThrownResponse< + 404, + string +>; + +export function getInvoice(id, user) { + const invoice = db.invoice.find({ where: { id } }); + if (invoice === null) { + throw json("Not Found", { status: 404 }); + } + return invoice; +} +``` + +```ts filename=app/http.ts +import { redirect } from "@remix-run/{runtime}"; + +import { getSession } from "./session"; + +export async function requireUserSession(request) { + const session = await getSession( + request.headers.get("cookie") + ); + if (!session) { + // can throw our helpers like `redirect` and `json` because they + // return responses. + throw redirect("/login", 302); + } + return session.get("user"); +} +``` + +```tsx filename=app/routes/invoice/$invoiceId.tsx +import { useCatch, useLoaderData } from "@remix-run/react"; +import type { ThrownResponse } from "@remix-run/react"; + +import { requireUserSession } from "~/http"; +import { getInvoice } from "~/db"; +import type { + Invoice, + InvoiceNotFoundResponse, +} from "~/db"; + +type InvoiceCatchData = { + invoiceOwnerEmail: string; +}; + +type ThrownResponses = + | InvoiceNotFoundResponse + | ThrownResponse<401, InvoiceCatchData>; + +export const loader = async ({ request, params }) => { + const user = await requireUserSession(request); + const invoice: Invoice = getInvoice(params.invoiceId); + + if (!invoice.userIds.includes(user.id)) { + const data: InvoiceCatchData = { + invoiceOwnerEmail: invoice.owner.email, + }; + throw json(data, { status: 401 }); + } + + return json(invoice); +}; + +export default function InvoiceRoute() { + const invoice = useLoaderData(); + return ; +} + +export function CatchBoundary() { + // this returns { status, statusText, data } + const caught = useCatch(); + + switch (caught.status) { + case 401: + return ( +
+

You don't have access to this invoice.

+

+ Contact {caught.data.invoiceOwnerEmail} to get + access +

+
+ ); + case 404: + return
Invoice not found!
; + } + + // You could also `throw new Error("Unknown status in catch boundary")`. + // This will be caught by the closest `ErrorBoundary`. + return ( +
+ Something went wrong: {caught.status}{" "} + {caught.statusText} +
+ ); +} +``` diff --git a/docs/route/index.md b/docs/route/index.md new file mode 100644 index 0000000000..57ae4c0e4b --- /dev/null +++ b/docs/route/index.md @@ -0,0 +1,4 @@ +--- +title: Route +order: 2 +--- diff --git a/docs/route/loader.md b/docs/route/loader.md new file mode 100644 index 0000000000..99b2bc578b --- /dev/null +++ b/docs/route/loader.md @@ -0,0 +1,181 @@ +--- +title: loader +new: true +--- + +# `loader` + +Each route can define a "loader" function to provide data to the route element before it renders. + +This feature only works if using a data router + +```js [5-7,12-14] + + } + path="teams" + loader={async () => { + return fakeDb.from("teams").select("*"); + }} + > + } + path=":teamId" + loader={async ({ params }) => { + return fetch(`/api/teams/${params.teamId}.json`); + }} + /> + + +``` + +As the user navigates around the app, the loaders for the next matching branch of routes will be called in parallel and their data made available to components through [`useLoaderData`][useloaderdata]. + +## `params` + +Route params are parsed from [dynamic segments][dynamicsegments] and passed to your loader. This is useful for figuring out which resource to load: + +```tsx + { + return fakeGetTeam(params.teamId); + }} +/> +``` + +## `request` + +This is a [Fetch Request][request] instance about the what is being requested from your application. + +```tsx + {}} /> +``` + +> A request?! + +It might seem odd at first that loaders receive a "request". Consider that `` does something like the following code and ask yourself, "what default behavior is being prevented here?". + +```tsx [4] + { + event.preventDefault(); + navigate(props.to); + }} +/> +``` + +Without React Router, the browser would have made a Request to your server, but React Router prevented it! Instead of the browser sending the request to your server, React Router sends the request to your loaders. + +The most common use case is creating a [URL][url] and reading the [URLSearchParams][urlsearchparams] from it: + +```tsx + { + const url = new URL(request.url); + const searchTerm = url.searchParams.get("q"); + return searchProducts(searchTerm); + }} +/> +``` + +Note that the APIs here are not React Router specific, but rather standard web objects: [Request][request], [URL][url], [URLSearchParams][urlsearchparams]. + +## Returning Responses + +While you can return anything you want from a loader and get access to it from [`useLoaderData`][useloaderdata], you can also return a web [Response][response]. + +This might not seem immediately useful, but consider `fetch`. Since the return value of of `fetch` is a Response, and loaders understand responses, many loaders can return a simple fetch! + +```tsx [4,11-17] +// an HTTP API +} + loader={() => fetch("/api/teams.json")} +/>; + +// or even a graphql endpoint +} + loader={({ params }) => + fetch("/_gql", { + method: "post", + body: JSON.stringify({ + query: gql`...`, + params: params, + }), + }) + } +/>; +``` + +You can construct the response yourself as well: + +```tsx [5-10] +} + loader={() => { + const data = { some: "thing" }; + return new Response(JSON.stringify(data), { + status: 200, + headers: { + "Content-Type": "application/json; utf-8", + }, + }); + }} +/> +``` + +Remix will automatically call `response.json()` so your components don't need to parse it while rendering: + +```tsx +function SomeRoute() { + const data = useLoaderData(); + // { some: "thing" } +} +``` + +Using the [`json`][json] utility simplifies this so you don't have to construct them yourself. This next example is effectively the same as the previous example: + +```tsx +import { json } from "react-router-dom"; + +} + loader={() => { + const data = { some: "thing" }; + return json(data); + }} +/>; +``` + +If you're planning an upgrade to Remix, returning responses from every loader will make the migration smoother. You can read more about that here: [Migrating to Remix][migratingtoremix]. + +## Throwing in Loaders + +You can `throw` in your loader to break out of the current call stack (stop running the current code) and React Router will start over down the "error path". + +```tsx [5] + { + const res = await fetch(`/api/properties/${params.id}`); + if (res.status === 404) { + throw new Response("Not Found", { status: 404 }); + } + return res.json(); + }} +/> +``` + +For more details, read the [errorElement][errorelement] documentation. + +[dynamicsegments]: ./route#dynamic-segments +[request]: https://developer.mozilla.org/en-US/docs/Web/API/Request +[response]: https://developer.mozilla.org/en-US/docs/Web/API/Response +[url]: https://developer.mozilla.org/en-US/docs/Web/API/URL +[urlsearchparams]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams +[migratingtoremix]: ../guides/migrating-to-remix +[useloaderdata]: ../hooks/use-loader-data +[json]: ../fetch/json +[errorelement]: ./error-element diff --git a/docs/route/route.md b/docs/route/route.md new file mode 100644 index 0000000000..6e51bfcaad --- /dev/null +++ b/docs/route/route.md @@ -0,0 +1,269 @@ +--- +title: Route +new: true +order: 1 +--- + +# Route + +Routes are perhaps the most important part of a React Router app. They couple URL segments to components, data loading and data mutations. Through route nesting, complex application layouts and data dependencies become simple and declarative. + +```tsx + + } + // when the URL matches this segment + path="teams/:teamId" + // with this data before rendering + loader={async ({ params }) => { + return fetch(`/fake/api/teams/${params.teamId}.json`); + }} + // performing this mutation when data is submitted to it + action={async ({ request }) => { + return updateFakeTeam(await request.formData()); + }} + // and renders this element in case something went wrong + errorElement={} + /> + +``` + +They can also be configured as objects, which is useful for frameworks and abstractions like [Remix][remix] to do file based routing and server rendering. + +```tsx +const routes = [ + { + path: "/", + element: , + children: [ + { + path: "teams/:teamId", + element: , + loader: () => {}, + action: () => {}, + errorElement: , + }, + ], + }, +]; + +; +``` + +## Type declaration + +```tsx +interface RouteProps { + path?: string; + index?: boolean; + children?: React.ReactNode; + caseSensitive?: boolean; + loader?: DataFunction; + action?: DataFunction; + element?: React.ReactNode | null; + errorElement?: React.Node | null; +} +``` + +## `path` + +The path pattern to match against the URL to determine if this route matches a URL, link href, or form action. + +### Dynamic Segments + +If a path segment starts with `:` then it becomes a "dynamic segment". When the route matches the URL, the dynamic segment will be parsed from the URL and provided as `params` to other router APIs. + +```tsx + { + console.log(params.teamId); // "hotspur" + }} + // and the action + action={({ params }) => {}} + element={} +/>; + +// and the element through `useParams` +function Team() { + let params = useParams(); + console.log(params.teamId); // "hotspurs" +} +``` + +You can have multiple dynamic segments in one route path: + +```tsx +; +// both will be available +params.categoryId; +params.productId; +``` + +Dynamic segments cannot be "partial": + +- ๐Ÿšซ `"/teams-:teamId"` +- โœ… `"/teams/:teamId"` +- ๐Ÿšซ `"/:category--:productId"` +- โœ… `"/:productSlug"` + +You can still support URL patterns like that, you just have to do a bit of your own parsing: + +```tsx +function Product() { + const { productSlug } = useParams(); + const [category, product] = productSlug.split("--"); + // ... +} +``` + +### Splats + +Also known as "catchall" and "star" segments. If a route path pattern ends with `/*` then it will match any characters following the `/`, including other `/` characters. + +```tsx + { + console.log(params["*"]); // "files/one/two" + }} + // and the action + action={({ params }) => {}} + element={} +/>; + +// and the element through `useParams` +function Team() { + let params = useParams(); + console.log(params["*"]); // "hotspurs" +} +``` + +You can destructure the `*`, you just have to assign it a new name. A common name is `splat`: + +```tsx +let { org, "*": splat } = params; +``` + +## `index` + +Determines if the route is an index route. Index routes render into their parent's [Outlet][outlet] at their parent's URL (like a default child route). + +```jsx [2] +}> + } /> + } /> + +``` + +These special routes can be confusing to understand at first, so we have a guide dedicated to them here: [Index Route][indexroute]. + +## `children` + +(TODO: need to talk about nesting, maybe even a separate doc) + +## `caseSensitive` + +Instructs the route to match case or not: + +```jsx + +``` + +- Will match `"wEll-aCtuA11y"` +- Will not match `"well-actua11y"` + +## `loader` + +The route loader is called before the route renders and provides data for the element through [`useLoaderData`][useloaderdata]. + +```tsx [3-5] + { + return fetchTeam(params.teamId); + }} +/>; + +function Team() { + let team = useLoaderData(); + // ... +} +``` + +If you are not using a Data router, this will do nothing + +Please see the [loader][loader] documentation for more details. + +## `action` + +The route action is called when a submission is sent to the route from a [Form][form], [fetcher][fetcher], or [submission][usesubmit]. + +```tsx [3-5] + { + const formData = await request.formData(); + return updateTeam(formData); + }} +/> +``` + +If you are not using a Data router, this will do nothing + +Please see the [action][action] documentation for more details. + +## `element` + +The element to render when the route matches the URL. + +```tsx +} /> +``` + +## `errorElement` + +When a route throws an exception while rendering, in a `loader` or in an `action`, this element will render instead of the normal `element`. + +```tsx +} + // or this while loading properties + loader={() => loadProperties()} + // or this while creating a property + action={({ request }) => + createProperty(await request.formData()) + } + // then this element will render + errorElement={} +/> +``` + +If you are not using a Data router, this will do nothing + +Please see the [errorElement][errorelement] documentation for more details. + +[outlet]: ./outlet +[remix]: https://remix.run +[indexroute]: ../guides/index-route +[outlet]: ../components/outlet +[useloaderdata]: ../hooks/use-loader-data +[loader]: ./loader +[action]: ./action +[errorelement]: ./error-element +[form]: ../components/form +[fetcher]: ../hooks/use-fetcher +[usesubmit]: ../hooks/use-submit diff --git a/docs/routers/index.md b/docs/routers/index.md index 3373065ace..83e2a643a1 100644 --- a/docs/routers/index.md +++ b/docs/routers/index.md @@ -1,4 +1,4 @@ --- title: Routers -order: 1 +order: 3 --- diff --git a/docs/routers/picking-a-router.md b/docs/routers/picking-a-router.md index b47d64dcdf..5c88b87e82 100644 --- a/docs/routers/picking-a-router.md +++ b/docs/routers/picking-a-router.md @@ -6,6 +6,10 @@ new: true # Picking a Router + + + + This doc is a work in progress React Router ships with several "routers" depending on the environment you're app is running in and the use cases you have. This document should help you figure out which one to use. @@ -19,3 +23,55 @@ React Router ships with several "routers" depending on the environment you're ap [staticrouter]: ./static-router [memoryrouter]: ./memory-router [nativerouter]: ./native-router + +```jsx +} +> + } + errorElement={} + loader={() => { + let project = getProject(); + if (!useHasAccess(project)) { + throw { contactEmail }; + } + return project; + }} + /> +; + +import { isErrorResponse } from "react-router-dom"; + +function OnlyKnows401s() { + let error = useRouteError(); + + if (!error.contactEmail) { + throw error; + } + + return
Contact {error.data.contactEmail}
; +} + +// unwrapped response to make rendering boundaries easy +// don't want async react code/state/effects to unwrap .json() +class ErrorResponse { + // constructor(@status, @statusText, @data) {} +} + +function isRouteErrorResponse(thing) { + return thing instanceof ErrorResponse; +} + +function Project() { + let data = useLoaderData(); + return ( +
+

Project

+ {data.project.title} +
+ ); +} +``` diff --git a/docs/upgrading/index.md b/docs/upgrading/index.md index c0bc58dfc8..6f43e67dce 100644 --- a/docs/upgrading/index.md +++ b/docs/upgrading/index.md @@ -1,4 +1,4 @@ --- title: Upgrading -order: 6 +order: 1 --- diff --git a/docs/utils/index.md b/docs/utils/index.md index 17b2a770ad..20dd7eaaff 100644 --- a/docs/utils/index.md +++ b/docs/utils/index.md @@ -1,4 +1,4 @@ --- title: Utilities -order: 4 +order: 5 --- From c78e67473b2121a762b233e0648697ae0718e851 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Sat, 21 May 2022 08:43:59 -0400 Subject: [PATCH 091/119] chore: remove navigation.type --- packages/router/__tests__/router-test.ts | 76 ++++-------- packages/router/router.ts | 147 ++++++++--------------- packages/router/utils.ts | 6 +- 3 files changed, 73 insertions(+), 156 deletions(-) diff --git a/packages/router/__tests__/router-test.ts b/packages/router/__tests__/router-test.ts index 6593e14a59..a8d28ab4ee 100644 --- a/packages/router/__tests__/router-test.ts +++ b/packages/router/__tests__/router-test.ts @@ -585,7 +585,8 @@ function setup({ // if a revalidation interrupts an action submission, we don't actually // start a new new navigation so don't increment here let navigationId = - currentRouter.state.navigation.type === "actionSubmission" + currentRouter.state.navigation.state === "submitting" && + currentRouter.state.navigation.formMethod !== "get" ? guid : ++guid; activeLoaderType = "navigation"; @@ -800,7 +801,6 @@ describe("a router", () => { navigation: { location: undefined, state: "idle", - type: "idle", }, resetScrollPosition: true, restoreScrollPosition: null, @@ -1075,7 +1075,7 @@ describe("a router", () => { it("loads new data on new routes even if there's also a hash change", async () => { let t = initializeTmTest(); let A = await t.navigate("/foo#bar"); - expect(t.router.state.navigation.type).toBe("normalLoad"); + expect(t.router.state.navigation.state).toBe("loading"); await A.loaders.foo.resolve("A"); expect(t.router.state.loaderData).toMatchObject({ root: "ROOT", @@ -1087,21 +1087,21 @@ describe("a router", () => { let t = initializeTmTest(); let A = await t.navigate("/bar"); - expect(t.router.state.navigation.type).toBe("normalLoad"); + expect(t.router.state.navigation.state).toBe("loading"); expect(t.router.state.navigation.location?.pathname).toBe("/bar"); expect(t.router.state.loaderData).toMatchObject({ root: "ROOT", }); let B = await A.loaders.bar.redirect("/baz"); - expect(t.router.state.navigation.type).toBe("normalRedirect"); + expect(t.router.state.navigation.state).toBe("loading"); expect(t.router.state.navigation.location?.pathname).toBe("/baz"); expect(t.router.state.loaderData).toMatchObject({ root: "ROOT", }); await B.loaders.baz.resolve("B"); - expect(t.router.state.navigation.type).toBe("idle"); + expect(t.router.state.navigation.state).toBe("idle"); expect(t.router.state.location.pathname).toBe("/baz"); expect(t.router.state.loaderData).toMatchObject({ root: "ROOT", @@ -1113,21 +1113,21 @@ describe("a router", () => { let t = initializeTmTest(); let A = await t.navigate("/bar"); - expect(t.router.state.navigation.type).toBe("normalLoad"); + expect(t.router.state.navigation.state).toBe("loading"); expect(t.router.state.navigation.location?.pathname).toBe("/bar"); expect(t.router.state.loaderData).toMatchObject({ root: "ROOT", }); let B = await A.loaders.bar.redirectReturn("/baz"); - expect(t.router.state.navigation.type).toBe("normalRedirect"); + expect(t.router.state.navigation.state).toBe("loading"); expect(t.router.state.navigation.location?.pathname).toBe("/baz"); expect(t.router.state.loaderData).toMatchObject({ root: "ROOT", }); await B.loaders.baz.resolve("B"); - expect(t.router.state.navigation.type).toBe("idle"); + expect(t.router.state.navigation.state).toBe("idle"); expect(t.router.state.location.pathname).toBe("/baz"); expect(t.router.state.loaderData).toMatchObject({ root: "ROOT", @@ -1139,7 +1139,7 @@ describe("a router", () => { let t = initializeTmTest(); let A = await t.navigate("/bar"); - expect(t.router.state.navigation.type).toBe("normalLoad"); + expect(t.router.state.navigation.state).toBe("loading"); expect(t.router.state.navigation.location?.pathname).toBe("/bar"); expect(t.router.state.loaderData).toMatchObject({ root: "ROOT", @@ -1148,7 +1148,7 @@ describe("a router", () => { let B = await A.loaders.bar.redirectReturn("/baz", undefined, { "X-Remix-Revalidate": "yes", }); - expect(t.router.state.navigation.type).toBe("normalRedirect"); + expect(t.router.state.navigation.state).toBe("loading"); expect(t.router.state.navigation.location?.pathname).toBe("/baz"); expect(t.router.state.loaderData).toMatchObject({ root: "ROOT", @@ -1156,7 +1156,7 @@ describe("a router", () => { await B.loaders.root.resolve("ROOT*"); await B.loaders.baz.resolve("B"); - expect(t.router.state.navigation.type).toBe("idle"); + expect(t.router.state.navigation.state).toBe("idle"); expect(t.router.state.location.pathname).toBe("/baz"); expect(t.router.state.loaderData).toMatchObject({ root: "ROOT*", @@ -2189,7 +2189,6 @@ describe("a router", () => { let t = initializeTmTest(); let navigation = t.router.state.navigation; expect(navigation.state).toBe("idle"); - expect(navigation.type).toBe("idle"); expect(navigation.formData).toBeUndefined(); expect(navigation.location).toBeUndefined(); }); @@ -2199,7 +2198,6 @@ describe("a router", () => { let A = await t.navigate("/foo"); let navigation = t.router.state.navigation; expect(navigation.state).toBe("loading"); - expect(navigation.type).toBe("normalLoad"); expect(navigation.formData).toBeUndefined(); expect(navigation.location).toMatchObject({ pathname: "/foo", @@ -2210,7 +2208,6 @@ describe("a router", () => { await A.loaders.foo.resolve("A"); navigation = t.router.state.navigation; expect(navigation.state).toBe("idle"); - expect(navigation.type).toBe("idle"); expect(navigation.formData).toBeUndefined(); expect(navigation.location).toBeUndefined(); }); @@ -2223,14 +2220,12 @@ describe("a router", () => { let navigation = t.router.state.navigation; expect(navigation.state).toBe("loading"); - expect(navigation.type).toBe("normalRedirect"); expect(navigation.formData).toBeUndefined(); expect(navigation.location?.pathname).toBe("/bar"); await B.loaders.bar.resolve("B"); navigation = t.router.state.navigation; expect(navigation.state).toBe("idle"); - expect(navigation.type).toBe("idle"); expect(navigation.formData).toBeUndefined(); expect(navigation.location).toBeUndefined(); }); @@ -2244,7 +2239,6 @@ describe("a router", () => { }); let navigation = t.router.state.navigation; expect(navigation.state).toBe("submitting"); - expect(navigation.type).toBe("actionSubmission"); expect( // @ts-expect-error @@ -2261,7 +2255,6 @@ describe("a router", () => { await A.actions.foo.resolve("A"); navigation = t.router.state.navigation; expect(navigation.state).toBe("loading"); - expect(navigation.type).toBe("actionReload"); expect( // @ts-expect-error new URLSearchParams(navigation.formData).toString() @@ -2277,12 +2270,10 @@ describe("a router", () => { await A.loaders.foo.resolve("A"); navigation = t.router.state.navigation; expect(navigation.state).toBe("loading"); - expect(navigation.type).toBe("actionReload"); await A.loaders.root.resolve("B"); navigation = t.router.state.navigation; expect(navigation.state).toBe("idle"); - expect(navigation.type).toBe("idle"); expect(navigation.formData).toBeUndefined(); expect(navigation.location).toBeUndefined(); }); @@ -2298,7 +2289,6 @@ describe("a router", () => { let navigation = t.router.state.navigation; expect(navigation.state).toBe("loading"); - expect(navigation.type).toBe("submissionRedirect"); expect( // @ts-expect-error new URLSearchParams(navigation.formData).toString() @@ -2313,12 +2303,10 @@ describe("a router", () => { await B.loaders.bar.resolve("B"); navigation = t.router.state.navigation; expect(navigation.state).toBe("loading"); - expect(navigation.type).toBe("submissionRedirect"); await B.loaders.root.resolve("C"); navigation = t.router.state.navigation; expect(navigation.state).toBe("idle"); - expect(navigation.type).toBe("idle"); expect(navigation.formData).toBeUndefined(); expect(navigation.location).toBeUndefined(); }); @@ -2330,7 +2318,6 @@ describe("a router", () => { }); let navigation = t.router.state.navigation; expect(navigation.state).toBe("submitting"); - expect(navigation.type).toBe("loaderSubmission"); expect( // @ts-expect-error new URLSearchParams(navigation.formData).toString() @@ -2346,7 +2333,6 @@ describe("a router", () => { await A.loaders.foo.resolve("A"); navigation = t.router.state.navigation; expect(navigation.state).toBe("idle"); - expect(navigation.type).toBe("idle"); expect(navigation.formData).toBeUndefined(); expect(navigation.location).toBeUndefined(); }); @@ -2361,7 +2347,6 @@ describe("a router", () => { let navigation = t.router.state.navigation; expect(navigation.state).toBe("loading"); - expect(navigation.type).toBe("submissionRedirect"); expect( // @ts-expect-error new URLSearchParams(navigation.formData).toString() @@ -2372,7 +2357,6 @@ describe("a router", () => { await B.loaders.bar.resolve("B"); navigation = t.router.state.navigation; expect(navigation.state).toBe("idle"); - expect(navigation.type).toBe("idle"); expect(navigation.formData).toBeUndefined(); expect(navigation.location).toBeUndefined(); }); @@ -2600,7 +2584,7 @@ describe("a router", () => { formData: createFormData({ key: "value" }), }); await A.actions.foo.resolve("A ACTION"); - expect(t.router.state.navigation.type).toBe("actionReload"); + expect(t.router.state.navigation.state).toBe("loading"); // Interrupting the actionReload should cause the next load to call all loaders let B = await t.navigate("/bar"); await B.loaders.root.resolve("ROOT*"); @@ -2628,7 +2612,7 @@ describe("a router", () => { formData: createFormData({ key: "value" }), }); await A.actions.foo.redirect("/baz"); - expect(t.router.state.navigation.type).toBe("submissionRedirect"); + expect(t.router.state.navigation.state).toBe("loading"); // Interrupting the submissionRedirect should cause the next load to call all loaders let B = await t.navigate("/bar"); await B.loaders.root.resolve("ROOT*"); @@ -2835,7 +2819,6 @@ describe("a router", () => { initialized: false, navigation: { state: "loading", - type: "normalLoad", location: { pathname: "/child" }, }, }); @@ -2849,7 +2832,6 @@ describe("a router", () => { initialized: false, navigation: { state: "loading", - type: "normalLoad", location: { pathname: "/child" }, }, }); @@ -2910,7 +2892,6 @@ describe("a router", () => { initialized: false, navigation: { state: "loading", - type: "normalLoad", }, }); expect(router.state.loaderData).toEqual({}); @@ -3018,7 +2999,6 @@ describe("a router", () => { initialized: false, navigation: { state: "loading", - type: "normalLoad", location: { pathname: "/child" }, }, }); @@ -3032,7 +3012,6 @@ describe("a router", () => { initialized: false, navigation: { state: "loading", - type: "normalLoad", location: { pathname: "/child" }, }, }); @@ -3047,7 +3026,6 @@ describe("a router", () => { initialized: false, navigation: { state: "loading", - type: "normalLoad", location: { pathname: "/child2" }, }, }); @@ -3127,7 +3105,6 @@ describe("a router", () => { pathname: "/tasks", }, state: "loading", - type: "normalLoad", }, loaderData: { root: "ROOT_DATA", @@ -3192,7 +3169,6 @@ describe("a router", () => { pathname: "/tasks", }, state: "loading", - type: "normalLoad", }, loaderData: { root: "ROOT_DATA", @@ -3243,7 +3219,6 @@ describe("a router", () => { pathname: "/tasks", }, state: "loading", - type: "normalLoad", }, loaderData: { root: "ROOT_DATA", @@ -3412,7 +3387,7 @@ describe("a router", () => { let historySpy = jest.spyOn(t.history, "push"); let nav = await t.navigate("/tasks"); - expect(t.router.state.navigation.type).toEqual("normalLoad"); + expect(t.router.state.navigation.state).toEqual("loading"); expect(t.router.state.location.pathname).toEqual("/"); expect(nav.loaders.tasks.signal.aborted).toBe(false); expect(t.history.action).toEqual("POP"); @@ -3420,7 +3395,7 @@ describe("a router", () => { // Interrupt and confirm prior loader was aborted let nav2 = await t.navigate("/tasks/1"); - expect(t.router.state.navigation.type).toEqual("normalLoad"); + expect(t.router.state.navigation.state).toEqual("loading"); expect(t.router.state.location.pathname).toEqual("/"); expect(nav.loaders.tasks.signal.aborted).toBe(true); expect(t.history.action).toEqual("POP"); @@ -3483,7 +3458,6 @@ describe("a router", () => { pathname: "/tasks", }, state: "loading", - type: "normalLoad", }, loaderData: { root: "ROOT_DATA", @@ -3507,7 +3481,6 @@ describe("a router", () => { pathname: "/tasks/1", }, state: "loading", - type: "normalRedirect", }, loaderData: { root: "ROOT_DATA", @@ -3557,7 +3530,6 @@ describe("a router", () => { pathname: "/tasks", }, state: "loading", - type: "normalLoad", }, loaderData: { root: "ROOT_DATA", @@ -3581,7 +3553,6 @@ describe("a router", () => { pathname: "/tasks/1", }, state: "loading", - type: "normalRedirect", }, loaderData: { root: "ROOT_DATA", @@ -4562,7 +4533,6 @@ describe("a router", () => { location: { pathname: "/" }, navigation: { state: "loading", - type: "normalRedirect", }, revalidation: "loading", loaderData: { @@ -5085,7 +5055,7 @@ describe("a router", () => { let fetcher = A.fetcher; await A.loaders.foo.redirect("/bar"); expect(t.router.getFetcher(A.key)).toBe(fetcher); - expect(t.router.state.navigation.type).toBe("normalRedirect"); + expect(t.router.state.navigation.state).toBe("loading"); expect(t.router.state.navigation.location?.pathname).toBe("/bar"); }); @@ -5098,7 +5068,7 @@ describe("a router", () => { let fetcher = A.fetcher; await A.loaders.foo.redirect("/bar"); expect(t.router.getFetcher(A.key)).toBe(fetcher); - expect(t.router.state.navigation.type).toBe("normalRedirect"); + expect(t.router.state.navigation.state).toBe("loading"); expect(t.router.state.navigation.location?.pathname).toBe("/bar"); }); @@ -5114,7 +5084,7 @@ describe("a router", () => { expect(A.fetcher.state).toBe("submitting"); let AR = await A.actions.foo.redirect("/bar"); expect(A.fetcher.state).toBe("loading"); - expect(t.router.state.navigation.type).toBe("submissionRedirect"); + expect(t.router.state.navigation.state).toBe("loading"); expect(t.router.state.navigation.location?.pathname).toBe("/bar"); await AR.loaders.root.resolve("ROOT*"); await AR.loaders.bar.resolve("stuff"); @@ -5498,12 +5468,12 @@ describe("a router", () => { }); let B = await t.navigate("/foo"); expect(A.actions.foo.signal.aborted).toBe(false); - expect(t.router.state.navigation.type).toBe("normalLoad"); + expect(t.router.state.navigation.state).toBe("loading"); expect(t.router.state.navigation.location?.pathname).toBe("/foo"); await B.loaders.root.resolve("B root"); await B.loaders.foo.resolve("B"); - expect(t.router.state.navigation.type).toBe("idle"); + expect(t.router.state.navigation.state).toBe("idle"); expect(t.router.state.location.pathname).toBe("/foo"); expect(t.router.state.loaderData.foo).toBe("B"); expect(A.loaders.foo.signal).toBe(undefined); // A loaders not called yet @@ -5541,7 +5511,7 @@ describe("a router", () => { await B.loaders.foo.resolve("B"); expect(A.actions.foo.signal.aborted).toBe(false); expect(A.loaders.foo.signal.aborted).toBe(false); - expect(t.router.state.navigation.type).toBe("idle"); + expect(t.router.state.navigation.state).toBe("idle"); expect(t.router.state.location.pathname).toBe("/foo"); expect(t.router.state.loaderData.foo).toBe("B"); @@ -5572,7 +5542,7 @@ describe("a router", () => { let B = await t.navigate("/foo"); await B.loaders.root.resolve("ROOT*"); await B.loaders.foo.resolve("B"); - expect(t.router.state.navigation.type).toBe("idle"); + expect(t.router.state.navigation.state).toBe("idle"); expect(t.router.state.location.pathname).toBe("/foo"); expect(t.router.state.loaderData).toEqual({ root: "ROOT*", diff --git a/packages/router/router.ts b/packages/router/router.ts index ee5e0526e7..cde73bcb71 100644 --- a/packages/router/router.ts +++ b/packages/router/router.ts @@ -2,13 +2,11 @@ import { History, Location, parsePath, To } from "./history"; import { Action as HistoryAction, createLocation } from "./history"; import { - ActionFormMethod, ActionSubmission, DataRouteObject, FormEncType, FormMethod, invariant, - LoaderFormMethod, RouteMatch, RouteObject, Submission, @@ -260,7 +258,6 @@ export type NavigateOptions = LinkNavigateOptions | SubmissionNavigateOptions; export type NavigationStates = { Idle: { state: "idle"; - type: "idle"; location: undefined; formMethod: undefined; formAction: undefined; @@ -269,58 +266,20 @@ export type NavigationStates = { }; Loading: { state: "loading"; - type: "normalLoad"; location: Location; - formMethod: undefined; - formAction: undefined; - formEncType: undefined; - formData: undefined; - }; - LoadingRedirect: { - state: "loading"; - type: "normalRedirect"; - location: Location; - formMethod: undefined; - formAction: undefined; - formEncType: undefined; - formData: undefined; + formMethod: FormMethod | undefined; + formAction: string | undefined; + formEncType: FormEncType | undefined; + formData: FormData | undefined; }; - SubmittingLoader: { + Submitting: { state: "submitting"; - type: "loaderSubmission"; - location: Location; - formMethod: LoaderFormMethod; - formAction: string; - formEncType: "application/x-www-form-urlencoded"; - formData: FormData; - }; - SubmissionRedirect: { - state: "loading"; - type: "submissionRedirect"; location: Location; formMethod: FormMethod; formAction: string; formEncType: FormEncType; formData: FormData; }; - SubmittingAction: { - state: "submitting"; - type: "actionSubmission"; - location: Location; - formMethod: ActionFormMethod; - formAction: string; - formEncType: FormEncType; - formData: FormData; - }; - LoadingAction: { - state: "loading"; - type: "actionReload"; - location: Location; - formMethod: ActionFormMethod; - formAction: string; - formEncType: FormEncType; - formData: FormData; - }; }; export type Navigation = NavigationStates[keyof NavigationStates]; @@ -433,7 +392,6 @@ interface HandleLoadersResult extends ShortCircuitable { export const IDLE_NAVIGATION: NavigationStates["Idle"] = { state: "idle", location: undefined, - type: "idle", formMethod: undefined, formAction: undefined, formEncType: undefined, @@ -610,13 +568,24 @@ export function createRouter(init: RouterInit): Router { location: Location, newState: Partial> ): void { + // Deduce if we're in a loading/actionReload state: + // - We have committed actionData in the store + // - The current navigation was a submission + // - We're past the submitting state and into the loading state + // - This should not be susceptible to false positives for + // loading/submissionRedirect since there would not be actionData in the + // state since the prior action would have returned a redirect response + // and short circuited + let isActionReload = + state.actionData != null && + state.navigation.formMethod != null && + state.navigation.state === "loading"; + updateState({ // Clear existing actionData on any completed navigation beyond the original - // action. Do this prior to spreading in newState in case we've gotten back - // to back actions - ...(state.actionData != null && state.navigation.type !== "actionReload" - ? { actionData: null } - : {}), + // action, unless we're currently finishing the loading/actionReload state. + // Do this prior to spreading in newState in case we got back to back actions + ...(isActionReload ? {} : { actionData: null }), ...newState, historyAction, location, @@ -683,8 +652,6 @@ export function createRouter(init: RouterInit): Router { // is interrupted by a navigation, allow this to "succeed" by calling all // loaders during the next loader round function revalidate() { - let { state: navigationState, type } = state.navigation; - // Toggle isRevalidationRequired so the next data load will call all loaders, // and mark us in a revalidating state isRevalidationRequired = true; @@ -692,7 +659,10 @@ export function createRouter(init: RouterInit): Router { // If we're currently submitting an action, we don't need to start a new // navigation, we'll just let the follow up loader execution call all loaders - if (navigationState === "submitting" && type === "actionSubmission") { + if ( + state.navigation.state === "submitting" && + state.navigation.formMethod !== "get" + ) { return; } @@ -779,12 +749,12 @@ export function createRouter(init: RouterInit): Router { pendingActionData = actionOutput.pendingActionData || null; pendingActionError = actionOutput.pendingActionError || null; - loadingNavigation = { + let navigation: NavigationStates["Loading"] = { state: "loading", - type: "actionReload", location, ...opts.submission, - } as NavigationStates["LoadingAction"]; + }; + loadingNavigation = navigation; } // Call loaders @@ -830,9 +800,8 @@ export function createRouter(init: RouterInit): Router { } // Put us in a submitting state - let navigation: NavigationStates["SubmittingAction"] = { + let navigation: NavigationStates["Submitting"] = { state: "submitting", - type: "actionSubmission", location, ...submission, }; @@ -878,9 +847,8 @@ export function createRouter(init: RouterInit): Router { // If the action threw a redirect Response, start a new REPLACE navigation if (isRedirectResult(result)) { - let redirectNavigation: NavigationStates["SubmissionRedirect"] = { + let redirectNavigation: NavigationStates["Loading"] = { state: "loading", - type: "submissionRedirect", location: createLocation(state.location, result.location), ...submission, }; @@ -919,22 +887,22 @@ export function createRouter(init: RouterInit): Router { if (overrideNavigation) { loadingNavigation = overrideNavigation; } else if (submission?.formMethod === "get") { - loadingNavigation = { + let navigation: NavigationStates["Submitting"] = { state: "submitting", - type: "loaderSubmission", location, ...submission, - } as NavigationStates["SubmittingLoader"]; + }; + loadingNavigation = navigation; } else { - loadingNavigation = { + let navigation: NavigationStates["Loading"] = { state: "loading", - type: "normalLoad", location, formMethod: undefined, formAction: undefined, formEncType: undefined, formData: undefined, - } as NavigationStates["Loading"]; + }; + loadingNavigation = navigation; } let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad( @@ -1150,9 +1118,8 @@ export function createRouter(init: RouterInit): Router { state.fetchers.set(key, loadingFetcher); updateState({ fetchers: new Map(state.fetchers) }); - let redirectNavigation: NavigationStates["SubmissionRedirect"] = { + let redirectNavigation: NavigationStates["Loading"] = { state: "loading", - type: "submissionRedirect", location: createLocation(state.location, actionResult.location), ...submission, }; @@ -1177,7 +1144,7 @@ export function createRouter(init: RouterInit): Router { // in the middle of a navigation let nextLocation = state.navigation.location || state.location; let matches = - state.navigation.type !== "idle" + state.navigation.state !== "idle" ? matchRoutes(dataRoutes, state.navigation.location) : state.matches; @@ -1552,34 +1519,16 @@ function getLoaderRedirect( state: RouterState, redirect: RedirectResult ): Navigation { - let redirectLocation = createLocation(state.location, redirect.location); - if ( - state.navigation.type === "loaderSubmission" || - state.navigation.type === "actionReload" - ) { - let { formMethod, formAction, formEncType, formData } = state.navigation; - let navigation: NavigationStates["SubmissionRedirect"] = { - state: "loading", - type: "submissionRedirect", - location: redirectLocation, - formMethod, - formAction, - formEncType, - formData, - }; - return navigation; - } else { - let navigation: NavigationStates["LoadingRedirect"] = { - state: "loading", - type: "normalRedirect", - location: redirectLocation, - formMethod: undefined, - formAction: undefined, - formEncType: undefined, - formData: undefined, - }; - return navigation; - } + let { formMethod, formAction, formEncType, formData } = state.navigation; + let navigation: NavigationStates["Loading"] = { + state: "loading", + location: createLocation(state.location, redirect.location), + formMethod: formMethod || undefined, + formAction: formAction || undefined, + formEncType: formEncType || undefined, + formData: formData || undefined, + }; + return navigation; } function getMatchesToLoad( diff --git a/packages/router/utils.ts b/packages/router/utils.ts index 1a7c24f5b6..ed9ade638c 100644 --- a/packages/router/utils.ts +++ b/packages/router/utils.ts @@ -2,9 +2,7 @@ import type { Location, Path, To } from "./history"; import { parsePath } from "./history"; import { DataResult, DataRouteMatch } from "./router"; -export type LoaderFormMethod = "get"; -export type ActionFormMethod = "post" | "put" | "patch" | "delete"; -export type FormMethod = LoaderFormMethod | ActionFormMethod; +export type FormMethod = "get" | "post" | "put" | "patch" | "delete"; export type FormEncType = "application/x-www-form-urlencoded"; /** @@ -21,7 +19,7 @@ export interface Submission { * Narrowed type enforcing a non-GET method */ export interface ActionSubmission extends Submission { - formMethod: ActionFormMethod; + formMethod: Exclude; } /** From a8a3f7c7bf4f93ae7ea8cfa91f953b5d6463fc6c Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Sat, 21 May 2022 08:37:50 -0600 Subject: [PATCH 092/119] docs docs docs --- docs/getting-started/concepts.md | 2 +- docs/getting-started/data.md | 9 ++ docs/getting-started/faq.md | 2 +- docs/getting-started/tutorial.md | 2 +- docs/guides/code-splitting.md | 8 + docs/guides/form-data.md | 18 +++ docs/guides/index-route.md | 1 + docs/route/action.md | 140 ++++++++++++++++- docs/route/error-element.md | 248 ++++++++++++++++++++----------- docs/route/loader.md | 2 +- docs/route/route.md | 2 +- 11 files changed, 340 insertions(+), 94 deletions(-) create mode 100644 docs/getting-started/data.md create mode 100644 docs/guides/code-splitting.md create mode 100644 docs/guides/form-data.md diff --git a/docs/getting-started/concepts.md b/docs/getting-started/concepts.md index 35b8a18e5a..2128e9b444 100644 --- a/docs/getting-started/concepts.md +++ b/docs/getting-started/concepts.md @@ -1,6 +1,6 @@ --- title: Main Concepts -order: 4 +order: 5 --- # Main Concepts diff --git a/docs/getting-started/data.md b/docs/getting-started/data.md new file mode 100644 index 0000000000..f034db16b4 --- /dev/null +++ b/docs/getting-started/data.md @@ -0,0 +1,9 @@ +--- +title: Data Quick Start +new: true +order: 3 +--- + +# Data APIs Quick Start + +TODO: This doc is a stub diff --git a/docs/getting-started/faq.md b/docs/getting-started/faq.md index c7043a878c..b1ffbf57fc 100644 --- a/docs/getting-started/faq.md +++ b/docs/getting-started/faq.md @@ -1,6 +1,6 @@ --- title: FAQs -order: 3 +order: 4 --- # FAQs diff --git a/docs/getting-started/tutorial.md b/docs/getting-started/tutorial.md index c9bfff33b5..defa6c6bd1 100644 --- a/docs/getting-started/tutorial.md +++ b/docs/getting-started/tutorial.md @@ -1,6 +1,6 @@ --- title: Tutorial -order: 2 +order: 3 --- # Tutorial diff --git a/docs/guides/code-splitting.md b/docs/guides/code-splitting.md new file mode 100644 index 0000000000..3dd9305fec --- /dev/null +++ b/docs/guides/code-splitting.md @@ -0,0 +1,8 @@ +--- +title: Code Splitting +new: true +--- + +# Code Splitting + +TODO: This doc is a stub diff --git a/docs/guides/form-data.md b/docs/guides/form-data.md new file mode 100644 index 0000000000..e1a283cb5b --- /dev/null +++ b/docs/guides/form-data.md @@ -0,0 +1,18 @@ +--- +title: Working With FormData +new: true +--- + +# Working With FormData + +TODO: This document is a stub + +A common trick is to turn the entire formData into an object with [`Object.fromEntries`][object-fromentries]: + +```tsx +const data = Object.fromEntries(await request.formData()); +data.songTitle; +data.lyrics; +``` + +[object-fromentries]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries diff --git a/docs/guides/index-route.md b/docs/guides/index-route.md index 11e3bf2c89..c0fcd81b48 100644 --- a/docs/guides/index-route.md +++ b/docs/guides/index-route.md @@ -1,5 +1,6 @@ --- title: Index Route +new: true --- # Index Route diff --git a/docs/route/action.md b/docs/route/action.md index 444c5e56d5..3554d7cbb2 100644 --- a/docs/route/action.md +++ b/docs/route/action.md @@ -3,4 +3,142 @@ title: action new: true --- -# Route Action +# `action` + +Route actions are the "writes" to route [loader][loader] "reads". They provide a way for apps to perform data mutations with simple HTML and HTTP semantics while React Router abstracts away the complexity of asynchronous UI and revalidation. This gives you the simple mental model of HTML + HTTP (where the browser handles the asynchrony and revalidation) with the behavior and and UX capabilities of modern SPAs. + +```tsx +} + action={async ({ params, request }) => { + let formData = await request.formData(); + return fakeUpdateSong(params.songId, formData); + }} + loader={({ params }) => { + return fakeGetSong(params.songId); + }} +/> +``` + +Actions are called whenever the app sends a non-get submission ("post", "put", "patch", "delete") to your route. This can happen in a few ways: + +```tsx +// forms +
; +; + +// imperative submissions +let submit = useSubmit(); +submit(data, { + method: "delete", + action: "/songs/123", +}); +fetcher.submit(data, { + method: "patch", + action: "/songs/123/edit", +}); +``` + +## `params` + +Route params are parsed from [dynamic segments][dynamicsegments] and passed to your loader. This is useful for figuring out which resource to mutate: + +```tsx + { + return fakeDeleteProject(params.teamId); + }} +/> +``` + +## `request` + +This is a [Fetch Request][request] instance being sent to your route. The most common use case is to parse the [FormData][formdata] from the request + +```tsx + { + let formData = await request.formData(); + // ... + }} +/> +``` + +> A Request?! + +It might seem odd at first that actions receive a "request". Have you ever written this line of code? + +```tsx [3] + { + event.preventDefault(); + // ... + }} +/> +``` + +What exactly are you preventing? + +Without JavaScript, just plain HTML and an HTTP web server, that default event that was prevented is actually pretty great. Browsers will serialize all the data in the form into [`FormData`][formdata] and send it as the body of a new request to your server. Like the code above, React Router [``][form] prevents the browser from sending that request and instead sends the request to your route action! This enables highly dynamic web apps with the simple model of HTML and HTTP. + +Remember that the values in the `formData` are automatically serialized from the form submission, so your inputs need a `name`. + +```tsx + + +