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);