From 9640d014a6d68bbee909f0e947c3def252de59be Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Wed, 11 Jan 2023 15:53:18 -0500 Subject: [PATCH 1/3] ci(release): sync with remix (#9813) * ci(release): sync with remix * ci: sync with remix * Delete postrelease.yml * ci: sync latest changes around getting previous release (cherry picked from commit a80a62d345e21194b2ef743b26c2611f45a69f4c) --- .github/workflows/postrelease.yml | 17 ----- .github/workflows/release.yml | 71 +++++++++++++------ scripts/release/comment.ts | 41 ++++------- scripts/release/constants.ts | 3 +- .../release/find-release-from-changeset.js | 38 ++++++++++ scripts/release/github.ts | 70 ++++++++++++------ scripts/release/utils.ts | 6 ++ 7 files changed, 159 insertions(+), 87 deletions(-) delete mode 100644 .github/workflows/postrelease.yml create mode 100644 scripts/release/find-release-from-changeset.js diff --git a/.github/workflows/postrelease.yml b/.github/workflows/postrelease.yml deleted file mode 100644 index f865eebcda..0000000000 --- a/.github/workflows/postrelease.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: 🕊 Post-release - -on: - push: - tags: - # only run on `react-router` tags - - "react-router@*" - -jobs: - comment: - name: 📝 Comment on related issues and pull requests - if: github.repository == 'remix-run/react-router' - uses: ./.github/workflows/release-comments.yml - with: - ref: ${{ github.ref }} - # this should match the above tag to watch excluding the trailing "@" - packageVersionToFollow: "react-router" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 46f630529a..bcc2ac404b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: 🕊 Release +name: 🦋 Changesets Release on: push: branches: @@ -6,34 +6,34 @@ on: - "release-*" - "!release-experimental" - "!release-experimental-*" - -concurrency: ${{ github.workflow }}-${{ github.ref }} - -env: - CI: true + - "!release-manual" + - "!release-manual-*" jobs: release: name: 🦋 Changesets Release if: github.repository == 'remix-run/react-router' runs-on: ubuntu-latest - + outputs: + publishedPackages: ${{ steps.changesets.outputs.publishedPackages }} + published: ${{ steps.changesets.outputs.published }} steps: + - name: 🛑 Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.11.0 + - name: ⬇️ Checkout repo uses: actions/checkout@v3 with: fetch-depth: 0 - - name: ⎔ Setup Node + - name: ⎔ Setup node uses: actions/setup-node@v3 with: node-version-file: ".nvmrc" - cache: yarn + cache: "yarn" - - name: 📥 Install dependencies - # even though this is called "npm-install" it does use yarn to install - # because we have a yarn.lock and caches efficiently. - uses: bahmutov/npm-install@v1 + - name: 📥 Install deps + run: yarn --frozen-lockfile - name: 🔐 Setup npm auth run: | @@ -52,16 +52,45 @@ jobs: version: yarn run version commit: "chore: Update version for release" title: "chore: Update version for release" - publish: yarn release + publish: yarn run release createGithubReleases: false env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN_SO_OTHER_ACTIONS_RUN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - # comment: - # needs: [release] - # name: 📝 Comment on related issues and pull requests - # if: github.repository == 'remix-run/react-router' - # uses: remix-run/react-router/.github/workflows/release-comments.yml@main - # with: - # ref: ${{ github.ref }} + findPackage: + name: 🦋 Find Package + needs: [release] + runs-on: ubuntu-latest + if: github.repository == 'remix-run/react-router' && needs.release.outputs.published == 'true' + outputs: + package: ${{ steps.findPackage.outputs.package }} + steps: + - name: 🛑 Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.11.0 + + - name: ⬇️ Checkout repo + uses: actions/checkout@v3 + + - name: ⎔ Setup node + uses: actions/setup-node@v3 + with: + node-version: 16 + cache: "npm" + + - id: findPackage + run: | + package=$(node ./scripts/release/find-release-from-changeset.js) + echo "package=${package}" >> $GITHUB_OUTPUT + env: + packageVersionToFollow: "react-router" + publishedPackages: ${{ needs.release.outputs.publishedPackages }} + + comment: + name: 📝 Comment on related issues and pull requests + if: github.repository == 'remix-run/react-router' && needs.findPackage.outputs.package != '' + needs: [release, findPackage] + uses: ./.github/workflows/release-comments.yml + with: + ref: refs/tags/${{ needs.findPackage.outputs.package }} + packageVersionToFollow: "react-router" diff --git a/scripts/release/comment.ts b/scripts/release/comment.ts index f8ef8f5f2e..84aad5e789 100644 --- a/scripts/release/comment.ts +++ b/scripts/release/comment.ts @@ -3,7 +3,7 @@ import { OWNER, REPO, PR_FILES_STARTS_WITH, - IS_NIGHTLY_RELEASE, + IS_STABLE_RELEASE, AWAITING_RELEASE_LABEL, } from "./constants"; import { @@ -53,7 +53,7 @@ async function commentOnIssuesAndPrsAboutRelease() { let prLabels = pr.labels.map((label) => label.name); let prIsAwaitingRelease = prLabels.includes(AWAITING_RELEASE_LABEL); - if (!IS_NIGHTLY_RELEASE && prIsAwaitingRelease) { + if (IS_STABLE_RELEASE && prIsAwaitingRelease) { promises.push( removeLabel({ owner: OWNER, repo: REPO, issue: pr.number }) ); @@ -73,27 +73,19 @@ async function commentOnIssuesAndPrsAboutRelease() { issuesCommentedOn.add(issue.number); let issueUrl = getGitHubUrl("issue", issue.number); + console.log(`commenting on issue ${issueUrl}`); - if (IS_NIGHTLY_RELEASE || !prIsAwaitingRelease) { - console.log(`commenting on ${issueUrl}`); - promises.push( - commentOnIssue({ - owner: OWNER, - repo: REPO, - issue: issue.number, - version: VERSION, - }) - ); - } else { - console.log(`commenting on and closing ${issueUrl}`); - promises.push( - commentOnIssue({ - owner: OWNER, - repo: REPO, - issue: issue.number, - version: VERSION, - }) - ); + promises.push( + commentOnIssue({ + owner: OWNER, + repo: REPO, + issue: issue.number, + version: VERSION, + }) + ); + + if (IS_STABLE_RELEASE) { + console.log(`closing issue ${issueUrl}`); promises.push( closeIssue({ owner: OWNER, repo: REPO, issue: issue.number }) ); @@ -104,10 +96,7 @@ async function commentOnIssuesAndPrsAboutRelease() { let result = await Promise.allSettled(promises); let rejected = result.filter((r) => r.status === "rejected"); if (rejected.length > 0) { - console.log( - "🚨 failed to comment on some issues/prs - the most likely reason is they were issues that were turned into discussions, which don't have an api to comment with" - ); - console.log(rejected); + console.error("🚨 failed to comment on some issues/prs", rejected); } } diff --git a/scripts/release/constants.ts b/scripts/release/constants.ts index d78288f362..68a58fca1c 100644 --- a/scripts/release/constants.ts +++ b/scripts/release/constants.ts @@ -1,4 +1,4 @@ -import { cleanupRef, cleanupTagName, isNightly } from "./utils"; +import { cleanupRef, cleanupTagName, isNightly, isStable } from "./utils"; if (!process.env.DEFAULT_BRANCH) { throw new Error("DEFAULT_BRANCH is required"); @@ -32,3 +32,4 @@ export const NIGHTLY_BRANCH = process.env.NIGHTLY_BRANCH; export const PR_FILES_STARTS_WITH = ["packages/"]; export const IS_NIGHTLY_RELEASE = isNightly(VERSION); export const AWAITING_RELEASE_LABEL = "awaiting release"; +export const IS_STABLE_RELEASE = isStable(VERSION); diff --git a/scripts/release/find-release-from-changeset.js b/scripts/release/find-release-from-changeset.js new file mode 100644 index 0000000000..ea0cb529e7 --- /dev/null +++ b/scripts/release/find-release-from-changeset.js @@ -0,0 +1,38 @@ +/** + * + * @param {string | undefined} publishedPackages + * @param {string | undefined} packageVersionToFollow + * @returns {string | undefined} + */ +function findReleaseFromChangeset(publishedPackages, packageVersionToFollow) { + if (!publishedPackages) { + throw new Error("No published packages found"); + } + + let packages = JSON.parse(publishedPackages); + + if (!Array.isArray(packages)) { + throw new Error("Published packages is not an array"); + } + + /** @see https://github.com/changesets/action#outputs */ + /** @type { { name: string; version: string }[] } */ + let typed = packages.filter((pkg) => "name" in pkg && "version" in pkg); + + let found = typed.find((pkg) => pkg.name === packageVersionToFollow); + + if (!found) { + throw new Error( + `${packageVersionToFollow} was not found in the published packages` + ); + } + + let result = `${found.name}@${found.version}`; + console.log(result); + return result; +} + +findReleaseFromChangeset( + process.env.publishedPackages, + process.env.packageVersionToFollow +); diff --git a/scripts/release/github.ts b/scripts/release/github.ts index 1b685e9654..565555ba1d 100644 --- a/scripts/release/github.ts +++ b/scripts/release/github.ts @@ -7,9 +7,12 @@ import { DEFAULT_BRANCH, PACKAGE_VERSION_TO_FOLLOW, AWAITING_RELEASE_LABEL, + IS_NIGHTLY_RELEASE, + IS_STABLE_RELEASE, } from "./constants"; import { gql, graphqlWithAuth, octokit } from "./octokit"; import type { MinimalTag } from "./utils"; +import { isNightly, isStable } from "./utils"; import { cleanupTagName } from "./utils"; import { checkIfStringStartsWith } from "./utils"; @@ -140,34 +143,32 @@ function getPreviousTagFromCurrentTag( return { tag: tagName, date, isPrerelease }; }) - .filter((v: any): v is MinimalTag => typeof v !== "undefined"); + .filter((v: any): v is MinimalTag => typeof v !== "undefined") + .filter((tag) => { + if (IS_STABLE_RELEASE) return isStable(tag.tag); + let isNightlyTag = isNightly(tag.tag); + if (IS_NIGHTLY_RELEASE) return isNightlyTag; + return !isNightlyTag; + }) + .sort((a, b) => { + if (IS_NIGHTLY_RELEASE) { + return b.date.getTime() - a.date.getTime(); + } + + return semver.rcompare(a.tag, b.tag); + }); let currentTagIndex = validTags.findIndex((tag) => tag.tag === currentTag); let currentTagInfo: MinimalTag | undefined = validTags.at(currentTagIndex); let previousTagInfo: MinimalTag | undefined; if (!currentTagInfo) { - throw new Error(`Could not find last tag ${currentTag}`); - } - - // if the currentTag was a stable tag, then we want to find the previous stable tag - if (!currentTagInfo.isPrerelease) { - validTags = validTags - .filter((tag) => !tag.isPrerelease) - .sort((a, b) => semver.rcompare(a.tag, b.tag)); - - currentTagIndex = validTags.findIndex((tag) => tag.tag === currentTag); - currentTagInfo = validTags.at(currentTagIndex); - if (!currentTagInfo) { - throw new Error(`Could not find last stable tag ${currentTag}`); - } + throw new Error(`Could not find tag ${currentTag}`); } previousTagInfo = validTags.at(currentTagIndex + 1); if (!previousTagInfo) { - throw new Error( - `Could not find previous prerelease tag from ${currentTag}` - ); + throw new Error(`Could not find previous tag from ${currentTag}`); } return { @@ -232,21 +233,35 @@ interface GitHubGraphqlTag { interface GitHubGraphqlTagResponse { repository: { refs: { + pageInfo: { + hasNextPage: boolean; + endCursor: string; + }; nodes: Array; }; }; } -async function getTags(owner: string, repo: string) { +async function getTags( + owner: string, + repo: string, + endCursor?: string, + nodes: Array = [] +): Promise { let response: GitHubGraphqlTagResponse = await graphqlWithAuth( gql` - query GET_TAGS($owner: String!, $repo: String!) { + query GET_TAGS($owner: String!, $repo: String!, $endCursor: String) { repository(owner: $owner, name: $repo) { refs( refPrefix: "refs/tags/" first: 100 orderBy: { field: TAG_COMMIT_DATE, direction: DESC } + after: $endCursor ) { + pageInfo { + hasNextPage + endCursor + } nodes { name target { @@ -267,15 +282,26 @@ async function getTags(owner: string, repo: string) { } } `, - { owner, repo } + { owner, repo, endCursor } ); - return response.repository.refs.nodes.filter((node) => { + let filtered = response.repository.refs.nodes.filter((node) => { return ( node.name.startsWith(PACKAGE_VERSION_TO_FOLLOW) || node.name.startsWith("v0.0.0-nightly-") ); }); + + if (response.repository.refs.pageInfo.hasNextPage) { + console.log("has next page", response.repository.refs.pageInfo.endCursor); + + return getTags(owner, repo, response.repository.refs.pageInfo.endCursor, [ + ...nodes, + ...filtered, + ]); + } + + return [...nodes, ...filtered]; } export async function getIssuesClosedByPullRequests( diff --git a/scripts/release/utils.ts b/scripts/release/utils.ts index ea6a86f7b4..a84a60673c 100644 --- a/scripts/release/utils.ts +++ b/scripts/release/utils.ts @@ -1,3 +1,5 @@ +import * as semver from "semver"; + import { GITHUB_REPOSITORY, PACKAGE_VERSION_TO_FOLLOW } from "./constants"; export function checkIfStringStartsWith( @@ -34,3 +36,7 @@ export function cleanupRef(ref: string) { export function isNightly(tagName: string) { return tagName.startsWith("v0.0.0-nightly-"); } + +export function isStable(tagName: string) { + return semver.prerelease(tagName) === null; +} From bb7590ac85feb0cb039a5aeac2e2874c117207c9 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 13 Jan 2023 13:22:11 -0800 Subject: [PATCH 2/3] `@remix-run/router`: Add support for navigation blocking (#9709) * feat(router): add support for history blocking APIs * feat(react-router): add `unstable_useBlocker` hook * feat(react-router-dom): add `capture` option to `useBeforeUnload` Co-authored-by: Matt Brophy --- .changeset/lazy-needles-shout.md | 5 + .changeset/light-phones-impress.md | 5 + .changeset/violet-timers-type.md | 5 + .gitignore | 3 +- examples/navigation-blocking/.gitignore | 5 + examples/navigation-blocking/.stackblitzrc | 4 + examples/navigation-blocking/README.md | 15 + examples/navigation-blocking/index.html | 12 + .../navigation-blocking/package-lock.json | 2456 +++++++++++++++++ examples/navigation-blocking/package.json | 23 + examples/navigation-blocking/src/app.tsx | 140 + examples/navigation-blocking/src/main.tsx | 9 + .../navigation-blocking/src/vite-env.d.ts | 1 + examples/navigation-blocking/tsconfig.json | 21 + examples/navigation-blocking/vite.config.ts | 36 + package.json | 5 +- .../__tests__/use-blocker-test.tsx | 1004 +++++++ packages/react-router-dom/index.tsx | 14 +- packages/react-router-dom/server.tsx | 7 + packages/react-router-native/index.tsx | 3 + packages/react-router/index.ts | 6 + packages/react-router/lib/hooks.tsx | 33 + .../router/__tests__/TestSequences/GoBack.ts | 1 + .../__tests__/TestSequences/GoForward.ts | 2 + .../__tests__/navigation-blocking-test.ts | 493 ++++ .../router/__tests__/router-memory-test.ts | 1 + packages/router/__tests__/router-test.ts | 2 + packages/router/history.ts | 68 +- packages/router/router.ts | 262 +- packages/router/utils.ts | 2 +- rollup.config.js | 10 +- 31 files changed, 4622 insertions(+), 31 deletions(-) create mode 100644 .changeset/lazy-needles-shout.md create mode 100644 .changeset/light-phones-impress.md create mode 100644 .changeset/violet-timers-type.md create mode 100644 examples/navigation-blocking/.gitignore create mode 100644 examples/navigation-blocking/.stackblitzrc create mode 100644 examples/navigation-blocking/README.md create mode 100644 examples/navigation-blocking/index.html create mode 100644 examples/navigation-blocking/package-lock.json create mode 100644 examples/navigation-blocking/package.json create mode 100644 examples/navigation-blocking/src/app.tsx create mode 100644 examples/navigation-blocking/src/main.tsx create mode 100644 examples/navigation-blocking/src/vite-env.d.ts create mode 100644 examples/navigation-blocking/tsconfig.json create mode 100644 examples/navigation-blocking/vite.config.ts create mode 100644 packages/react-router-dom/__tests__/use-blocker-test.tsx create mode 100644 packages/router/__tests__/navigation-blocking-test.ts diff --git a/.changeset/lazy-needles-shout.md b/.changeset/lazy-needles-shout.md new file mode 100644 index 0000000000..27bbb1dddb --- /dev/null +++ b/.changeset/lazy-needles-shout.md @@ -0,0 +1,5 @@ +--- +"react-router-dom": patch +--- + +Added pass-through event listener options argument to `useBeforeUnload` diff --git a/.changeset/light-phones-impress.md b/.changeset/light-phones-impress.md new file mode 100644 index 0000000000..2abce6d194 --- /dev/null +++ b/.changeset/light-phones-impress.md @@ -0,0 +1,5 @@ +--- +"@remix-run/router": minor +--- + +Added support for navigation blocking APIs diff --git a/.changeset/violet-timers-type.md b/.changeset/violet-timers-type.md new file mode 100644 index 0000000000..0f97aaab93 --- /dev/null +++ b/.changeset/violet-timers-type.md @@ -0,0 +1,5 @@ +--- +"react-router-dom": minor +--- + +Add `unstable_useBlocker` hook for blocking navigations within the app's location origin diff --git a/.gitignore b/.gitignore index 509667a660..1387c7fdff 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ node_modules/ /packages/react-router-dom-v5-compat/react-router-dom .eslintcache -/.env \ No newline at end of file +/.env +/NOTES.md diff --git a/examples/navigation-blocking/.gitignore b/examples/navigation-blocking/.gitignore new file mode 100644 index 0000000000..d451ff16c1 --- /dev/null +++ b/examples/navigation-blocking/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local diff --git a/examples/navigation-blocking/.stackblitzrc b/examples/navigation-blocking/.stackblitzrc new file mode 100644 index 0000000000..d98146f4d0 --- /dev/null +++ b/examples/navigation-blocking/.stackblitzrc @@ -0,0 +1,4 @@ +{ + "installDependencies": true, + "startCommand": "npm run dev" +} diff --git a/examples/navigation-blocking/README.md b/examples/navigation-blocking/README.md new file mode 100644 index 0000000000..f2875a1c51 --- /dev/null +++ b/examples/navigation-blocking/README.md @@ -0,0 +1,15 @@ +--- +title: Navigation Blocking +toc: false +order: 1 +--- + +# Navigation Blocking + +This example demonstrates using `unstable_useBlocker` to prevent navigating away from a page where you might lose user-entered form data. A potentially better UX for this is storing user-entered information in `sessionStorage` and pre-populating the form on return. + +## 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/navigation-blocking?file=src/App.tsx) diff --git a/examples/navigation-blocking/index.html b/examples/navigation-blocking/index.html new file mode 100644 index 0000000000..a8e66e86e0 --- /dev/null +++ b/examples/navigation-blocking/index.html @@ -0,0 +1,12 @@ + + + + + + React Router - Navigation Blocking + + +
+ + + diff --git a/examples/navigation-blocking/package-lock.json b/examples/navigation-blocking/package-lock.json new file mode 100644 index 0000000000..0ec340c526 --- /dev/null +++ b/examples/navigation-blocking/package-lock.json @@ -0,0 +1,2456 @@ +{ + "name": "navigation-blocking", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "navigation-blocking", + "dependencies": { + "react": "18.1.0", + "react-dom": "18.1.0", + "react-router-dom": "^6.6.2" + }, + "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/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/@remix-run/router": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.2.1.tgz", + "integrity": "sha512-XiY0IsyHR+DXYS5vBxpoBe/8veTeoRpMHP+vDosLZxL5bnpetzI0igkxkLZS235ldLzyfkxF+2divEwWHP3vMQ==", + "engines": { + "node": ">=14" + } + }, + "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/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.6.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.6.2.tgz", + "integrity": "sha512-uJPG55Pek3orClbURDvfljhqFvMgJRo59Pktywkk8hUUkTY2aRfza8Yhl/vZQXs+TNQyr6tu+uqz/fLxPICOGQ==", + "dependencies": { + "@remix-run/router": "1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.6.2.tgz", + "integrity": "sha512-6SCDXxRQqW5af8ImOqKza7icmQ47/EMbz572uFjzvcArg3lZ+04PxSPp8qGs+p2Y+q+b+S/AjXv8m8dyLndIIA==", + "dependencies": { + "@remix-run/router": "1.2.1", + "react-router": "6.6.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "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/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" + } + }, + "@remix-run/router": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.2.1.tgz", + "integrity": "sha512-XiY0IsyHR+DXYS5vBxpoBe/8veTeoRpMHP+vDosLZxL5bnpetzI0igkxkLZS235ldLzyfkxF+2divEwWHP3vMQ==" + }, + "@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 + }, + "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.6.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.6.2.tgz", + "integrity": "sha512-uJPG55Pek3orClbURDvfljhqFvMgJRo59Pktywkk8hUUkTY2aRfza8Yhl/vZQXs+TNQyr6tu+uqz/fLxPICOGQ==", + "requires": { + "@remix-run/router": "1.2.1" + } + }, + "react-router-dom": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.6.2.tgz", + "integrity": "sha512-6SCDXxRQqW5af8ImOqKza7icmQ47/EMbz572uFjzvcArg3lZ+04PxSPp8qGs+p2Y+q+b+S/AjXv8m8dyLndIIA==", + "requires": { + "@remix-run/router": "1.2.1", + "react-router": "6.6.2" + } + }, + "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/navigation-blocking/package.json b/examples/navigation-blocking/package.json new file mode 100644 index 0000000000..5c18774281 --- /dev/null +++ b/examples/navigation-blocking/package.json @@ -0,0 +1,23 @@ +{ + "name": "navigation-blocking", + "private": true, + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "serve": "vite preview" + }, + "dependencies": { + "react": "18.1.0", + "react-dom": "18.1.0", + "react-router-dom": "^6.6.2" + }, + "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/navigation-blocking/src/app.tsx b/examples/navigation-blocking/src/app.tsx new file mode 100644 index 0000000000..29d4457a93 --- /dev/null +++ b/examples/navigation-blocking/src/app.tsx @@ -0,0 +1,140 @@ +import React from "react"; +import type { unstable_Blocker as Blocker } from "react-router-dom"; +import { + createBrowserRouter, + createRoutesFromElements, + Form, + json, + Link, + Outlet, + Route, + RouterProvider, + unstable_useBlocker as useBlocker, + useLocation, +} from "react-router-dom"; + +let router = createBrowserRouter( + createRoutesFromElements( + }> + Index} /> + One} /> + Two} /> + json({ ok: true })} + element={ + <> +

Three

+ + + } + /> + Four} /> + + ) +); + +if (import.meta.hot) { + import.meta.hot.dispose(() => router.dispose()); +} + +export default function App() { + return ; +} + +function Layout() { + let [historyIndex, setHistoryIndex] = React.useState( + window.history.state?.idx + ); + let location = useLocation(); + + // Expose the underlying history index in the UI for debugging + React.useEffect(() => { + setHistoryIndex(window.history.state?.idx); + }, [location]); + + // Give us meaningful document titles for popping back/forward more than 1 entry + React.useEffect(() => { + document.title = location.pathname; + }, [location]); + + return ( + <> +

Navigation Blocking Example

+ +

+ Current location (index): {location.pathname} ({historyIndex}) +

+ + + ); +} + +function ImportantForm() { + let [value, setValue] = React.useState(""); + let isBlocked = value !== ""; + let blocker = useBlocker(isBlocked); + + // Reset the blocker if the user cleans the form + React.useEffect(() => { + if (blocker.state === "blocked" && !isBlocked) { + blocker.reset(); + } + }, [blocker, isBlocked]); + + return ( + <> +

+ Is the form dirty?{" "} + {isBlocked ? ( + Yes + ) : ( + No + )} +

+ +
+ + +
+ + {blocker ? : null} + + ); +} + +function ConfirmNavigation({ blocker }: { blocker: Blocker }) { + if (blocker.state === "blocked") { + return ( + <> +

+ Blocked the last navigation to {blocker.location.pathname} +

+ + + + ); + } + + if (blocker.state === "proceeding") { + return ( +

Proceeding through blocked navigation

+ ); + } + + return

Blocker is currently unblocked

; +} diff --git a/examples/navigation-blocking/src/main.tsx b/examples/navigation-blocking/src/main.tsx new file mode 100644 index 0000000000..32a669c16c --- /dev/null +++ b/examples/navigation-blocking/src/main.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./app"; + +ReactDOM.createRoot(document.getElementById("root")).render( + + + +); diff --git a/examples/navigation-blocking/src/vite-env.d.ts b/examples/navigation-blocking/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/navigation-blocking/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/navigation-blocking/tsconfig.json b/examples/navigation-blocking/tsconfig.json new file mode 100644 index 0000000000..8bdaabfe5d --- /dev/null +++ b/examples/navigation-blocking/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/navigation-blocking/vite.config.ts b/examples/navigation-blocking/vite.config.ts new file mode 100644 index 0000000000..b77eb48a30 --- /dev/null +++ b/examples/navigation-blocking/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/package.json b/package.json index 037d859131..3cb029f761 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "release": "changeset publish", "size": "filesize", "test": "jest", + "test:inspect": "node --inspect-brk ./node_modules/.bin/jest", "changeset": "changeset", "version": "changeset version", "postversion": "node scripts/postversion.mjs", @@ -108,10 +109,10 @@ }, "filesize": { "packages/router/dist/router.umd.min.js": { - "none": "38.5 kB" + "none": "41 kB" }, "packages/react-router/dist/react-router.production.min.js": { - "none": "12.5 kB" + "none": "13 kB" }, "packages/react-router/dist/umd/react-router.production.min.js": { "none": "15 kB" diff --git a/packages/react-router-dom/__tests__/use-blocker-test.tsx b/packages/react-router-dom/__tests__/use-blocker-test.tsx new file mode 100644 index 0000000000..417540729c --- /dev/null +++ b/packages/react-router-dom/__tests__/use-blocker-test.tsx @@ -0,0 +1,1004 @@ +import * as React from "react"; +import * as ReactDOM from "react-dom/client"; +import { act } from "react-dom/test-utils"; +import type { unstable_Blocker as Blocker, RouteObject } from "../index"; +import { + createMemoryRouter, + json, + NavLink, + Outlet, + RouterProvider, + unstable_useBlocker as useBlocker, + useNavigate, +} from "../index"; + +type Router = ReturnType; + +const LOADER_LATENCY_MS = 100; + +async function slowLoader() { + await sleep(LOADER_LATENCY_MS); + return json(null); +} + +describe("navigation blocking with useBlocker", () => { + let node: HTMLDivElement; + let router: Router; + let blocker: Blocker | null = null; + let root: ReactDOM.Root; + + beforeEach(() => { + node = document.createElement("div"); + document.body.appendChild(node); + }); + + afterEach(() => { + document.body.removeChild(node); + node = null!; + }); + + it("initializes an 'unblocked' blocker", async () => { + let initialEntries = ["/"]; + let routes: RouteObject[] = [ + { + path: "/", + element: React.createElement(() => { + let b = useBlocker(false); + blocker = b; + return null; + }), + }, + ]; + router = createMemoryRouter(routes, { initialEntries }); + act(() => { + root = ReactDOM.createRoot(node); + root.render(); + }); + expect(blocker).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + }); + act(() => { + root.unmount(); + }); + }); + + describe("on navigation", () => { + describe("blocker returns false", () => { + beforeEach(() => { + let initialEntries = ["/"]; + let initialIndex = 0; + router = createMemoryRouter( + [ + { + element: React.createElement(() => { + let b = useBlocker(false); + blocker = b; + return ( +
+ Home + About + +
+ ); + }), + children: [ + { + path: "/", + element:

Home

, + }, + { + path: "/about", + element:

About

, + loader: slowLoader, + }, + ], + }, + ], + { + initialEntries, + initialIndex, + } + ); + act(() => { + root = ReactDOM.createRoot(node); + root.render(); + }); + }); + + afterEach(() => { + act(() => root.unmount()); + }); + + it("navigates", async () => { + await act(async () => { + click(node.querySelector("a[href='/about']")); + await sleep(LOADER_LATENCY_MS); + }); + let h1 = node.querySelector("h1"); + expect(h1?.textContent).toBe("About"); + }); + + it("gets an 'unblocked' blocker after navigation starts", async () => { + act(() => { + click(node.querySelector("a[href='/about']")); + }); + expect(blocker).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, + }); + }); + + it("gets an 'unblocked' blocker after navigation completes", async () => { + await act(async () => { + click(node.querySelector("a[href='/about']")); + await sleep(LOADER_LATENCY_MS); + }); + expect(blocker).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, + }); + }); + }); + + describe("blocker returns true", () => { + beforeEach(() => { + let initialEntries = ["/"]; + let initialIndex = 0; + router = createMemoryRouter( + [ + { + element: React.createElement(() => { + let b = useBlocker(true); + blocker = b; + return ( +
+ Home + About + +
+ ); + }), + children: [ + { + path: "/", + element:

Home

, + }, + { + path: "/about", + element:

About

, + loader: slowLoader, + }, + ], + }, + ], + { + initialEntries, + initialIndex, + } + ); + act(() => { + root = ReactDOM.createRoot(node); + root.render(); + }); + }); + + afterEach(() => { + act(() => root.unmount()); + }); + + it("does not navigate", async () => { + await act(async () => { + click(node.querySelector("a[href='/about']")); + await sleep(LOADER_LATENCY_MS); + }); + let h1 = node.querySelector("h1"); + expect(h1?.textContent).not.toBe("About"); + }); + + it("gets a 'blocked' blocker after navigation starts", async () => { + act(() => { + click(node.querySelector("a[href='/about']")); + }); + expect(blocker).toEqual({ + state: "blocked", + proceed: expect.any(Function), + reset: expect.any(Function), + location: expect.any(Object), + }); + }); + + it("gets a 'blocked' blocker after navigation promise resolves", async () => { + await act(async () => { + click(node.querySelector("a[href='/about']")); + await sleep(LOADER_LATENCY_MS); + }); + expect(blocker).toEqual({ + state: "blocked", + proceed: expect.any(Function), + reset: expect.any(Function), + location: expect.any(Object), + }); + }); + }); + + describe("exiting from blocked state", () => { + beforeEach(() => { + let initialEntries = ["/"]; + let initialIndex = 0; + router = createMemoryRouter( + [ + { + element: React.createElement(() => { + let b = useBlocker(true); + blocker = b; + return ( +
+ Home + About + {b.state === "blocked" && ( +
+ + +
+ )} + +
+ ); + }), + children: [ + { + path: "/", + element:

Home

, + }, + { + path: "/about", + element:

About

, + loader: slowLoader, + }, + ], + }, + ], + { + initialEntries, + initialIndex, + } + ); + act(() => { + root = ReactDOM.createRoot(node); + root.render(); + }); + }); + + afterEach(() => { + act(() => root.unmount()); + }); + + it("gets a 'proceeding' blocker after proceeding navigation starts", async () => { + act(() => { + click(node.querySelector("a[href='/about']")); + }); + act(() => { + click(node.querySelector("[data-action='proceed']")); + }); + expect(blocker).toEqual({ + state: "proceeding", + proceed: undefined, + reset: undefined, + location: expect.any(Object), + }); + }); + + it("gets an 'unblocked' blocker after proceeding navigation completes", async () => { + act(() => { + click(node.querySelector("a[href='/about']")); + }); + await act(async () => { + click(node.querySelector("[data-action='proceed']")); + await sleep(LOADER_LATENCY_MS); + }); + expect(blocker).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, + }); + }); + + it("navigates after proceeding navigation completes", async () => { + act(() => { + click(node.querySelector("a[href='/about']")); + }); + await act(async () => { + click(node.querySelector("[data-action='proceed']")); + await sleep(LOADER_LATENCY_MS); + }); + let h1 = node.querySelector("h1"); + expect(h1?.textContent).toBe("About"); + }); + + it("gets an 'unblocked' blocker after resetting navigation", async () => { + act(() => { + click(node.querySelector("a[href='/about']")); + }); + act(() => { + click(node.querySelector("[data-action='reset']")); + }); + expect(blocker).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, + }); + }); + + it("stays at current location after resetting", async () => { + act(() => { + click(node.querySelector("a[href='/about']")); + }); + await act(async () => { + click(node.querySelector("[data-action='reset']")); + // wait for '/about' loader so we catch failure if navigation proceeds + await sleep(LOADER_LATENCY_MS); + }); + let h1 = node.querySelector("h1"); + expect(h1?.textContent).toBe("Home"); + }); + }); + }); + + describe("on navigation", () => { + describe("blocker returns false", () => { + beforeEach(() => { + let initialEntries = ["/"]; + let initialIndex = 0; + router = createMemoryRouter( + [ + { + element: React.createElement(() => { + let b = useBlocker(false); + blocker = b; + return ( +
+ + Home + + + About + + +
+ ); + }), + children: [ + { + path: "/", + element:

Home

, + }, + { + path: "/about", + element:

About

, + loader: slowLoader, + }, + ], + }, + ], + { + initialEntries, + initialIndex, + } + ); + act(() => { + root = ReactDOM.createRoot(node); + root.render(); + }); + }); + + afterEach(() => { + act(() => root.unmount()); + }); + + it("navigates", async () => { + await act(async () => { + click(node.querySelector("a[href='/about']")); + await sleep(LOADER_LATENCY_MS); + }); + let h1 = node.querySelector("h1"); + expect(h1?.textContent).toBe("About"); + }); + + it("gets an 'unblocked' blocker after navigation starts", async () => { + act(() => { + click(node.querySelector("a[href='/about']")); + }); + expect(blocker).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, + }); + }); + + it("gets an 'unblocked' blocker after navigation completes", async () => { + await act(async () => { + click(node.querySelector("a[href='/about']")); + await sleep(LOADER_LATENCY_MS); + }); + expect(blocker).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, + }); + }); + }); + + describe("blocker returns true", () => { + beforeEach(() => { + let initialEntries = ["/"]; + let initialIndex = 0; + router = createMemoryRouter( + [ + { + element: React.createElement(() => { + let b = useBlocker(true); + blocker = b; + return ( +
+ + Home + + + About + + +
+ ); + }), + children: [ + { + path: "/", + element:

Home

, + }, + { + path: "/about", + element:

About

, + loader: slowLoader, + }, + ], + }, + ], + { + initialEntries, + initialIndex, + } + ); + act(() => { + root = ReactDOM.createRoot(node); + root.render(); + }); + }); + + afterEach(() => { + act(() => root.unmount()); + }); + + it("does not navigate", async () => { + await act(async () => { + click(node.querySelector("a[href='/about']")); + await sleep(LOADER_LATENCY_MS); + }); + let h1 = node.querySelector("h1"); + expect(h1?.textContent).not.toBe("About"); + }); + + it("gets a 'blocked' blocker after navigation starts", async () => { + act(() => { + click(node.querySelector("a[href='/about']")); + }); + expect(blocker).toEqual({ + state: "blocked", + proceed: expect.any(Function), + reset: expect.any(Function), + location: expect.any(Object), + }); + }); + + it("gets a 'blocked' blocker after navigation promise resolves", async () => { + await act(async () => { + click(node.querySelector("a[href='/about']")); + await sleep(LOADER_LATENCY_MS); + }); + expect(blocker).toEqual({ + state: "blocked", + proceed: expect.any(Function), + reset: expect.any(Function), + location: expect.any(Object), + }); + }); + }); + + describe("exiting from blocked state", () => { + beforeEach(() => { + let initialEntries = ["/"]; + let initialIndex = 0; + router = createMemoryRouter( + [ + { + element: React.createElement(() => { + let b = useBlocker(true); + blocker = b; + return ( +
+ + Home + + + About + + {b.state === "blocked" && ( +
+ + +
+ )} + +
+ ); + }), + children: [ + { + path: "/", + element:

Home

, + }, + { + path: "/about", + element:

About

, + loader: slowLoader, + }, + ], + }, + ], + { + initialEntries, + initialIndex, + } + ); + act(() => { + root = ReactDOM.createRoot(node); + root.render(); + }); + }); + + afterEach(() => { + act(() => root.unmount()); + }); + + it("gets a 'proceeding' blocker after proceeding navigation starts", async () => { + act(() => { + click(node.querySelector("a[href='/about']")); + }); + act(() => { + click(node.querySelector("[data-action='proceed']")); + }); + expect(blocker).toEqual({ + state: "proceeding", + proceed: undefined, + reset: undefined, + location: expect.any(Object), + }); + }); + + it("gets an 'unblocked' blocker after proceeding navigation completes", async () => { + act(() => { + click(node.querySelector("a[href='/about']")); + }); + await act(async () => { + click(node.querySelector("[data-action='proceed']")); + await sleep(LOADER_LATENCY_MS); + }); + expect(blocker).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, + }); + }); + + it("navigates after proceeding navigation completes", async () => { + act(() => { + click(node.querySelector("a[href='/about']")); + }); + await act(async () => { + click(node.querySelector("[data-action='proceed']")); + await sleep(LOADER_LATENCY_MS); + }); + let h1 = node.querySelector("h1"); + expect(h1?.textContent).toBe("About"); + }); + + it("gets an 'unblocked' blocker after resetting navigation", async () => { + act(() => { + click(node.querySelector("a[href='/about']")); + }); + act(() => { + click(node.querySelector("[data-action='reset']")); + }); + expect(blocker).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, + }); + }); + + it("stays at current location after resetting", async () => { + act(() => { + click(node.querySelector("a[href='/about']")); + }); + await act(async () => { + click(node.querySelector("[data-action='reset']")); + // wait for '/about' loader so we catch failure if navigation proceeds + await sleep(LOADER_LATENCY_MS); + }); + let h1 = node.querySelector("h1"); + expect(h1?.textContent).toBe("Home"); + }); + }); + }); + + describe("on POP navigation", () => { + describe("blocker returns false", () => { + beforeEach(() => { + let initialEntries = ["/", "/about", "/contact"]; + let initialIndex = 2; + router = createMemoryRouter( + [ + { + element: React.createElement(() => { + let b = useBlocker(false); + let navigate = useNavigate(); + blocker = b; + return ( +
+ + Home + + + About + + + +
+ ); + }), + children: [ + { + path: "/", + element:

Home

, + }, + { + path: "/about", + element:

About

, + loader: slowLoader, + }, + { + path: "/contact", + element:

Contact

, + }, + ], + }, + ], + { + initialEntries, + initialIndex, + } + ); + act(() => { + root = ReactDOM.createRoot(node); + root.render(); + }); + }); + + afterEach(() => { + act(() => root.unmount()); + }); + + it("navigates", async () => { + await act(async () => { + click(node.querySelector("[data-action='back']")); + await sleep(LOADER_LATENCY_MS); + }); + let h1 = node.querySelector("h1"); + expect(h1?.textContent).toBe("About"); + }); + + it("gets an 'unblocked' blocker after navigation starts", async () => { + act(() => { + click(node.querySelector("[data-action='back']")); + }); + expect(blocker).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, + }); + }); + + it("gets an 'unblocked' blocker after navigation completes", async () => { + await act(async () => { + click(node.querySelector("[data-action='back']")); + await sleep(LOADER_LATENCY_MS); + }); + expect(blocker).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, + }); + }); + }); + + describe("blocker returns true", () => { + beforeEach(() => { + let initialEntries = ["/", "/about", "/contact"]; + let initialIndex = 2; + router = createMemoryRouter( + [ + { + element: React.createElement(() => { + let b = useBlocker(true); + let navigate = useNavigate(); + blocker = b; + return ( +
+ + Home + + + About + + + +
+ ); + }), + children: [ + { + path: "/", + element:

Home

, + }, + { + path: "/about", + element:

About

, + loader: slowLoader, + }, + { + path: "/contact", + element:

Contact

, + }, + ], + }, + ], + { + initialEntries, + initialIndex, + } + ); + act(() => { + root = ReactDOM.createRoot(node); + root.render(); + }); + }); + + afterEach(() => { + act(() => root.unmount()); + }); + + it("does not navigate", async () => { + await act(async () => { + click(node.querySelector("[data-action='back']")); + await sleep(LOADER_LATENCY_MS); + }); + let h1 = node.querySelector("h1"); + expect(h1?.textContent).not.toBe("About"); + }); + + it("gets a 'blocked' blocker after navigation starts", async () => { + act(() => { + click(node.querySelector("[data-action='back']")); + }); + expect(blocker).toEqual({ + state: "blocked", + proceed: expect.any(Function), + reset: expect.any(Function), + location: expect.any(Object), + }); + }); + + it("gets a 'blocked' blocker after navigation promise resolves", async () => { + await act(async () => { + click(node.querySelector("[data-action='back']")); + await sleep(LOADER_LATENCY_MS); + }); + expect(blocker).toEqual({ + state: "blocked", + proceed: expect.any(Function), + reset: expect.any(Function), + location: expect.any(Object), + }); + }); + }); + + describe("exiting from blocked state", () => { + beforeEach(() => { + let initialEntries = ["/", "/about", "/contact"]; + let initialIndex = 2; + router = createMemoryRouter( + [ + { + element: React.createElement(() => { + let b = useBlocker(true); + let navigate = useNavigate(); + blocker = b; + return ( +
+ + Home + + + About + + + {b.state === "blocked" && ( +
+ + +
+ )} + +
+ ); + }), + children: [ + { + path: "/", + element:

Home

, + }, + { + path: "/about", + element:

About

, + loader: slowLoader, + }, + { + path: "/contact", + element:

Contact

, + }, + ], + }, + ], + { + initialEntries, + initialIndex, + } + ); + act(() => { + root = ReactDOM.createRoot(node); + root.render(); + }); + }); + + afterEach(() => { + act(() => root.unmount()); + }); + + it("gets a 'proceeding' blocker after proceeding navigation starts", async () => { + act(() => { + click(node.querySelector("[data-action='back']")); + }); + act(() => { + click(node.querySelector("[data-action='proceed']")); + }); + expect(blocker).toEqual({ + state: "proceeding", + proceed: undefined, + reset: undefined, + location: expect.any(Object), + }); + }); + + it("gets an 'unblocked' blocker after proceeding navigation completes", async () => { + act(() => { + click(node.querySelector("[data-action='back']")); + }); + await act(async () => { + click(node.querySelector("[data-action='proceed']")); + await sleep(LOADER_LATENCY_MS); + }); + expect(blocker).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, + }); + }); + + it("navigates after proceeding navigation completes", async () => { + act(() => { + click(node.querySelector("[data-action='back']")); + }); + await act(async () => { + click(node.querySelector("[data-action='proceed']")); + await sleep(LOADER_LATENCY_MS); + }); + let h1 = node.querySelector("h1"); + expect(h1?.textContent).toBe("About"); + }); + + it("gets an 'unblocked' blocker after resetting navigation", async () => { + act(() => { + click(node.querySelector("[data-action='back']")); + }); + act(() => { + click(node.querySelector("[data-action='reset']")); + }); + expect(blocker).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, + }); + }); + + it("stays at current location after resetting", async () => { + act(() => { + click(node.querySelector("[data-action='back']")); + }); + await act(async () => { + click(node.querySelector("[data-action='reset']")); + // wait for '/about' loader so we catch failure if navigation proceeds + await sleep(LOADER_LATENCY_MS); + }); + let h1 = node.querySelector("h1"); + expect(h1?.textContent).toBe("Contact"); + }); + }); + }); +}); + +function sleep(n: number = 500) { + return new Promise((r) => setTimeout(r, n)); +} + +function click(target: EventTarget | null | undefined) { + target?.dispatchEvent( + new MouseEvent("click", { + view: window, + bubbles: true, + cancelable: true, + }) + ); +} diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index cf0fd14240..f5a1dd4590 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -76,6 +76,8 @@ export type { ActionFunction, ActionFunctionArgs, AwaitProps, + unstable_Blocker, + unstable_BlockerFunction, DataRouteMatch, DataRouteObject, Fetcher, @@ -142,6 +144,7 @@ export { useActionData, useAsyncError, useAsyncValue, + unstable_useBlocker, useHref, useInRouterContext, useLoaderData, @@ -1185,14 +1188,17 @@ function useScrollRestoration({ * `React.useCallback()`. */ export function useBeforeUnload( - callback: (event: BeforeUnloadEvent) => any + callback: (event: BeforeUnloadEvent) => any, + options?: { capture?: boolean } ): void { + let { capture } = options || {}; React.useEffect(() => { - window.addEventListener("beforeunload", callback); + let opts = capture != null ? { capture } : undefined; + window.addEventListener("beforeunload", callback, opts); return () => { - window.removeEventListener("beforeunload", callback); + window.removeEventListener("beforeunload", callback, opts); }; - }, [callback]); + }, [callback, capture]); } //#endregion diff --git a/packages/react-router-dom/server.tsx b/packages/react-router-dom/server.tsx index a8ac8cb10c..3ace651b43 100644 --- a/packages/react-router-dom/server.tsx +++ b/packages/react-router-dom/server.tsx @@ -263,6 +263,7 @@ export function createStaticRouter( preventScrollReset: false, revalidation: "idle" as RevalidationState, fetchers: new Map(), + blockers: new Map(), }; }, get routes() { @@ -297,6 +298,12 @@ export function createStaticRouter( dispose() { throw msg("dispose"); }, + getBlocker() { + throw msg("getBlocker"); + }, + deleteBlocker() { + throw msg("deleteBlocker"); + }, _internalFetchControllers: new Map(), _internalActiveDeferreds: new Map(), }; diff --git a/packages/react-router-native/index.tsx b/packages/react-router-native/index.tsx index 5d66fccb83..d89e3fba19 100644 --- a/packages/react-router-native/index.tsx +++ b/packages/react-router-native/index.tsx @@ -23,6 +23,8 @@ export type { ActionFunction, ActionFunctionArgs, AwaitProps, + unstable_Blocker, + unstable_BlockerFunction, DataRouteMatch, DataRouteObject, Fetcher, @@ -89,6 +91,7 @@ export { useActionData, useAsyncError, useAsyncValue, + unstable_useBlocker, useHref, useInRouterContext, useLoaderData, diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts index 2af02fef6c..3d55ed2e22 100644 --- a/packages/react-router/index.ts +++ b/packages/react-router/index.ts @@ -1,6 +1,8 @@ import type { ActionFunction, ActionFunctionArgs, + Blocker, + BlockerFunction, Fetcher, HydrationState, JsonFunction, @@ -82,6 +84,7 @@ import { } from "./lib/context"; import type { NavigateFunction } from "./lib/hooks"; import { + useBlocker, useHref, useInRouterContext, useLocation, @@ -114,6 +117,8 @@ export type { ActionFunction, ActionFunctionArgs, AwaitProps, + Blocker as unstable_Blocker, + BlockerFunction as unstable_BlockerFunction, DataRouteMatch, DataRouteObject, Fetcher, @@ -179,6 +184,7 @@ export { useActionData, useAsyncError, useAsyncValue, + useBlocker as unstable_useBlocker, useHref, useInRouterContext, useLoaderData, diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index 96ff63e747..eb4bc220a8 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -1,5 +1,7 @@ import * as React from "react"; import type { + Blocker, + BlockerFunction, Location, ParamParseKey, Params, @@ -650,6 +652,7 @@ export function _renderMatches( } enum DataRouterHook { + UseBlocker = "useBlocker", UseRevalidator = "useRevalidator", } @@ -818,6 +821,36 @@ export function useAsyncError(): unknown { return value?._error; } +// useBlocker() is a singleton for now since we don't have any compelling use +// cases for multi-blocker yet +let blockerKey = "blocker-singleton"; + +/** + * Allow the application to block navigations within the SPA and present the + * user a confirmation dialog to confirm the navigation. Mostly used to avoid + * using half-filled form data. This does not handle hard-reloads or + * cross-origin navigations. + */ +export function useBlocker(shouldBlock: boolean | BlockerFunction): Blocker { + let { router } = useDataRouterContext(DataRouterHook.UseBlocker); + + let blockerFunction = React.useCallback( + (args) => { + return typeof shouldBlock === "function" + ? !!shouldBlock(args) + : !!shouldBlock; + }, + [shouldBlock] + ); + + let blocker = router.getBlocker(blockerKey, blockerFunction); + + // Cleanup on unmount + React.useEffect(() => () => router.deleteBlocker(blockerKey), [router]); + + return blocker; +} + const alreadyWarned: Record = {}; function warningOnce(key: string, cond: boolean, message: string) { diff --git a/packages/router/__tests__/TestSequences/GoBack.ts b/packages/router/__tests__/TestSequences/GoBack.ts index d7009441eb..03a6711818 100644 --- a/packages/router/__tests__/TestSequences/GoBack.ts +++ b/packages/router/__tests__/TestSequences/GoBack.ts @@ -31,6 +31,7 @@ export default async function GoBack(history: History) { }); expect(spy).toHaveBeenCalledWith({ action: "POP", + delta: expect.any(Number), location: { hash: "", key: expect.any(String), diff --git a/packages/router/__tests__/TestSequences/GoForward.ts b/packages/router/__tests__/TestSequences/GoForward.ts index 5949524b26..ccc9f09c3a 100644 --- a/packages/router/__tests__/TestSequences/GoForward.ts +++ b/packages/router/__tests__/TestSequences/GoForward.ts @@ -31,6 +31,7 @@ export default async function GoForward(history: History) { }); expect(spy).toHaveBeenCalledWith({ action: "POP", + delta: expect.any(Number), location: { hash: "", key: expect.any(String), @@ -58,6 +59,7 @@ export default async function GoForward(history: History) { }); expect(spy).toHaveBeenCalledWith({ action: "POP", + delta: expect.any(Number), location: { hash: "", key: expect.any(String), diff --git a/packages/router/__tests__/navigation-blocking-test.ts b/packages/router/__tests__/navigation-blocking-test.ts new file mode 100644 index 0000000000..e0fdc616f2 --- /dev/null +++ b/packages/router/__tests__/navigation-blocking-test.ts @@ -0,0 +1,493 @@ +import type { Router } from "../index"; +import { createMemoryHistory, createRouter } from "../index"; + +const LOADER_LATENCY_MS = 100; +const routes = [ + { path: "/" }, + { + path: "/about", + loader: () => sleep(LOADER_LATENCY_MS), + }, + { path: "/contact" }, + { path: "/help" }, +]; + +describe("navigation blocking", () => { + let router: Router; + it("initializes an 'unblocked' blocker", () => { + router = createRouter({ + history: createMemoryHistory({ + initialEntries: ["/"], + initialIndex: 0, + }), + routes, + }); + router.initialize(); + + let fn = () => true; + router.getBlocker("KEY", fn); + expect(router.getBlocker("KEY", fn)).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, + }); + }); + + describe("on history push", () => { + let initialEntries = ["/", "/about"]; + let initialIndex = 0; + beforeEach(() => { + router = createRouter({ + history: createMemoryHistory({ + initialEntries, + initialIndex, + }), + routes, + }); + router.initialize(); + }); + + describe("blocker returns false", () => { + let fn = () => false; + it("navigates", async () => { + router.getBlocker("KEY", fn); + await router.navigate("/about"); + expect(router.state.location.pathname).toBe("/about"); + }); + + it("gets an 'unblocked' blocker after navigation starts", async () => { + router.getBlocker("KEY", fn); + router.navigate("/about"); + expect(router.getBlocker("KEY", fn)).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, + }); + }); + + it("gets an 'unblocked' blocker after navigation completes", async () => { + router.getBlocker("KEY", fn); + await router.navigate("/about"); + expect(router.getBlocker("KEY", fn)).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, + }); + }); + }); + + describe("blocker returns true", () => { + let fn = () => true; + + it("does not navigate", async () => { + router.getBlocker("KEY", fn); + await router.navigate("/about"); + expect(router.state.location.pathname).toBe( + initialEntries[initialIndex] + ); + }); + + it("gets a 'blocked' blocker after navigation starts", async () => { + router.getBlocker("KEY", fn); + router.navigate("/about"); + expect(router.getBlocker("KEY", fn)).toEqual({ + state: "blocked", + proceed: expect.any(Function), + reset: expect.any(Function), + location: expect.any(Object), + }); + }); + + it("gets a 'blocked' blocker after navigation promise resolves", async () => { + router.getBlocker("KEY", fn); + await router.navigate("/about"); + expect(router.getBlocker("KEY", fn)).toEqual({ + state: "blocked", + proceed: expect.any(Function), + reset: expect.any(Function), + location: expect.any(Object), + }); + }); + }); + + describe("proceeds from blocked state", () => { + let fn = () => true; + it("gets a 'proceeding' blocker after proceeding navigation starts", async () => { + router.getBlocker("KEY", fn); + await router.navigate("/about"); + router.getBlocker("KEY", fn).proceed?.(); + expect(router.getBlocker("KEY", fn)).toEqual({ + state: "proceeding", + proceed: undefined, + reset: undefined, + location: expect.any(Object), + }); + }); + + it("gets an 'unblocked' blocker after proceeding navigation completes", async () => { + router.getBlocker("KEY", fn); + await router.navigate("/about"); + router.getBlocker("KEY", fn).proceed?.(); + await sleep(LOADER_LATENCY_MS); + expect(router.getBlocker("KEY", fn)).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, + }); + }); + + it("navigates after proceeding navigation completes", async () => { + router.getBlocker("KEY", fn); + await router.navigate("/about"); + router.getBlocker("KEY", fn).proceed?.(); + await sleep(LOADER_LATENCY_MS); + expect(router.state.location.pathname).toBe("/about"); + }); + }); + + describe("resets from blocked state", () => { + let fn = () => true; + it("gets an 'unblocked' blocker after resetting navigation", async () => { + router.getBlocker("KEY", fn); + await router.navigate("/about"); + router.getBlocker("KEY", fn).reset?.(); + expect(router.getBlocker("KEY", fn)).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, + }); + }); + + it("stays at current location after resetting", async () => { + router.getBlocker("KEY", fn); + let pathnameBeforeNavigation = router.state.location.pathname; + await router.navigate("/about"); + router.getBlocker("KEY", fn).reset?.(); + + // wait for '/about' loader so we catch failure if navigation proceeds + await sleep(LOADER_LATENCY_MS); + expect(router.state.location.pathname).toBe(pathnameBeforeNavigation); + }); + }); + }); + + describe("on history replace", () => { + let initialEntries = ["/", "/about"]; + let initialIndex = 0; + beforeEach(() => { + router = createRouter({ + history: createMemoryHistory({ + initialEntries, + initialIndex, + }), + routes, + }); + router.initialize(); + }); + + describe("blocker returns false", () => { + let fn = () => false; + it("navigates", async () => { + router.getBlocker("KEY", fn); + await router.navigate("/about", { replace: true }); + expect(router.state.location.pathname).toBe("/about"); + }); + + it("gets an 'unblocked' blocker after navigation starts", async () => { + router.getBlocker("KEY", fn); + router.navigate("/about", { replace: true }); + expect(router.getBlocker("KEY", fn)).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, + }); + }); + + it("gets an 'unblocked' blocker after navigation completes", async () => { + router.getBlocker("KEY", fn); + await router.navigate("/about", { replace: true }); + expect(router.getBlocker("KEY", fn)).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, + }); + }); + }); + + describe("blocker returns true", () => { + let fn = () => true; + + it("does not navigate", async () => { + router.getBlocker("KEY", fn); + await router.navigate("/about", { replace: true }); + expect(router.state.location.pathname).toBe( + initialEntries[initialIndex] + ); + }); + + it("gets a 'blocked' blocker after navigation starts", async () => { + router.getBlocker("KEY", fn); + router.navigate("/about", { replace: true }); + expect(router.getBlocker("KEY", fn)).toEqual({ + state: "blocked", + proceed: expect.any(Function), + reset: expect.any(Function), + location: expect.any(Object), + }); + }); + + it("gets a 'blocked' blocker after navigation promise resolves", async () => { + router.getBlocker("KEY", fn); + await router.navigate("/about", { replace: true }); + expect(router.getBlocker("KEY", fn)).toEqual({ + state: "blocked", + proceed: expect.any(Function), + reset: expect.any(Function), + location: expect.any(Object), + }); + }); + }); + + describe("proceeds from blocked state", () => { + let fn = () => true; + it("gets a 'proceeding' blocker after proceeding navigation starts", async () => { + router.getBlocker("KEY", fn); + await router.navigate("/about", { replace: true }); + router.getBlocker("KEY", fn).proceed?.(); + expect(router.getBlocker("KEY", fn)).toEqual({ + state: "proceeding", + proceed: undefined, + reset: undefined, + location: expect.any(Object), + }); + }); + + it("gets an 'unblocked' blocker after proceeding navigation completes", async () => { + router.getBlocker("KEY", fn); + await router.navigate("/about", { replace: true }); + router.getBlocker("KEY", fn).proceed?.(); + await sleep(LOADER_LATENCY_MS); + expect(router.getBlocker("KEY", fn)).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, + }); + }); + + it("navigates after proceeding navigation completes", async () => { + router.getBlocker("KEY", fn); + await router.navigate("/about", { replace: true }); + router.getBlocker("KEY", fn).proceed?.(); + await sleep(LOADER_LATENCY_MS); + expect(router.state.location.pathname).toBe("/about"); + }); + + it("replaces the current history entry after proceeding completes", async () => { + router.getBlocker("KEY", fn); + let historyLengthBeforeNavigation = window.history.length; + await router.navigate("/about", { replace: true }); + router.getBlocker("KEY", fn).proceed?.(); + await sleep(LOADER_LATENCY_MS); + expect(window.history.length).toBe(historyLengthBeforeNavigation); + }); + }); + + describe("resets from blocked state", () => { + let fn = () => true; + it("gets an 'unblocked' blocker after resetting navigation", async () => { + router.getBlocker("KEY", fn); + await router.navigate("/about", { replace: true }); + router.getBlocker("KEY", fn).reset?.(); + expect(router.getBlocker("KEY", fn)).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined + }); + }); + + it("stays at current location after resetting", async () => { + router.getBlocker("KEY", fn); + let pathnameBeforeNavigation = router.state.location.pathname; + await router.navigate("/about", { replace: true }); + router.getBlocker("KEY", fn).reset?.(); + + // wait for '/about' loader so we catch failure if navigation proceeds + await sleep(LOADER_LATENCY_MS); + expect(router.state.location.pathname).toBe(pathnameBeforeNavigation); + }); + }); + }); + + describe("on history pop", () => { + let initialEntries = ["/", "/about", "/contact", "/help"]; + let initialIndex = 1; + beforeEach(() => { + router = createRouter({ + history: createMemoryHistory({ + initialEntries, + initialIndex, + }), + routes, + }); + router.initialize(); + }); + + describe("blocker returns false", () => { + let fn = () => false; + it("navigates", async () => { + router.getBlocker("KEY", fn); + await router.navigate(-1); + expect(router.state.location.pathname).toBe( + initialEntries[initialIndex - 1] + ); + }); + + it("gets an 'unblocked' blocker after navigation starts", async () => { + router.getBlocker("KEY", fn); + router.navigate(-1); + expect(router.getBlocker("KEY", fn)).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, + }); + }); + + it("gets an 'unblocked' blocker after navigation completes", async () => { + router.getBlocker("KEY", fn); + await router.navigate(-1); + expect(router.getBlocker("KEY", fn)).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, + }); + }); + }); + + describe("blocker returns true", () => { + let fn = () => true; + + it("does not navigate", async () => { + router.getBlocker("KEY", fn); + await router.navigate(-1); + expect(router.state.location.pathname).toBe( + initialEntries[initialIndex] + ); + }); + + it("gets a 'blocked' blocker after navigation starts", async () => { + router.getBlocker("KEY", fn); + router.navigate(-1); + expect(router.getBlocker("KEY", fn)).toEqual({ + state: "blocked", + proceed: expect.any(Function), + reset: expect.any(Function), + location: expect.any(Object), + }); + }); + + it("gets a 'blocked' blocker after navigation promise resolves", async () => { + router.getBlocker("KEY", fn); + await router.navigate(-1); + expect(router.getBlocker("KEY", fn)).toEqual({ + state: "blocked", + proceed: expect.any(Function), + reset: expect.any(Function), + location: expect.any(Object), + }); + }); + }); + + describe("proceeds from blocked state", () => { + let fn = () => true; + + // we want to navigate so that `/about` is the previous entry in the + // stack here since it has a loader that won't resolve immediately + let initialEntries = ["/", "/about", "/contact"]; + let initialIndex = 2; + beforeEach(() => { + router = createRouter({ + history: createMemoryHistory({ + initialEntries, + initialIndex, + }), + routes, + }); + router.initialize(); + }); + + it("gets a 'proceeding' blocker after proceeding navigation starts", async () => { + router.getBlocker("KEY", fn); + await router.navigate(-1); + router.getBlocker("KEY", fn).proceed?.(); + expect(router.getBlocker("KEY", fn)).toEqual({ + state: "proceeding", + proceed: undefined, + reset: undefined, + location: expect.any(Object), + }); + }); + + it("gets an 'unblocked' blocker after proceeding navigation completes", async () => { + router.getBlocker("KEY", fn); + await router.navigate(-1); + router.getBlocker("KEY", fn).proceed?.(); + await sleep(LOADER_LATENCY_MS); + expect(router.getBlocker("KEY", fn)).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, + }); + }); + + it("navigates after proceeding navigation completes", async () => { + router.getBlocker("KEY", fn); + await router.navigate(-1); + router.getBlocker("KEY", fn).proceed?.(); + await sleep(LOADER_LATENCY_MS); + expect(router.state.location.pathname).toBe("/about"); + }); + }); + + describe("resets from blocked state", () => { + let fn = () => true; + it("gets an 'unblocked' blocker after resetting navigation", async () => { + router.getBlocker("KEY", fn); + await router.navigate(-1); + router.getBlocker("KEY", fn).reset?.(); + expect(router.getBlocker("KEY", fn)).toEqual({ + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, + }); + }); + + it("stays at current location after resetting", async () => { + router.getBlocker("KEY", fn); + let pathnameBeforeNavigation = router.state.location.pathname; + await router.navigate(-1); + router.getBlocker("KEY", fn).reset?.(); + + // wait for '/about' loader so we catch failure if navigation proceeds + await sleep(LOADER_LATENCY_MS); + expect(router.state.location.pathname).toBe(pathnameBeforeNavigation); + }); + }); + }); +}); + +function sleep(n: number = 500) { + return new Promise((r) => setTimeout(r, n)); +} diff --git a/packages/router/__tests__/router-memory-test.ts b/packages/router/__tests__/router-memory-test.ts index 2a1635fac4..f4d133c518 100644 --- a/packages/router/__tests__/router-memory-test.ts +++ b/packages/router/__tests__/router-memory-test.ts @@ -48,6 +48,7 @@ describe("a memory router", () => { restoreScrollPosition: null, revalidation: "idle", fetchers: new Map(), + blockers: new Map(), }); router.dispose(); }); diff --git a/packages/router/__tests__/router-test.ts b/packages/router/__tests__/router-test.ts index 6e4a7116ed..9cc383b2ee 100644 --- a/packages/router/__tests__/router-test.ts +++ b/packages/router/__tests__/router-test.ts @@ -957,6 +957,7 @@ describe("a router", () => { restoreScrollPosition: null, revalidation: "idle", fetchers: new Map(), + blockers: new Map(), }); }); @@ -1018,6 +1019,7 @@ describe("a router", () => { restoreScrollPosition: false, revalidation: "idle", fetchers: new Map(), + blockers: new Map(), }); }); diff --git a/packages/router/history.ts b/packages/router/history.ts index e8b6c10f7d..fee3e1191f 100644 --- a/packages/router/history.ts +++ b/packages/router/history.ts @@ -81,6 +81,11 @@ export interface Update { * The new location. */ location: Location; + + /** + * The delta between this location and the former location in the history stack + */ + delta: number; } /** @@ -181,6 +186,7 @@ export interface History { type HistoryState = { usr: any; key?: string; + idx: number; }; const PopStateEventType = "popstate"; @@ -294,7 +300,7 @@ export function createMemoryHistory( index += 1; entries.splice(index, entries.length, nextLocation); if (v5Compat && listener) { - listener({ action, location: nextLocation }); + listener({ action, location: nextLocation, delta: 1 }); } }, replace(to, state) { @@ -302,14 +308,16 @@ export function createMemoryHistory( let nextLocation = createMemoryLocation(to, state); entries[index] = nextLocation; if (v5Compat && listener) { - listener({ action, location: nextLocation }); + listener({ action, location: nextLocation, delta: 0 }); } }, go(delta) { action = Action.Pop; - index = clampIndex(index + delta); + let nextIndex = clampIndex(index + delta); + let nextLocation = entries[nextIndex]; + index = nextIndex; if (listener) { - listener({ action, location: getCurrentLocation() }); + listener({ action, location: nextLocation, delta }); } }, listen(fn: Listener) { @@ -497,10 +505,11 @@ function createKey() { /** * For browser-based histories, we combine the state and key into an object */ -function getHistoryState(location: Location): HistoryState { +function getHistoryState(location: Location, index: number): HistoryState { return { usr: location.state, key: location.key, + idx: index, }; } @@ -588,10 +597,43 @@ function getUrlBasedHistory( let action = Action.Pop; let listener: Listener | null = null; + let index = getIndex()!; + // Index should only be null when we initialize. If not, it's because the + // user called history.pushState or history.replaceState directly, in which + // case we should log a warning as it will result in bugs. + if (index == null) { + index = 0; + globalHistory.replaceState({ ...globalHistory.state, idx: index }, ""); + } + + function getIndex(): number { + let state = globalHistory.state || { idx: null }; + return state.idx; + } + function handlePop() { - action = Action.Pop; - if (listener) { - listener({ action, location: history.location }); + let nextAction = Action.Pop; + let nextIndex = getIndex(); + + if (nextIndex != null) { + let delta = nextIndex - index; + action = nextAction; + index = nextIndex; + if (listener) { + listener({ action, location: history.location, delta }); + } + } else { + warning( + false, + // TODO: Write up a doc that explains our blocking strategy in detail + // and link to it here so people can understand better what is going on + // and how to avoid it. + `You are trying to block a POP navigation to a location that was not ` + + `created by @remix-run/router. The block will fail silently in ` + + `production, but in general you should do all navigation with the ` + + `router (instead of using window.history.pushState directly) ` + + `to avoid this situation.` + ); } } @@ -600,7 +642,8 @@ function getUrlBasedHistory( let location = createLocation(history.location, to, state); if (validateLocation) validateLocation(location, to); - let historyState = getHistoryState(location); + index = getIndex() + 1; + let historyState = getHistoryState(location, index); let url = history.createHref(location); // try...catch because iOS limits us to 100 pushState calls :/ @@ -613,7 +656,7 @@ function getUrlBasedHistory( } if (v5Compat && listener) { - listener({ action, location: history.location }); + listener({ action, location: history.location, delta: 1 }); } } @@ -622,12 +665,13 @@ function getUrlBasedHistory( let location = createLocation(history.location, to, state); if (validateLocation) validateLocation(location, to); - let historyState = getHistoryState(location); + index = getIndex(); + let historyState = getHistoryState(location, index); let url = history.createHref(location); globalHistory.replaceState(historyState, "", url); if (v5Compat && listener) { - listener({ action, location: history.location }); + listener({ action, location: history.location, delta: 0 }); } } diff --git a/packages/router/router.ts b/packages/router/router.ts index e8ceef4abd..21ae862585 100644 --- a/packages/router/router.ts +++ b/packages/router/router.ts @@ -32,6 +32,7 @@ import { joinPaths, matchRoutes, resolveTo, + warning, } from "./utils"; //////////////////////////////////////////////////////////////////////////////// @@ -110,14 +111,14 @@ export interface Router { * Navigate forward/backward in the history stack * @param to Delta to move in the history stack */ - navigate(to: number): void; + navigate(to: number): Promise; /** * Navigate to the given path * @param to Path to navigate to * @param opts Navigation options (method, submission, etc.) */ - navigate(to: To, opts?: RouterNavigateOptions): void; + navigate(to: To, opts?: RouterNavigateOptions): Promise; /** * @internal @@ -190,6 +191,25 @@ export interface Router { */ dispose(): void; + /** + * @internal + * PRIVATE - DO NOT USE + * + * Get a navigation blocker + * @param key The identifier for the blocker + * @param fn The blocker function implementation + */ + getBlocker(key: string, fn: BlockerFunction): Blocker; + + /** + * @internal + * PRIVATE - DO NOT USE + * + * Delete a navigation blocker + * @param key The identifier for the blocker + */ + deleteBlocker(key: string): void; + /** * @internal * PRIVATE - DO NOT USE @@ -275,6 +295,11 @@ export interface RouterState { * Map of current fetchers */ fetchers: Map; + + /** + * Map of current blockers + */ + blockers: Map; } /** @@ -460,6 +485,35 @@ type FetcherStates = { export type Fetcher = FetcherStates[keyof FetcherStates]; +interface BlockerBlocked { + state: "blocked"; + reset(): void; + proceed(): void; + location: Location; +} + +interface BlockerUnblocked { + state: "unblocked"; + reset: undefined; + proceed: undefined; + location: undefined; +} + +interface BlockerProceeding { + state: "proceeding"; + reset: undefined; + proceed: undefined; + location: Location; +} + +export type Blocker = BlockerUnblocked | BlockerBlocked | BlockerProceeding; + +export type BlockerFunction = (args: { + currentLocation: Location; + nextLocation: Location; + historyAction: HistoryAction; +}) => boolean; + interface ShortCircuitable { /** * startNavigation does not need to complete the navigation because we @@ -561,6 +615,13 @@ export const IDLE_FETCHER: FetcherStates["Idle"] = { formData: undefined, }; +export const IDLE_BLOCKER: BlockerUnblocked = { + state: "unblocked", + proceed: undefined, + reset: undefined, + location: undefined, +}; + const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined" && @@ -636,50 +697,76 @@ export function createRouter(init: RouterInit): Router { actionData: (init.hydrationData && init.hydrationData.actionData) || null, errors: (init.hydrationData && init.hydrationData.errors) || initialErrors, fetchers: new Map(), + blockers: new Map(), }; // -- Stateful internal variables to manage navigations -- // Current navigation in progress (to be committed in completeNavigation) let pendingAction: HistoryAction = HistoryAction.Pop; + // Should the current navigation prevent the scroll reset if scroll cannot // be restored? let pendingPreventScrollReset = false; + // AbortController for the active navigation let pendingNavigationController: AbortController | null; + // We use this to avoid touching history in completeNavigation if a // revalidation is entirely uninterrupted let isUninterruptedRevalidation = false; + // Use this internal flag to force revalidation of all loaders: // - submissions (completed or interrupted) // - useRevalidate() // - X-Remix-Revalidate (from redirect) let isRevalidationRequired = false; + // Use this internal array to capture routes that require revalidation due // to a cancelled deferred on action submission let cancelledDeferredRoutes: string[] = []; + // Use this internal array to capture fetcher loads that were cancelled by an // action navigation and require revalidation let cancelledFetcherLoads: string[] = []; + // AbortControllers for any in-flight fetchers let fetchControllers = new Map(); + // Track loads based on the order in which they started let incrementingLoadId = 0; + // Track the outstanding pending navigation data load to be compared against // the globally incrementing load when a fetcher load lands after a completed // navigation let pendingNavigationLoadId = -1; + // Fetchers that triggered data reloads as a result of their actions let fetchReloadIds = new Map(); + // Fetchers that triggered redirect navigations from their actions let fetchRedirectIds = new Set(); + // Most recent href/match for fetcher.load calls for fetchers let fetchLoadMatches = new Map(); + // Store DeferredData instances for active route matches. When a // route loader returns defer() we stick one in here. Then, when a nested // promise resolves we update loaderData. If a new navigation starts we // cancel active deferreds for eliminated routes. let activeDeferreds = new Map(); + // We ony support a single active blocker at the moment since we don't have + // any compelling use cases for multi-blocker yet + let activeBlocker: string | null = null; + + // Store blocker functions in a separate Map outside of router state since + // we don't need to update UI state if they change + let blockerFunctions = new Map(); + + // Flag to ignore the next history update, so we can revert the URL change on + // a POP navigation that was blocked by the user without touching router state + let ignoreNextHistoryUpdate = false; + // 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(); @@ -687,8 +774,48 @@ export function createRouter(init: RouterInit): Router { // 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 unlistenHistory = init.history.listen( - ({ action: historyAction, location }) => - startNavigation(historyAction, location) + ({ action: historyAction, location, delta }) => { + // Ignore this event if it was just us resetting the URL from a + // blocked POP navigation + if (ignoreNextHistoryUpdate) { + ignoreNextHistoryUpdate = false; + return; + } + + let blockerKey = shouldBlockNavigation({ + currentLocation: state.location, + nextLocation: location, + historyAction, + }); + if (blockerKey) { + // Restore the URL to match the current UI, but don't update router state + ignoreNextHistoryUpdate = true; + init.history.go(delta * -1); + + // Put the blocker into a blocked state + updateBlocker(blockerKey, { + state: "blocked", + location, + proceed() { + updateBlocker(blockerKey!, { + state: "proceeding", + proceed: undefined, + reset: undefined, + location, + }); + // Re-do the same POP navigation we just blocked + init.history.go(delta); + }, + reset() { + deleteBlocker(blockerKey!); + updateState({ blockers: new Map(router.state.blockers) }); + }, + }); + return; + } + + return startNavigation(historyAction, location); + } ); // Kick off initial data load if needed. Use Pop to avoid modifying history @@ -707,6 +834,7 @@ export function createRouter(init: RouterInit): Router { subscribers.clear(); pendingNavigationController && pendingNavigationController.abort(); state.fetchers.forEach((_, key) => deleteFetcher(key)); + state.blockers.forEach((_, key) => deleteBlocker(key)); } // Subscribe to state updates for the router @@ -771,6 +899,12 @@ export function createRouter(init: RouterInit): Router { ) : state.loaderData; + // On a successful navigation we can assume we got through all blockers + // so we can start fresh + for (let [key] of blockerFunctions) { + deleteBlocker(key); + } + updateState({ ...newState, // matches, errors, fetchers go through as-is actionData, @@ -785,6 +919,7 @@ export function createRouter(init: RouterInit): Router { ? false : getSavedScrollPosition(location, newState.matches || state.matches), preventScrollReset: pendingPreventScrollReset, + blockers: new Map(state.blockers), }); if (isUninterruptedRevalidation) { @@ -819,16 +954,17 @@ export function createRouter(init: RouterInit): Router { let { path, submission, error } = normalizeNavigateOptions(to, opts); - let location = createLocation(state.location, path, opts && opts.state); + let currentLocation = state.location; + let nextLocation = createLocation(state.location, path, opts && opts.state); // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded // URL from window.location, so we need to encode it here so the behavior // remains the same as POP and non-data-router usages. new URL() does all // the same encoding we'd get from a history.pushState/window.location read // without having to touch history - location = { - ...location, - ...init.history.encodeLocation(location), + nextLocation = { + ...nextLocation, + ...init.history.encodeLocation(nextLocation), }; let userReplace = opts && opts.replace != null ? opts.replace : undefined; @@ -856,7 +992,35 @@ export function createRouter(init: RouterInit): Router { ? opts.preventScrollReset === true : undefined; - return await startNavigation(historyAction, location, { + let blockerKey = shouldBlockNavigation({ + currentLocation, + nextLocation, + historyAction, + }); + if (blockerKey) { + // Put the blocker into a blocked state + updateBlocker(blockerKey, { + state: "blocked", + location: nextLocation, + proceed() { + updateBlocker(blockerKey!, { + state: "proceeding", + proceed: undefined, + reset: undefined, + location: nextLocation, + }); + // Send the same navigation through + navigate(to, opts); + }, + reset() { + deleteBlocker(blockerKey!); + updateState({ blockers: new Map(state.blockers) }); + }, + }); + return; + } + + return await startNavigation(historyAction, nextLocation, { submission, // Send through the formData serialization error if we have one so we can // render at the right error boundary after we match routes @@ -1926,6 +2090,84 @@ export function createRouter(init: RouterInit): Router { return yeetedKeys.length > 0; } + function getBlocker(key: string, fn: BlockerFunction) { + let blocker: Blocker = state.blockers.get(key) || IDLE_BLOCKER; + + if (blockerFunctions.get(key) !== fn) { + blockerFunctions.set(key, fn); + if (activeBlocker == null) { + // This is now the active blocker + activeBlocker = key; + } else if (key !== activeBlocker) { + warning(false, "A router only supports one blocker at a time"); + } + } + + return blocker; + } + + function deleteBlocker(key: string) { + state.blockers.delete(key); + blockerFunctions.delete(key); + if (activeBlocker === key) { + activeBlocker = null; + } + } + + // Utility function to update blockers, ensuring valid state transitions + function updateBlocker(key: string, newBlocker: Blocker) { + let blocker = state.blockers.get(key) || IDLE_BLOCKER; + + // Poor mans state machine :) + // https://mermaid.live/edit#pako:eNqVkc9OwzAMxl8l8nnjAYrEtDIOHEBIgwvKJTReGy3_lDpIqO27k6awMG0XcrLlnz87nwdonESogKXXBuE79rq75XZO3-yHds0RJVuv70YrPlUrCEe2HfrORS3rubqZfuhtpg5C9wk5tZ4VKcRUq88q9Z8RS0-48cE1iHJkL0ugbHuFLus9L6spZy8nX9MP2CNdomVaposqu3fGayT8T8-jJQwhepo_UtpgBQaDEUom04dZhAN1aJBDlUKJBxE1ceB2Smj0Mln-IBW5AFU2dwUiktt_2Qaq2dBfaKdEup85UV7Yd-dKjlnkabl2Pvr0DTkTreM + invariant( + (blocker.state === "unblocked" && newBlocker.state === "blocked") || + (blocker.state === "blocked" && newBlocker.state === "blocked") || + (blocker.state === "blocked" && newBlocker.state === "proceeding") || + (blocker.state === "blocked" && newBlocker.state === "unblocked") || + (blocker.state === "proceeding" && newBlocker.state === "unblocked"), + `Invalid blocker state transition: ${blocker.state} -> ${newBlocker.state}` + ); + + state.blockers.set(key, newBlocker); + updateState({ blockers: new Map(state.blockers) }); + } + + function shouldBlockNavigation({ + currentLocation, + nextLocation, + historyAction, + }: { + currentLocation: Location; + nextLocation: Location; + historyAction: HistoryAction; + }): string | undefined { + if (activeBlocker == null) { + return; + } + + // We only allow a single blocker at the moment. This will need to be + // updated if we enhance to support multiple blockers in the future + let blockerFunction = blockerFunctions.get(activeBlocker); + invariant( + blockerFunction, + "Could not find a function for the active blocker" + ); + let blocker = state.blockers.get(activeBlocker); + + if (blocker && blocker.state === "proceeding") { + // If the blocker is currently proceeding, we don't need to re-check + // it and can let this navigation continue + return; + } + + // At this point, we know we're unblocked/blocked so we need to check the + // user-provided blocker function + if (blockerFunction({ currentLocation, nextLocation, historyAction })) { + return activeBlocker; + } + } + function cancelActiveDeferreds( predicate?: (routeId: string) => boolean ): string[] { @@ -2025,6 +2267,8 @@ export function createRouter(init: RouterInit): Router { getFetcher, deleteFetcher, dispose, + getBlocker, + deleteBlocker, _internalFetchControllers: fetchControllers, _internalActiveDeferreds: activeDeferreds, }; diff --git a/packages/router/utils.ts b/packages/router/utils.ts index aca20ae7f7..1a01048c3a 100644 --- a/packages/router/utils.ts +++ b/packages/router/utils.ts @@ -900,7 +900,7 @@ export function warning(cond: any, message: string): void { if (typeof console !== "undefined") console.warn(message); try { - // Welcome to debugging React Router! + // Welcome to debugging @remix-run/router! // // This error is thrown as a convenience so you can more easily // find the source for a warning that appears in the console by diff --git a/rollup.config.js b/rollup.config.js index d4f780327f..ba258c604f 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -2,9 +2,15 @@ const fs = require("fs"); const path = require("path"); module.exports = function rollup(options) { - return fs - .readdirSync("packages") + return [ + "router", + "react-router", + "react-router-dom", + "react-router-dom-v5-compat", + "react-router-native", + ] .flatMap((dir) => { + // if (dir !== "router") return null; let configPath = path.join("packages", dir, "rollup.config.js"); try { fs.readFileSync(configPath); From 2cd8266765925f8e4651d7caf42ebe60ec8e163a Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Fri, 13 Jan 2023 21:23:09 +0000 Subject: [PATCH 3/3] chore: format --- packages/router/__tests__/navigation-blocking-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/router/__tests__/navigation-blocking-test.ts b/packages/router/__tests__/navigation-blocking-test.ts index e0fdc616f2..5b010f8a1f 100644 --- a/packages/router/__tests__/navigation-blocking-test.ts +++ b/packages/router/__tests__/navigation-blocking-test.ts @@ -310,7 +310,7 @@ describe("navigation blocking", () => { state: "unblocked", proceed: undefined, reset: undefined, - location: undefined + location: undefined, }); });