diff --git a/.github/workflows/postrelease.yml b/.github/workflows/postrelease.yml new file mode 100644 index 0000000000..f865eebcda --- /dev/null +++ b/.github/workflows/postrelease.yml @@ -0,0 +1,17 @@ +name: 🕊 Post-release + +on: + push: + tags: + # only run on `react-router` tags + - "react-router@*" + +jobs: + comment: + name: 📝 Comment on related issues and pull requests + if: github.repository == 'remix-run/react-router' + uses: ./.github/workflows/release-comments.yml + with: + ref: ${{ github.ref }} + # this should match the above tag to watch excluding the trailing "@" + packageVersionToFollow: "react-router" diff --git a/.github/workflows/release-comments.yml b/.github/workflows/release-comments.yml index e3365b45e6..a23c83e1b2 100644 --- a/.github/workflows/release-comments.yml +++ b/.github/workflows/release-comments.yml @@ -6,6 +6,9 @@ on: ref: required: true type: string + packageVersionToFollow: + required: true + type: string jobs: comment: @@ -20,16 +23,20 @@ jobs: uses: actions/setup-node@v3 with: node-version-file: ".nvmrc" - cache: "yarn" + cache: "npm" + cache-dependency-path: scripts/release/package-lock.json - name: 📥 Install deps - # even though this is called "npm-install" it does use yarn to install - # because we have a yarn.lock and caches efficiently. - uses: bahmutov/npm-install@v1 + run: npm ci + working-directory: ./scripts/release - name: 📝 Comment on issues - run: node ./scripts/release/comment.mjs + working-directory: ./scripts/release + run: node -r esbuild-register ./comment.ts env: GITHUB_REPOSITORY: ${{ github.repository }} GITHUB_TOKEN: ${{ github.token }} VERSION: ${{ inputs.ref }} + DEFAULT_BRANCH: "main" + NIGHTLY_BRANCH: "dev" + PACKAGE_VERSION_TO_FOLLOW: ${{ inputs.packageVersionToFollow }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 390f48e12d..46f630529a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,7 +55,7 @@ jobs: publish: yarn release createGithubReleases: false env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN_SO_OTHER_ACTIONS_RUN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # comment: diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 2372e811c6..3a5891b860 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -37,6 +37,11 @@ You may need to make changes to a pre-release prior to publishing a final stable - Commit the unpublished changesets and push the the `release-*` branch to GitHub; wait for the release workflow to finish and the Changesets action to open its PR that will increment all versions to stable - Review the updated `CHANGELOG` files and make any adjustments necessary, then merge the PR - Once the PR is merged, the release workflow will publish the updated packages to npm +- Once the release is published: + - merge the `release-*` branch into `main` and push it up to github + - merge the `release-*` branch into `dev` and push it up to github + - Convert the `react-router@6.x.y` tag to a Release on Github with the name `v6.x.y` + - Copy the relevant changelog entries from all packages into the Release Notes and adjust accordingly, matching the format used by prior releases ### Experimental releases diff --git a/contributors.yml b/contributors.yml index 5fc89c85cf..f6d0b57fb9 100644 --- a/contributors.yml +++ b/contributors.yml @@ -1,10 +1,11 @@ - abdallah-nour - abhi-kr-2100 -- adamdotjs - AchThomas +- adamdotjs - Ajayff4 - alany411 - alexlbr +- auaustorg-ms - AmRo045 - andreiduca - arnassavickas @@ -18,6 +19,7 @@ - brockross - brophdawg11 - btav +- bvangraafeiland - CanRau - chaance - chasinhues @@ -31,6 +33,7 @@ - danielberndt - dauletbaev - david-crespo +- dmitrytarassov - dokeet - Drishtantr - edwin177 @@ -57,6 +60,7 @@ - infoxicator - IsaiStormBlesed - Isammoc +- ivanjeremic - jacob-ebey - JaffParker - JakubDrozd @@ -74,7 +78,9 @@ - KostiantynPopovych - KutnerUri - latin-1 +- lequangdongg - liuhanqu +- lopezac - loun4 - lqze - lukerSpringTree @@ -82,6 +88,7 @@ - manzano78 - marc2332 - markivancho +- maruffahmed - marvinruder - maxpou - mcansh @@ -93,6 +100,7 @@ - morleytatro - ms10596 - noisypigeon +- omar-moquete - p13i - parched - paulsmithkc @@ -128,6 +136,7 @@ - underager - vijaypushkin - vikingviolinist +- vishwast03 - willemarcel - williamsdyyz - xavier-lc diff --git a/docs/components/await.md b/docs/components/await.md index fed73662fa..809691cbf2 100644 --- a/docs/components/await.md +++ b/docs/components/await.md @@ -108,7 +108,7 @@ import { defer, Route, useLoaderData, - Async, + Await, } from "react-router-dom"; // given this route diff --git a/docs/components/form.md b/docs/components/form.md index b062dec138..d634b9e858 100644 --- a/docs/components/form.md +++ b/docs/components/form.md @@ -128,7 +128,7 @@ The method will be available on [`request.method`][requestmethod] inside the rou path="/projects/:id" element={} loader={async ({ params }) => { - return fakeLoadProject(params.id) + return fakeLoadProject(params.id); }} action={async ({ request, params }) => { switch (request.method) { @@ -140,8 +140,8 @@ The method will be available on [`request.method`][requestmethod] inside the rou case "delete": { return fakeDeleteProject(params.id); } - default { - throw new Response("", { status: 405 }) + default: { + throw new Response("", { status: 405 }); } } }} diff --git a/docs/guides/deferred.md b/docs/guides/deferred.md index aed1fd6ee8..9698799b7e 100644 --- a/docs/guides/deferred.md +++ b/docs/guides/deferred.md @@ -205,6 +205,6 @@ So just keep this in mind: **Deferred is 100% only about the initial load of a r [link]: ../components/link [usefetcher]: ../hooks/use-fetcher -[defer response]: ../fetch/defer +[defer response]: ../utils/defer [await]: ../components/await -[useasyncvalue]: ../hooks/use-async-data +[useasyncvalue]: ../hooks/use-async-value diff --git a/docs/hooks/use-outlet-context.md b/docs/hooks/use-outlet-context.md index f55eb9b848..222a0850a0 100644 --- a/docs/hooks/use-outlet-context.md +++ b/docs/hooks/use-outlet-context.md @@ -24,7 +24,7 @@ function Parent() { } ``` -```tsx lines=[2] +```tsx lines=[4] import { useOutletContext } from "react-router-dom"; function Child() { @@ -36,7 +36,7 @@ function Child() { If you're using TypeScript, we recommend the parent component provide a custom hook for accessing the context value. This makes it easier for consumers to get nice typings, control consumers, and know who's consuming the context value. Here's a more realistic example: -```tsx filename=src/routes/dashboard.tsx lines=[12,17-19] +```tsx filename=src/routes/dashboard.tsx lines=[13, 19] import * as React from "react"; import type { User } from "./types"; import { Outlet, useOutletContext } from "react-router-dom"; diff --git a/docs/hooks/use-route-error.md b/docs/hooks/use-route-error.md index 377cdcc800..587f0f149b 100644 --- a/docs/hooks/use-route-error.md +++ b/docs/hooks/use-route-error.md @@ -5,7 +5,7 @@ new: true # `useRouteError` -Inside of an [`errorElement`][errorelement], this hooks returns anything thrown during an action, loader, or rendering. Note that thrown responses have special treatment, see [`isRouteErrorResponse`][isrouteerrorresponse] for more information. +Inside of an [`errorElement`][errorelement], this hook returns anything thrown during an action, loader, or rendering. Note that thrown responses have special treatment, see [`isRouteErrorResponse`][isrouteerrorresponse] for more information. This feature only works if using a data router, see [Picking a Router][pickingarouter] diff --git a/docs/route/loader.md b/docs/route/loader.md index 43314f839d..5951cec118 100644 --- a/docs/route/loader.md +++ b/docs/route/loader.md @@ -89,7 +89,7 @@ Note that the APIs here are not React Router specific, but rather standard web o While you can return anything you want from a loader and get access to it from [`useLoaderData`][useloaderdata], you can also return a web [Response][response]. -This might not seem immediately useful, but consider `fetch`. Since the return value of of `fetch` is a Response, and loaders understand responses, many loaders can return a simple fetch! +This might not seem immediately useful, but consider `fetch`. Since the return value of `fetch` is a Response, and loaders understand responses, many loaders can return a simple fetch! ```tsx // an HTTP/REST API diff --git a/docs/start/tutorial.md b/docs/start/tutorial.md index 683cab2ab6..9b65d68ee2 100644 --- a/docs/start/tutorial.md +++ b/docs/start/tutorial.md @@ -1830,7 +1830,7 @@ If you click the button now you should see the star _immediately_ change to the ## Not Found Data -What happens if the contact we're trying load doesn't exist? +What happens if the contact we're trying to load doesn't exist? diff --git a/docs/upgrading/v5.md b/docs/upgrading/v5.md index e1cc3b825b..08c15015b4 100644 --- a/docs/upgrading/v5.md +++ b/docs/upgrading/v5.md @@ -922,8 +922,8 @@ import { matchPath } from "react-router-dom"; const match = matchPath( { path: "/users/:id", - caseSensitive: false, // Optional. Should be `true` if the static portions of the `path` should be matched in the same case. - end: true, // Optional. Should be `true` if this pattern should match the entire URL pathname + caseSensitive: false, // Optional, `true` == static parts of `path` should match case + end: true, // Optional, `true` == pattern should match the entire URL pathname }, "/users/123" ); diff --git a/examples/scoped-memory-router/.gitignore b/examples/scoped-memory-router/.gitignore new file mode 100644 index 0000000000..d451ff16c1 --- /dev/null +++ b/examples/scoped-memory-router/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local diff --git a/examples/scoped-memory-router/.stackblitzrc b/examples/scoped-memory-router/.stackblitzrc new file mode 100644 index 0000000000..d98146f4d0 --- /dev/null +++ b/examples/scoped-memory-router/.stackblitzrc @@ -0,0 +1,4 @@ +{ + "installDependencies": true, + "startCommand": "npm run dev" +} diff --git a/examples/scoped-memory-router/README.md b/examples/scoped-memory-router/README.md new file mode 100644 index 0000000000..dd35c152d4 --- /dev/null +++ b/examples/scoped-memory-router/README.md @@ -0,0 +1,21 @@ +--- +title: Basics +toc: false +order: 1 +--- + +# Basic Example + +This example demonstrates some of the basic features of React Router, including: + +- Layouts and nested ``s +- Index ``s +- Catch-all ``s +- Using `` as a placeholder for child routes +- Using ``s for navigation + +## Preview + +Open this example on [StackBlitz](https://stackblitz.com): + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/basic?file=src/App.tsx) diff --git a/examples/scoped-memory-router/index.html b/examples/scoped-memory-router/index.html new file mode 100644 index 0000000000..a8652d215a --- /dev/null +++ b/examples/scoped-memory-router/index.html @@ -0,0 +1,12 @@ + + + + + + React Router - Basic Example + + +
+ + + diff --git a/examples/scoped-memory-router/package-lock.json b/examples/scoped-memory-router/package-lock.json new file mode 100644 index 0000000000..375ba16dba --- /dev/null +++ b/examples/scoped-memory-router/package-lock.json @@ -0,0 +1,2351 @@ +{ + "name": "basic", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "basic", + "dependencies": { + "history": "^5.2.0", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-router": "6.4.0-pre.2", + "react-router-dom": "6.4.0-pre.2" + }, + "devDependencies": { + "@rollup/plugin-replace": "^2.2.1", + "@types/node": "14.x", + "@types/react": "^17.0.19", + "@types/react-dom": "^17.0.9", + "@vitejs/plugin-react": "^1.0.1", + "typescript": "^4.3.5", + "vite": "^2.5.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.15.0.tgz", + "integrity": "sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.15.8.tgz", + "integrity": "sha512-3UG9dsxvYBMYwRv+gS41WKHno4K60/9GPy1CJaH6xy3Elq8CTtvtjT5R5jmNhXfCYLX2mTw+7/aq5ak/gOE0og==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.15.8", + "@babel/generator": "^7.15.8", + "@babel/helper-compilation-targets": "^7.15.4", + "@babel/helper-module-transforms": "^7.15.8", + "@babel/helpers": "^7.15.4", + "@babel/parser": "^7.15.8", + "@babel/template": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.6", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "semver": "^6.3.0", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.8.tgz", + "integrity": "sha512-ECmAKstXbp1cvpTTZciZCgfOt6iN64lR0d+euv3UZisU5awfRawOvg07Utn/qBGuH4bRIEZKrA/4LzZyXhZr8g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.6", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.15.4.tgz", + "integrity": "sha512-QwrtdNvUNsPCj2lfNQacsGSQvGX8ee1ttrBrcozUP2Sv/jylewBP/8QFe6ZkBsC8T/GYWonNAWJV4aRR9AL2DA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.4.tgz", + "integrity": "sha512-rMWPCirulnPSe4d+gwdWXLfAXTTBj8M3guAf5xFQJ0nvFY7tfNAFnWdqaHegHlgDZOCT4qvhF3BYlSJag8yhqQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.15.0", + "@babel/helper-validator-option": "^7.14.5", + "browserslist": "^4.16.6", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz", + "integrity": "sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==", + "dev": true, + "dependencies": { + "@babel/helper-get-function-arity": "^7.15.4", + "@babel/template": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-get-function-arity": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz", + "integrity": "sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.15.4.tgz", + "integrity": "sha512-VTy085egb3jUGVK9ycIxQiPbquesq0HUQ+tPO0uv5mPEBZipk+5FkRKiWq5apuyTE9FUrjENB0rCf8y+n+UuhA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.4.tgz", + "integrity": "sha512-cokOMkxC/BTyNP1AlY25HuBWM32iCEsLPI4BHDpJCHHm1FU2E7dKWWIXJgQgSFiu4lp8q3bL1BIKwqkSUviqtA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.15.4.tgz", + "integrity": "sha512-jeAHZbzUwdW/xHgHQ3QmWR4Jg6j15q4w/gCfwZvtqOxoo5DKtLHk8Bsf4c5RZRC7NmLEs+ohkdq8jFefuvIxAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.8.tgz", + "integrity": "sha512-DfAfA6PfpG8t4S6npwzLvTUpp0sS7JrcuaMiy1Y5645laRJIp/LiLGIBbQKaXSInK8tiGNI7FL7L8UvB8gdUZg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.15.4", + "@babel/helper-replace-supers": "^7.15.4", + "@babel/helper-simple-access": "^7.15.4", + "@babel/helper-split-export-declaration": "^7.15.4", + "@babel/helper-validator-identifier": "^7.15.7", + "@babel/template": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.15.4.tgz", + "integrity": "sha512-E/z9rfbAOt1vDW1DR7k4SzhzotVV5+qMciWV6LaG1g4jeFrkDlJedjtV4h0i4Q/ITnUu+Pk08M7fczsB9GXBDw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", + "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.15.4.tgz", + "integrity": "sha512-/ztT6khaXF37MS47fufrKvIsiQkx1LBRvSJNzRqmbyeZnTwU9qBxXYLaaT/6KaxfKhjs2Wy8kG8ZdsFUuWBjzw==", + "dev": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.15.4", + "@babel/helper-optimise-call-expression": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.15.4.tgz", + "integrity": "sha512-UzazrDoIVOZZcTeHHEPYrr1MvTR/K+wgLg6MY6e1CJyaRhbibftF6fR2KU2sFRtI/nERUZR9fBd6aKgBlIBaPg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz", + "integrity": "sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz", + "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.15.4.tgz", + "integrity": "sha512-V45u6dqEJ3w2rlryYYXf6i9rQ5YMNu4FLS6ngs8ikblhu2VdR1AqAd6aJjBzmf2Qzh6KOLqKHxEN9+TFbAkAVQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.8.tgz", + "integrity": "sha512-BRYa3wcQnjS/nqI8Ac94pYYpJfojHVvVXJ97+IDCImX4Jc8W8Xv1+47enbruk+q1etOpsQNwnfFcNGw+gtPGxA==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.14.5.tgz", + "integrity": "sha512-ohuFIsOMXJnbOMRfX7/w7LocdR6R7whhuRD4ax8IipLcLPlZGJKkBxgHp++U4N/vKyU16/YDQr2f5seajD3jIw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.14.9.tgz", + "integrity": "sha512-30PeETvS+AeD1f58i1OVyoDlVYQhap/K20ZrMjLmmzmC2AYR/G43D4sdJAaDAqCD3MYpSWbmrz3kES158QSLjw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.14.5", + "@babel/helper-module-imports": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-jsx": "^7.14.5", + "@babel/types": "^7.14.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.14.5.tgz", + "integrity": "sha512-rdwG/9jC6QybWxVe2UVOa7q6cnTpw8JRRHOxntG/h6g/guAOe6AhtQHJuJh5FwmnXIT1bdm5vC2/5huV8ZOorQ==", + "dev": true, + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.14.9.tgz", + "integrity": "sha512-Fqqu0f8zv9W+RyOnx29BX/RlEsBRANbOf5xs5oxb2aHP4FKbLXxIaVPUiCti56LAR1IixMH4EyaixhUsKqoBHw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.14.5.tgz", + "integrity": "sha512-1TpSDnD9XR/rQ2tzunBVPThF5poaYT9GqP+of8fAtguYuI/dm2RkrMBDemsxtY0XBzvW7nXjYM0hRyKX9QYj7Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz", + "integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==", + "dependencies": { + "regenerator-runtime": "^0.13.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", + "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz", + "integrity": "sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.15.4", + "@babel/helper-function-name": "^7.15.4", + "@babel/helper-hoist-variables": "^7.15.4", + "@babel/helper-split-export-declaration": "^7.15.4", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@remix-run/router": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-0.1.0.tgz", + "integrity": "sha512-mK2wZWAFsijTGIk7ZhKvEAzU7qIS6F6RhPy5AiEosaEs3mB3J2NvUWRoN3W74V+5Jju/AisT5NaM2pIbnfGXWg==" + }, + "node_modules/@rollup/plugin-replace": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", + "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", + "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": "14.17.32", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.32.tgz", + "integrity": "sha512-JcII3D5/OapPGx+eJ+Ik1SQGyt6WvuqdRfh9jUwL6/iHGjmyOriBDciBUu7lEIBTL2ijxwrR70WUnw5AEDmFvQ==", + "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": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.33.tgz", + "integrity": "sha512-pLWntxXpDPaU+RTAuSGWGSEL2FRTNyRQOjSWDke/rxRg14ncsZvx8AKWMWZqvc1UOaJIAoObdZhAWvRaHFi5rw==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "17.0.10", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.10.tgz", + "integrity": "sha512-8oz3NAUId2z/zQdFI09IMhQPNgIbiP8Lslhv39DIDamr846/0spjZK0vnrMak0iB8EKb9QFTTIdg2Wj2zH5a3g==", + "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.0.7", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-1.0.7.tgz", + "integrity": "sha512-dzxzohFOAVVXpGlFn6Uvw2xaSLp80Vjmg2e5G1XdMV266vVKrcDqg9CWP/AiJiXuubNUdgy1k4E8dNXI6WCyhw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.15.5", + "@babel/plugin-transform-react-jsx": "^7.14.9", + "@babel/plugin-transform-react-jsx-development": "^7.14.5", + "@babel/plugin-transform-react-jsx-self": "^7.14.9", + "@babel/plugin-transform-react-jsx-source": "^7.14.5", + "@rollup/pluginutils": "^4.1.1", + "react-refresh": "^0.10.0", + "resolve": "^1.20.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@vitejs/plugin-react/node_modules/@rollup/pluginutils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.1.1.tgz", + "integrity": "sha512-clDjivHqWGXi7u+0d2r2sBi4Ie6VLEAzWMIkvJLnDmxoOhBYOTfzGbOQBA32THHm11/LiJbd01tJUpJsbshSWQ==", + "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.17.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.5.tgz", + "integrity": "sha512-I3ekeB92mmpctWBoLXe0d5wPS2cBuRvvW0JyyJHMrk9/HmP2ZjrTboNAZ8iuGqaEIlKguljbQY32OkOJIRrgoA==", + "dev": true, + "dependencies": { + "caniuse-lite": "^1.0.30001271", + "electron-to-chromium": "^1.3.878", + "escalade": "^3.1.1", + "node-releases": "^2.0.1", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001272", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001272.tgz", + "integrity": "sha512-DV1j9Oot5dydyH1v28g25KoVm7l8MTxazwuiH3utWiAS6iL/9Nh//TGwqFEeqqN8nnWYQ8HHhUq+o4QPt9kvYw==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.3.884", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.884.tgz", + "integrity": "sha512-kOaCAa+biA98PwH5BpCkeUeTL6mCeg8p3Q3OhqzPyqhu/5QUnWAN2wr/3IK8xMQxIV76kfoQpP+Bn/wij/jXrg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.10.tgz", + "integrity": "sha512-0NfCsnAh5XatHIx6Cu93wpR2v6opPoOMxONYhaAoZKzGYqAE+INcDeX2wqMdcndvPQdWCuuCmvlnsh0zmbHcSQ==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "optionalDependencies": { + "esbuild-android-arm64": "0.13.10", + "esbuild-darwin-64": "0.13.10", + "esbuild-darwin-arm64": "0.13.10", + "esbuild-freebsd-64": "0.13.10", + "esbuild-freebsd-arm64": "0.13.10", + "esbuild-linux-32": "0.13.10", + "esbuild-linux-64": "0.13.10", + "esbuild-linux-arm": "0.13.10", + "esbuild-linux-arm64": "0.13.10", + "esbuild-linux-mips64le": "0.13.10", + "esbuild-linux-ppc64le": "0.13.10", + "esbuild-netbsd-64": "0.13.10", + "esbuild-openbsd-64": "0.13.10", + "esbuild-sunos-64": "0.13.10", + "esbuild-windows-32": "0.13.10", + "esbuild-windows-64": "0.13.10", + "esbuild-windows-arm64": "0.13.10" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.10.tgz", + "integrity": "sha512-1sCdVAq64yMp2Uhlu+97/enFxpmrj31QHtThz7K+/QGjbHa7JZdBdBsZCzWJuntKHZ+EU178tHYkvjaI9z5sGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/esbuild-darwin-64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.10.tgz", + "integrity": "sha512-XlL+BYZ2h9cz3opHfFgSHGA+iy/mljBFIRU9q++f9SiBXEZTb4gTW/IENAD1l9oKH0FdO9rUpyAfV+lM4uAxrg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.10.tgz", + "integrity": "sha512-RZMMqMTyActMrXKkW71IQO8B0tyQm0Bm+ZJQWNaHJchL5LlqazJi7rriwSocP+sKLszHhsyTEBBh6qPdw5g5yQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.10.tgz", + "integrity": "sha512-pf4BEN9reF3jvZEZdxljVgOv5JS4kuYFCI78xk+2HWustbLvTP0b9XXfWI/OD0ZLWbyLYZYIA+VbVe4tdAklig==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.10.tgz", + "integrity": "sha512-j9PUcuNWmlxr4/ry4dK/s6zKh42Jhh/N5qnAAj7tx3gMbkIHW0JBoVSbbgp97p88X9xgKbXx4lG2sJDhDWmsYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/esbuild-linux-32": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.10.tgz", + "integrity": "sha512-imtdHG5ru0xUUXuc2ofdtyw0fWlHYXV7JjF7oZHgmn0b+B4o4Nr6ZON3xxoo1IP8wIekW+7b9exIf/MYq0QV7w==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/esbuild-linux-64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.10.tgz", + "integrity": "sha512-O7fzQIH2e7GC98dvoTH0rad5BVLm9yU3cRWfEmryCEIFTwbNEWCEWOfsePuoGOHRtSwoVY1hPc21CJE4/9rWxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/esbuild-linux-arm": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.10.tgz", + "integrity": "sha512-R2Jij4A0K8BcmBehvQeUteQEcf24Y2YZ6mizlNFuJOBPxe3vZNmkZ4mCE7Pf1tbcqA65qZx8J3WSHeGJl9EsJA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.10.tgz", + "integrity": "sha512-bkGxN67S2n0PF4zhh87/92kBTsH2xXLuH6T5omReKhpXdJZF5SVDSk5XU/nngARzE+e6QK6isK060Dr5uobzNw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.10.tgz", + "integrity": "sha512-UDNO5snJYOLWrA2uOUxM/PVbzzh2TR7Zf2i8zCCuFlYgvAb/81XO+Tasp3YAElDpp4VGqqcpBXLtofa9nrnJGA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.10.tgz", + "integrity": "sha512-xu6J9rMWu1TcEGuEmoc8gsTrJCEPsf+QtxK4IiUZNde9r4Q4nlRVah4JVZP3hJapZgZJcxsse0XiKXh1UFdOeA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.10.tgz", + "integrity": "sha512-d+Gr0ScMC2J83Bfx/ZvJHK0UAEMncctwgjRth9d4zppYGLk/xMfFKxv5z1ib8yZpQThafq8aPm8AqmFIJrEesw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ] + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.10.tgz", + "integrity": "sha512-OuCYc+bNKumBvxflga+nFzZvxsgmWQW+z4rMGIjM5XIW0nNbGgRc5p/0PSDv0rTdxAmwCpV69fezal0xjrDaaA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/esbuild-sunos-64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.10.tgz", + "integrity": "sha512-gUkgivZK11bD56wDoLsnYrsOHD/zHzzLSdqKcIl3wRMulfHpRBpoX8gL0dbWr+8N9c+1HDdbNdvxSRmZ4RCVwg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ] + }, + "node_modules/esbuild-windows-32": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.10.tgz", + "integrity": "sha512-C1xJ54E56dGWRaYcTnRy7amVZ9n1/D/D2/qVw7e5EtS7p+Fv/yZxxgqyb1hMGKXgtFYX4jMpU5eWBF/AsYrn+A==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/esbuild-windows-64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.10.tgz", + "integrity": "sha512-6+EXEXopEs3SvPFAHcps2Krp/FvqXXsOQV33cInmyilb0ZBEQew4MIoZtMIyB3YXoV6//dl3i6YbPrFZaWEinQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.10.tgz", + "integrity": "sha512-xTqM/XKhORo6u9S5I0dNJWEdWoemFjogLUTVLkQMVyUV3ZuMChahVA+bCqKHdyX55pCFxD/8v2fm3/sfFMWN+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/history": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", + "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", + "dependencies": { + "@babel/runtime": "^7.7.6" + } + }, + "node_modules/is-core-module": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", + "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.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "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/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "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.1.30", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", + "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz", + "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", + "dev": true + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "engines": { + "node": ">=0.10.0" + } + }, + "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.3.11", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.11.tgz", + "integrity": "sha512-hCmlUAIlUiav8Xdqw3Io4LcpA1DOt7h3LSTAC4G6JGHFFaWzI6qvFt9oilvl8BmkbBRX1IhM90ZAmpk68zccQA==", + "dev": true, + "dependencies": { + "nanoid": "^3.1.30", + "picocolors": "^1.0.0", + "source-map-js": "^0.6.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/react": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", + "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "scheduler": "^0.20.2" + }, + "peerDependencies": { + "react": "17.0.2" + } + }, + "node_modules/react-refresh": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.10.0.tgz", + "integrity": "sha512-PgidR3wST3dDYKr6b4pJoqQFpPGNKDSCDx4cZoshjXipw3LzO7mG1My2pwEzz2JVkF+inx3xRpDeQLFQGH/hsQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.4.0-pre.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.4.0-pre.2.tgz", + "integrity": "sha512-JFwatrS+QFmyoOrjUNpHFeAO72Y1FUZ9wOfQoCm1a6cRglAbLXAdKll/WEGUtGUb3LwLx3uAZ57kPXHQX5vutw==", + "dependencies": { + "@remix-run/router": "^0.1.0", + "use-sync-external-store": "1.1.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.4.0-pre.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.4.0-pre.2.tgz", + "integrity": "sha512-Y6tOV4zs7uS+7pTbMoOcMWP9H5hVKINIyLIO1oWVVQEKv2KDEQx0njyPnhso4LkbOK6l7JhUMNO5DLwMqZVWFQ==", + "dependencies": { + "react-router": "6.4.0-pre.2" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + }, + "node_modules/resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "dependencies": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "2.58.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.58.3.tgz", + "integrity": "sha512-ei27MSw1KhRur4p87Q0/Va2NAYqMXOX++FNEumMBcdreIRLURKy+cE2wcDJKBn0nfmhP2ZGrJkP1XPO+G8FJQw==", + "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.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "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": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", + "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==", + "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/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.4.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", + "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.1.0.tgz", + "integrity": "sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/vite": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.6.13.tgz", + "integrity": "sha512-+tGZ1OxozRirTudl4M3N3UTNJOlxdVo/qBl2IlDEy/ZpTFcskp+k5ncNjayR3bRYTCbqSOFz2JWGN1UmuDMScA==", + "dev": true, + "dependencies": { + "esbuild": "^0.13.2", + "postcss": "^8.3.8", + "resolve": "^1.20.0", + "rollup": "^2.57.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": { + "@babel/code-frame": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.14.5" + } + }, + "@babel/compat-data": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.15.0.tgz", + "integrity": "sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA==", + "dev": true + }, + "@babel/core": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.15.8.tgz", + "integrity": "sha512-3UG9dsxvYBMYwRv+gS41WKHno4K60/9GPy1CJaH6xy3Elq8CTtvtjT5R5jmNhXfCYLX2mTw+7/aq5ak/gOE0og==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.15.8", + "@babel/generator": "^7.15.8", + "@babel/helper-compilation-targets": "^7.15.4", + "@babel/helper-module-transforms": "^7.15.8", + "@babel/helpers": "^7.15.4", + "@babel/parser": "^7.15.8", + "@babel/template": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.6", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "semver": "^6.3.0", + "source-map": "^0.5.0" + } + }, + "@babel/generator": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.8.tgz", + "integrity": "sha512-ECmAKstXbp1cvpTTZciZCgfOt6iN64lR0d+euv3UZisU5awfRawOvg07Utn/qBGuH4bRIEZKrA/4LzZyXhZr8g==", + "dev": true, + "requires": { + "@babel/types": "^7.15.6", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.15.4.tgz", + "integrity": "sha512-QwrtdNvUNsPCj2lfNQacsGSQvGX8ee1ttrBrcozUP2Sv/jylewBP/8QFe6ZkBsC8T/GYWonNAWJV4aRR9AL2DA==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.4.tgz", + "integrity": "sha512-rMWPCirulnPSe4d+gwdWXLfAXTTBj8M3guAf5xFQJ0nvFY7tfNAFnWdqaHegHlgDZOCT4qvhF3BYlSJag8yhqQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.15.0", + "@babel/helper-validator-option": "^7.14.5", + "browserslist": "^4.16.6", + "semver": "^6.3.0" + } + }, + "@babel/helper-function-name": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz", + "integrity": "sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.15.4", + "@babel/template": "^7.15.4", + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz", + "integrity": "sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.15.4.tgz", + "integrity": "sha512-VTy085egb3jUGVK9ycIxQiPbquesq0HUQ+tPO0uv5mPEBZipk+5FkRKiWq5apuyTE9FUrjENB0rCf8y+n+UuhA==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.4.tgz", + "integrity": "sha512-cokOMkxC/BTyNP1AlY25HuBWM32iCEsLPI4BHDpJCHHm1FU2E7dKWWIXJgQgSFiu4lp8q3bL1BIKwqkSUviqtA==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-module-imports": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.15.4.tgz", + "integrity": "sha512-jeAHZbzUwdW/xHgHQ3QmWR4Jg6j15q4w/gCfwZvtqOxoo5DKtLHk8Bsf4c5RZRC7NmLEs+ohkdq8jFefuvIxAA==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-module-transforms": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.8.tgz", + "integrity": "sha512-DfAfA6PfpG8t4S6npwzLvTUpp0sS7JrcuaMiy1Y5645laRJIp/LiLGIBbQKaXSInK8tiGNI7FL7L8UvB8gdUZg==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.15.4", + "@babel/helper-replace-supers": "^7.15.4", + "@babel/helper-simple-access": "^7.15.4", + "@babel/helper-split-export-declaration": "^7.15.4", + "@babel/helper-validator-identifier": "^7.15.7", + "@babel/template": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.6" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.15.4.tgz", + "integrity": "sha512-E/z9rfbAOt1vDW1DR7k4SzhzotVV5+qMciWV6LaG1g4jeFrkDlJedjtV4h0i4Q/ITnUu+Pk08M7fczsB9GXBDw==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", + "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", + "dev": true + }, + "@babel/helper-replace-supers": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.15.4.tgz", + "integrity": "sha512-/ztT6khaXF37MS47fufrKvIsiQkx1LBRvSJNzRqmbyeZnTwU9qBxXYLaaT/6KaxfKhjs2Wy8kG8ZdsFUuWBjzw==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.15.4", + "@babel/helper-optimise-call-expression": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-simple-access": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.15.4.tgz", + "integrity": "sha512-UzazrDoIVOZZcTeHHEPYrr1MvTR/K+wgLg6MY6e1CJyaRhbibftF6fR2KU2sFRtI/nERUZR9fBd6aKgBlIBaPg==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz", + "integrity": "sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz", + "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==", + "dev": true + }, + "@babel/helpers": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.15.4.tgz", + "integrity": "sha512-V45u6dqEJ3w2rlryYYXf6i9rQ5YMNu4FLS6ngs8ikblhu2VdR1AqAd6aJjBzmf2Qzh6KOLqKHxEN9+TFbAkAVQ==", + "dev": true, + "requires": { + "@babel/template": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.4" + } + }, + "@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.8.tgz", + "integrity": "sha512-BRYa3wcQnjS/nqI8Ac94pYYpJfojHVvVXJ97+IDCImX4Jc8W8Xv1+47enbruk+q1etOpsQNwnfFcNGw+gtPGxA==", + "dev": true + }, + "@babel/plugin-syntax-jsx": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.14.5.tgz", + "integrity": "sha512-ohuFIsOMXJnbOMRfX7/w7LocdR6R7whhuRD4ax8IipLcLPlZGJKkBxgHp++U4N/vKyU16/YDQr2f5seajD3jIw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-react-jsx": { + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.14.9.tgz", + "integrity": "sha512-30PeETvS+AeD1f58i1OVyoDlVYQhap/K20ZrMjLmmzmC2AYR/G43D4sdJAaDAqCD3MYpSWbmrz3kES158QSLjw==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.14.5", + "@babel/helper-module-imports": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-jsx": "^7.14.5", + "@babel/types": "^7.14.9" + } + }, + "@babel/plugin-transform-react-jsx-development": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.14.5.tgz", + "integrity": "sha512-rdwG/9jC6QybWxVe2UVOa7q6cnTpw8JRRHOxntG/h6g/guAOe6AhtQHJuJh5FwmnXIT1bdm5vC2/5huV8ZOorQ==", + "dev": true, + "requires": { + "@babel/plugin-transform-react-jsx": "^7.14.5" + } + }, + "@babel/plugin-transform-react-jsx-self": { + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.14.9.tgz", + "integrity": "sha512-Fqqu0f8zv9W+RyOnx29BX/RlEsBRANbOf5xs5oxb2aHP4FKbLXxIaVPUiCti56LAR1IixMH4EyaixhUsKqoBHw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-react-jsx-source": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.14.5.tgz", + "integrity": "sha512-1TpSDnD9XR/rQ2tzunBVPThF5poaYT9GqP+of8fAtguYuI/dm2RkrMBDemsxtY0XBzvW7nXjYM0hRyKX9QYj7Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/runtime": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz", + "integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@babel/template": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", + "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4" + } + }, + "@babel/traverse": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz", + "integrity": "sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.15.4", + "@babel/helper-function-name": "^7.15.4", + "@babel/helper-hoist-variables": "^7.15.4", + "@babel/helper-split-export-declaration": "^7.15.4", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + } + }, + "@remix-run/router": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-0.1.0.tgz", + "integrity": "sha512-mK2wZWAFsijTGIk7ZhKvEAzU7qIS6F6RhPy5AiEosaEs3mB3J2NvUWRoN3W74V+5Jju/AisT5NaM2pIbnfGXWg==" + }, + "@rollup/plugin-replace": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", + "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", + "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": "14.17.32", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.32.tgz", + "integrity": "sha512-JcII3D5/OapPGx+eJ+Ik1SQGyt6WvuqdRfh9jUwL6/iHGjmyOriBDciBUu7lEIBTL2ijxwrR70WUnw5AEDmFvQ==", + "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": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.33.tgz", + "integrity": "sha512-pLWntxXpDPaU+RTAuSGWGSEL2FRTNyRQOjSWDke/rxRg14ncsZvx8AKWMWZqvc1UOaJIAoObdZhAWvRaHFi5rw==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "17.0.10", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.10.tgz", + "integrity": "sha512-8oz3NAUId2z/zQdFI09IMhQPNgIbiP8Lslhv39DIDamr846/0spjZK0vnrMak0iB8EKb9QFTTIdg2Wj2zH5a3g==", + "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.0.7", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-1.0.7.tgz", + "integrity": "sha512-dzxzohFOAVVXpGlFn6Uvw2xaSLp80Vjmg2e5G1XdMV266vVKrcDqg9CWP/AiJiXuubNUdgy1k4E8dNXI6WCyhw==", + "dev": true, + "requires": { + "@babel/core": "^7.15.5", + "@babel/plugin-transform-react-jsx": "^7.14.9", + "@babel/plugin-transform-react-jsx-development": "^7.14.5", + "@babel/plugin-transform-react-jsx-self": "^7.14.9", + "@babel/plugin-transform-react-jsx-source": "^7.14.5", + "@rollup/pluginutils": "^4.1.1", + "react-refresh": "^0.10.0", + "resolve": "^1.20.0" + }, + "dependencies": { + "@rollup/pluginutils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.1.1.tgz", + "integrity": "sha512-clDjivHqWGXi7u+0d2r2sBi4Ie6VLEAzWMIkvJLnDmxoOhBYOTfzGbOQBA32THHm11/LiJbd01tJUpJsbshSWQ==", + "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.17.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.5.tgz", + "integrity": "sha512-I3ekeB92mmpctWBoLXe0d5wPS2cBuRvvW0JyyJHMrk9/HmP2ZjrTboNAZ8iuGqaEIlKguljbQY32OkOJIRrgoA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001271", + "electron-to-chromium": "^1.3.878", + "escalade": "^3.1.1", + "node-releases": "^2.0.1", + "picocolors": "^1.0.0" + } + }, + "caniuse-lite": { + "version": "1.0.30001272", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001272.tgz", + "integrity": "sha512-DV1j9Oot5dydyH1v28g25KoVm7l8MTxazwuiH3utWiAS6iL/9Nh//TGwqFEeqqN8nnWYQ8HHhUq+o4QPt9kvYw==", + "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.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "electron-to-chromium": { + "version": "1.3.884", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.884.tgz", + "integrity": "sha512-kOaCAa+biA98PwH5BpCkeUeTL6mCeg8p3Q3OhqzPyqhu/5QUnWAN2wr/3IK8xMQxIV76kfoQpP+Bn/wij/jXrg==", + "dev": true + }, + "esbuild": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.10.tgz", + "integrity": "sha512-0NfCsnAh5XatHIx6Cu93wpR2v6opPoOMxONYhaAoZKzGYqAE+INcDeX2wqMdcndvPQdWCuuCmvlnsh0zmbHcSQ==", + "dev": true, + "requires": { + "esbuild-android-arm64": "0.13.10", + "esbuild-darwin-64": "0.13.10", + "esbuild-darwin-arm64": "0.13.10", + "esbuild-freebsd-64": "0.13.10", + "esbuild-freebsd-arm64": "0.13.10", + "esbuild-linux-32": "0.13.10", + "esbuild-linux-64": "0.13.10", + "esbuild-linux-arm": "0.13.10", + "esbuild-linux-arm64": "0.13.10", + "esbuild-linux-mips64le": "0.13.10", + "esbuild-linux-ppc64le": "0.13.10", + "esbuild-netbsd-64": "0.13.10", + "esbuild-openbsd-64": "0.13.10", + "esbuild-sunos-64": "0.13.10", + "esbuild-windows-32": "0.13.10", + "esbuild-windows-64": "0.13.10", + "esbuild-windows-arm64": "0.13.10" + } + }, + "esbuild-android-arm64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.10.tgz", + "integrity": "sha512-1sCdVAq64yMp2Uhlu+97/enFxpmrj31QHtThz7K+/QGjbHa7JZdBdBsZCzWJuntKHZ+EU178tHYkvjaI9z5sGg==", + "dev": true, + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.10.tgz", + "integrity": "sha512-XlL+BYZ2h9cz3opHfFgSHGA+iy/mljBFIRU9q++f9SiBXEZTb4gTW/IENAD1l9oKH0FdO9rUpyAfV+lM4uAxrg==", + "dev": true, + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.10.tgz", + "integrity": "sha512-RZMMqMTyActMrXKkW71IQO8B0tyQm0Bm+ZJQWNaHJchL5LlqazJi7rriwSocP+sKLszHhsyTEBBh6qPdw5g5yQ==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.10.tgz", + "integrity": "sha512-pf4BEN9reF3jvZEZdxljVgOv5JS4kuYFCI78xk+2HWustbLvTP0b9XXfWI/OD0ZLWbyLYZYIA+VbVe4tdAklig==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.10.tgz", + "integrity": "sha512-j9PUcuNWmlxr4/ry4dK/s6zKh42Jhh/N5qnAAj7tx3gMbkIHW0JBoVSbbgp97p88X9xgKbXx4lG2sJDhDWmsYQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-32": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.10.tgz", + "integrity": "sha512-imtdHG5ru0xUUXuc2ofdtyw0fWlHYXV7JjF7oZHgmn0b+B4o4Nr6ZON3xxoo1IP8wIekW+7b9exIf/MYq0QV7w==", + "dev": true, + "optional": true + }, + "esbuild-linux-64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.10.tgz", + "integrity": "sha512-O7fzQIH2e7GC98dvoTH0rad5BVLm9yU3cRWfEmryCEIFTwbNEWCEWOfsePuoGOHRtSwoVY1hPc21CJE4/9rWxQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.10.tgz", + "integrity": "sha512-R2Jij4A0K8BcmBehvQeUteQEcf24Y2YZ6mizlNFuJOBPxe3vZNmkZ4mCE7Pf1tbcqA65qZx8J3WSHeGJl9EsJA==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.10.tgz", + "integrity": "sha512-bkGxN67S2n0PF4zhh87/92kBTsH2xXLuH6T5omReKhpXdJZF5SVDSk5XU/nngARzE+e6QK6isK060Dr5uobzNw==", + "dev": true, + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.10.tgz", + "integrity": "sha512-UDNO5snJYOLWrA2uOUxM/PVbzzh2TR7Zf2i8zCCuFlYgvAb/81XO+Tasp3YAElDpp4VGqqcpBXLtofa9nrnJGA==", + "dev": true, + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.10.tgz", + "integrity": "sha512-xu6J9rMWu1TcEGuEmoc8gsTrJCEPsf+QtxK4IiUZNde9r4Q4nlRVah4JVZP3hJapZgZJcxsse0XiKXh1UFdOeA==", + "dev": true, + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.10.tgz", + "integrity": "sha512-d+Gr0ScMC2J83Bfx/ZvJHK0UAEMncctwgjRth9d4zppYGLk/xMfFKxv5z1ib8yZpQThafq8aPm8AqmFIJrEesw==", + "dev": true, + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.10.tgz", + "integrity": "sha512-OuCYc+bNKumBvxflga+nFzZvxsgmWQW+z4rMGIjM5XIW0nNbGgRc5p/0PSDv0rTdxAmwCpV69fezal0xjrDaaA==", + "dev": true, + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.10.tgz", + "integrity": "sha512-gUkgivZK11bD56wDoLsnYrsOHD/zHzzLSdqKcIl3wRMulfHpRBpoX8gL0dbWr+8N9c+1HDdbNdvxSRmZ4RCVwg==", + "dev": true, + "optional": true + }, + "esbuild-windows-32": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.10.tgz", + "integrity": "sha512-C1xJ54E56dGWRaYcTnRy7amVZ9n1/D/D2/qVw7e5EtS7p+Fv/yZxxgqyb1hMGKXgtFYX4jMpU5eWBF/AsYrn+A==", + "dev": true, + "optional": true + }, + "esbuild-windows-64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.10.tgz", + "integrity": "sha512-6+EXEXopEs3SvPFAHcps2Krp/FvqXXsOQV33cInmyilb0ZBEQew4MIoZtMIyB3YXoV6//dl3i6YbPrFZaWEinQ==", + "dev": true, + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.10.tgz", + "integrity": "sha512-xTqM/XKhORo6u9S5I0dNJWEdWoemFjogLUTVLkQMVyUV3ZuMChahVA+bCqKHdyX55pCFxD/8v2fm3/sfFMWN+g==", + "dev": true, + "optional": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "history": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", + "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", + "requires": { + "@babel/runtime": "^7.7.6" + } + }, + "is-core-module": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", + "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.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "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" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "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.1.30", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", + "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==", + "dev": true + }, + "node-releases": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz", + "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "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.3.11", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.11.tgz", + "integrity": "sha512-hCmlUAIlUiav8Xdqw3Io4LcpA1DOt7h3LSTAC4G6JGHFFaWzI6qvFt9oilvl8BmkbBRX1IhM90ZAmpk68zccQA==", + "dev": true, + "requires": { + "nanoid": "^3.1.30", + "picocolors": "^1.0.0", + "source-map-js": "^0.6.2" + } + }, + "react": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "react-dom": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", + "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "scheduler": "^0.20.2" + } + }, + "react-refresh": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.10.0.tgz", + "integrity": "sha512-PgidR3wST3dDYKr6b4pJoqQFpPGNKDSCDx4cZoshjXipw3LzO7mG1My2pwEzz2JVkF+inx3xRpDeQLFQGH/hsQ==", + "dev": true + }, + "react-router": { + "version": "6.4.0-pre.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.4.0-pre.2.tgz", + "integrity": "sha512-JFwatrS+QFmyoOrjUNpHFeAO72Y1FUZ9wOfQoCm1a6cRglAbLXAdKll/WEGUtGUb3LwLx3uAZ57kPXHQX5vutw==", + "requires": { + "@remix-run/router": "^0.1.0", + "use-sync-external-store": "1.1.0" + } + }, + "react-router-dom": { + "version": "6.4.0-pre.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.4.0-pre.2.tgz", + "integrity": "sha512-Y6tOV4zs7uS+7pTbMoOcMWP9H5hVKINIyLIO1oWVVQEKv2KDEQx0njyPnhso4LkbOK6l7JhUMNO5DLwMqZVWFQ==", + "requires": { + "react-router": "6.4.0-pre.2" + } + }, + "regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "rollup": { + "version": "2.58.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.58.3.tgz", + "integrity": "sha512-ei27MSw1KhRur4p87Q0/Va2NAYqMXOX++FNEumMBcdreIRLURKy+cE2wcDJKBn0nfmhP2ZGrJkP1XPO+G8FJQw==", + "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.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "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": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", + "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==", + "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" + } + }, + "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.4.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", + "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", + "dev": true + }, + "use-sync-external-store": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.1.0.tgz", + "integrity": "sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ==", + "requires": {} + }, + "vite": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.6.13.tgz", + "integrity": "sha512-+tGZ1OxozRirTudl4M3N3UTNJOlxdVo/qBl2IlDEy/ZpTFcskp+k5ncNjayR3bRYTCbqSOFz2JWGN1UmuDMScA==", + "dev": true, + "requires": { + "esbuild": "^0.13.2", + "fsevents": "~2.3.2", + "postcss": "^8.3.8", + "resolve": "^1.20.0", + "rollup": "^2.57.0" + } + } + } +} diff --git a/examples/scoped-memory-router/package.json b/examples/scoped-memory-router/package.json new file mode 100644 index 0000000000..4909081710 --- /dev/null +++ b/examples/scoped-memory-router/package.json @@ -0,0 +1,25 @@ +{ + "name": "basic", + "private": true, + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "serve": "vite preview" + }, + "dependencies": { + "history": "^5.2.0", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-router-dom": "6.4.0-pre.2", + "react-router": "6.4.0-pre.2" + }, + "devDependencies": { + "@rollup/plugin-replace": "^2.2.1", + "@types/node": "14.x", + "@types/react": "^17.0.19", + "@types/react-dom": "^17.0.9", + "@vitejs/plugin-react": "^1.0.1", + "typescript": "^4.3.5", + "vite": "^2.5.0" + } +} diff --git a/examples/scoped-memory-router/src/App.tsx b/examples/scoped-memory-router/src/App.tsx new file mode 100644 index 0000000000..3d93c3c1bc --- /dev/null +++ b/examples/scoped-memory-router/src/App.tsx @@ -0,0 +1,169 @@ +import * as React from "react"; +import { + Routes, + Route, + Outlet, + Link, + createScopedMemoryRouterEnvironment, +} from "react-router-dom"; + +const { + MemoryRouter: ScopedMemoryRouter, + Routes: ScopedRoutes, + Route: ScopedRoute, + useNavigate: useScopedNavigate, + Link: ScopedLink, +} = createScopedMemoryRouterEnvironment(); + +export default function App() { + return ( +
+

Scoped Memory Router Example

+ +

+ This example demonstrates using{" "} + createScopedMemoryRouterEnvironment to have nested + MemoryRouters. It leverages this feature to create a Modal routing + system inside of a BrowserRouter +

+ + {/* Routes nest inside one another. Nested route paths build upon + parent route paths, and nested route elements render inside + parent route elements. See the note about below. */} + + }> + } /> + } /> + } /> + } /> + + {/* Using path="*"" means "match anything", so this route + acts like a catch-all for URLs that we don't have explicit + routes for. */} + } /> + + +
+ ); +} + +function ModalRouter() { + return ( + + + } /> + } /> + } /> + + + ); +} + +function Layout() { + return ( +
+ {/* A "layout route" is a good place to put markup you want to + share across all the pages on your site, like navigation. */} + + +
+ + {/* An renders whatever child route is currently active, + so you can think about this as a placeholder for + the child routes we defined above. */} + +
+ ); +} + +function Home() { + return ( +
+

Home

+
+ ); +} + +function About() { + return ( +
+

About

+
+ ); +} + +function Dashboard() { + return ( +
+

Dashboard

+
+ ); +} + +function NoMatch() { + return ( +
+

Nothing to see here!

+

+ Go to the home page +

+
+ ); +} + +function FirstModal() { + return ( +
+
+

First Modal

+ Go to second modal + Go home +
+
+ ); +} + +function SecondModal() { + const navigate = useScopedNavigate(); + return ( +
+
+

Second Modal

+ Go to third modal + + Go home +
+
+ ); +} + +function ThirdModal() { + const navigate = useScopedNavigate(); + return ( +
+
+

Third Modal

+ + Go home +
+
+ ); +} diff --git a/examples/scoped-memory-router/src/index.css b/examples/scoped-memory-router/src/index.css new file mode 100644 index 0000000000..645f970b45 --- /dev/null +++ b/examples/scoped-memory-router/src/index.css @@ -0,0 +1,37 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", + monospace; +} + +.baseModal { + display: flex; + position: fixed; + height: 100vh; + width: 100vw; + align-items: center; + justify-content: center; + z-index: 1000; + top: 0; + left: 0; + overflow: auto; + background-color: rgba(0, 0, 0, 0.5); +} + +.modalContents { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 300px; + background-color: #fff; + border-radius: 4px; + padding: 20px; +} diff --git a/examples/scoped-memory-router/src/main.tsx b/examples/scoped-memory-router/src/main.tsx new file mode 100644 index 0000000000..6a13f4e35a --- /dev/null +++ b/examples/scoped-memory-router/src/main.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import { BrowserRouter } from "react-router-dom"; + +import "./index.css"; +import App from "./App"; + +ReactDOM.render( + + + + + , + document.getElementById("root") +); diff --git a/examples/scoped-memory-router/src/vite-env.d.ts b/examples/scoped-memory-router/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/scoped-memory-router/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/scoped-memory-router/tsconfig.json b/examples/scoped-memory-router/tsconfig.json new file mode 100644 index 0000000000..8bdaabfe5d --- /dev/null +++ b/examples/scoped-memory-router/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react", + "importsNotUsedAsValues": "error" + }, + "include": ["./src"] +} diff --git a/examples/scoped-memory-router/vite.config.ts b/examples/scoped-memory-router/vite.config.ts new file mode 100644 index 0000000000..fe56432876 --- /dev/null +++ b/examples/scoped-memory-router/vite.config.ts @@ -0,0 +1,39 @@ +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({ + server: { + port: process.env.PORT ? parseInt(process.env.PORT, 10) : undefined, + }, + 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/packages/react-router-dom/__tests__/exports-test.tsx b/packages/react-router-dom/__tests__/exports-test.tsx index bfffbf208f..cffcfdcb8b 100644 --- a/packages/react-router-dom/__tests__/exports-test.tsx +++ b/packages/react-router-dom/__tests__/exports-test.tsx @@ -3,6 +3,12 @@ import * as ReactRouterDOM from "react-router-dom"; describe("react-router-dom", () => { for (let key in ReactRouter) { + // react-router-dom wraps the createScopedMemoryRouterEnvironment function from react-router + // to add additional dom-related hooks / components. + if (key === "createScopedMemoryRouterEnvironment") { + continue; + } + it(`re-exports ${key} from react-router`, () => { expect(ReactRouterDOM[key]).toBe(ReactRouter[key]); }); diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index 6cd01dc86f..c18bb1ced8 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -8,22 +8,19 @@ import type { RelativeRoutingType, RouteObject, To, + Hooks, } from "react-router"; import { Router, createPath, - useHref, - useLocation, - useMatch, - useMatches, - useNavigate, - useNavigation, - useResolvedPath, UNSAFE_DataRouterContext as DataRouterContext, UNSAFE_DataRouterStateContext as DataRouterStateContext, UNSAFE_NavigationContext as NavigationContext, - UNSAFE_RouteContext as RouteContext, UNSAFE_enhanceManualRouteObjects as enhanceManualRouteObjects, + createScopedMemoryRouterEnvironment as baseCreateScopedMemoryRouterEnvironment, + UNSAFE_createReactRouterEnvironment, + UNSAFE_createReactRouterContexts, + UNSAFE_reactRouterContexts, } from "react-router"; import type { BrowserHistory, @@ -351,56 +348,62 @@ export interface LinkProps to: To; } -/** - * The public API for rendering a history-aware . - */ -export const Link = React.forwardRef( - function LinkWithRef( - { - onClick, - relative, - reloadDocument, - replace, - state, - target, - to, - preventScrollReset, - ...rest - }, - ref - ) { - let href = useHref(to, { relative }); - let internalOnClick = useLinkClickHandler(to, { - replace, - state, - target, - preventScrollReset, - relative, - }); - function handleClick( - event: React.MouseEvent +function createLink( + { useHref }: Hooks, + useLinkClickHandler: ReturnType +) { + /** + * The public API for rendering a history-aware . + */ + const Link = React.forwardRef( + function LinkWithRef( + { + onClick, + relative, + reloadDocument, + replace, + state, + target, + to, + preventScrollReset, + ...rest + }, + ref ) { - if (onClick) onClick(event); - if (!event.defaultPrevented) { - internalOnClick(event); + let href = useHref(to, { relative }); + let internalOnClick = useLinkClickHandler(to, { + replace, + state, + target, + preventScrollReset, + relative, + }); + function handleClick( + event: React.MouseEvent + ) { + if (onClick) onClick(event); + if (!event.defaultPrevented) { + internalOnClick(event); + } } + + return ( + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ); } + ); - return ( - // eslint-disable-next-line jsx-a11y/anchor-has-content - - ); + if (__DEV__) { + Link.displayName = "Link"; } -); - -if (__DEV__) { - Link.displayName = "Link"; + return Link; } export interface NavLinkProps @@ -424,101 +427,108 @@ export interface NavLinkProps }) => React.CSSProperties | undefined); } -/** - * A wrapper that knows if it's "active" or not. - */ -export const NavLink = React.forwardRef( - function NavLinkWithRef( - { - "aria-current": ariaCurrentProp = "page", - caseSensitive = false, - className: classNameProp = "", - end = false, - style: styleProp, - to, - children, - ...rest - }, - ref - ) { - let path = useResolvedPath(to, { relative: rest.relative }); - let location = useLocation(); - let routerState = React.useContext(DataRouterStateContext); - - let toPathname = path.pathname; - let locationPathname = location.pathname; - let nextLocationPathname = - routerState && routerState.navigation && routerState.navigation.location - ? routerState.navigation.location.pathname - : null; - - if (!caseSensitive) { - locationPathname = locationPathname.toLowerCase(); - nextLocationPathname = nextLocationPathname - ? nextLocationPathname.toLowerCase() - : null; - toPathname = toPathname.toLowerCase(); - } - - let isActive = - locationPathname === toPathname || - (!end && - locationPathname.startsWith(toPathname) && - locationPathname.charAt(toPathname.length) === "/"); +function createNavLink( + { DataRouterStateContext }: typeof UNSAFE_reactRouterContexts, + { useResolvedPath, useLocation }: Hooks, + Link: ReturnType +) { + /** + * A wrapper that knows if it's "active" or not. + */ + const NavLink = React.forwardRef( + function NavLinkWithRef( + { + "aria-current": ariaCurrentProp = "page", + caseSensitive = false, + className: classNameProp = "", + end = false, + style: styleProp, + to, + children, + ...rest + }, + ref + ) { + let path = useResolvedPath(to, { relative: rest.relative }); + let location = useLocation(); + let routerState = React.useContext(DataRouterStateContext); + + let toPathname = path.pathname; + let locationPathname = location.pathname; + let nextLocationPathname = + routerState && routerState.navigation && routerState.navigation.location + ? routerState.navigation.location.pathname + : null; + + if (!caseSensitive) { + locationPathname = locationPathname.toLowerCase(); + nextLocationPathname = nextLocationPathname + ? nextLocationPathname.toLowerCase() + : null; + toPathname = toPathname.toLowerCase(); + } - let isPending = - nextLocationPathname != null && - (nextLocationPathname === toPathname || + let isActive = + locationPathname === toPathname || (!end && - nextLocationPathname.startsWith(toPathname) && - nextLocationPathname.charAt(toPathname.length) === "/")); - - let ariaCurrent = isActive ? ariaCurrentProp : undefined; - - let className: string | undefined; - if (typeof classNameProp === "function") { - className = classNameProp({ isActive, isPending }); - } else { - // If the className prop is not a function, we use a default `active` - // class for s that are active. In v5 `active` was the default - // value for `activeClassName`, but we are removing that API and can still - // use the old default behavior for a cleaner upgrade path and keep the - // simple styling rules working as they currently do. - className = [ - classNameProp, - isActive ? "active" : null, - isPending ? "pending" : null, - ] - .filter(Boolean) - .join(" "); + locationPathname.startsWith(toPathname) && + locationPathname.charAt(toPathname.length) === "/"); + + let isPending = + nextLocationPathname != null && + (nextLocationPathname === toPathname || + (!end && + nextLocationPathname.startsWith(toPathname) && + nextLocationPathname.charAt(toPathname.length) === "/")); + + let ariaCurrent = isActive ? ariaCurrentProp : undefined; + + let className: string | undefined; + if (typeof classNameProp === "function") { + className = classNameProp({ isActive, isPending }); + } else { + // If the className prop is not a function, we use a default `active` + // class for s that are active. In v5 `active` was the default + // value for `activeClassName`, but we are removing that API and can still + // use the old default behavior for a cleaner upgrade path and keep the + // simple styling rules working as they currently do. + className = [ + classNameProp, + isActive ? "active" : null, + isPending ? "pending" : null, + ] + .filter(Boolean) + .join(" "); + } + + let style = + typeof styleProp === "function" + ? styleProp({ isActive, isPending }) + : styleProp; + + return ( + + {typeof children === "function" + ? children({ isActive, isPending }) + : children} + + ); } + ); - let style = - typeof styleProp === "function" - ? styleProp({ isActive, isPending }) - : styleProp; - - return ( - - {typeof children === "function" - ? children({ isActive, isPending }) - : children} - - ); + if (__DEV__) { + NavLink.displayName = "NavLink"; } -); -if (__DEV__) { - NavLink.displayName = "NavLink"; + return NavLink; } - export interface FormProps extends React.FormHTMLAttributes { /** * The HTTP verb to use when the form is submit. Supports "get", "post", @@ -557,20 +567,21 @@ export interface FormProps extends React.FormHTMLAttributes { onSubmit?: React.FormEventHandler; } -/** - * A `@remix-run/router`-aware `
`. It behaves like a normal form except - * that the interaction with the server is with `fetch` instead of new document - * requests, allowing components to add nicer UX to the page as the form is - * submitted and returns with data. - */ -export const Form = React.forwardRef( - (props, ref) => { +function createForm(FormImpl: ReturnType) { + /** + * A `@remix-run/router`-aware ``. It behaves like a normal form except + * that the interaction with the server is with `fetch` instead of new document + * requests, allowing components to add nicer UX to the page as the form is + * submitted and returns with data. + */ + const Form = React.forwardRef((props, ref) => { return ; - } -); + }); -if (__DEV__) { - Form.displayName = "Form"; + if (__DEV__) { + Form.displayName = "Form"; + } + return Form; } type HTMLSubmitEvent = React.BaseSyntheticEvent< @@ -586,50 +597,53 @@ interface FormImplProps extends FormProps { routeId?: string; } -const FormImpl = React.forwardRef( - ( - { - reloadDocument, - replace, - method = defaultMethod, - action, - onSubmit, - fetcherKey, - routeId, - relative, - ...props - }, - forwardedRef - ) => { - let submit = useSubmitImpl(fetcherKey, routeId); - let formMethod: FormMethod = - method.toLowerCase() === "get" ? "get" : "post"; - let formAction = useFormAction(action, { relative }); - let submitHandler: React.FormEventHandler = (event) => { - onSubmit && onSubmit(event); - if (event.defaultPrevented) return; - event.preventDefault(); - - let submitter = (event as unknown as HTMLSubmitEvent).nativeEvent - .submitter as HTMLFormSubmitter | null; - - submit(submitter || event.currentTarget, { method, replace, relative }); - }; +function createFormImpl( + useSubmitImpl: ReturnType, + useFormAction: ReturnType +) { + const FormImpl = React.forwardRef( + ( + { + reloadDocument, + replace, + method = defaultMethod, + action, + onSubmit, + fetcherKey, + routeId, + relative, + ...props + }, + forwardedRef + ) => { + let submit = useSubmitImpl(fetcherKey, routeId); + let formMethod: FormMethod = + method.toLowerCase() === "get" ? "get" : "post"; + let formAction = useFormAction(action, { relative }); + let submitHandler: React.FormEventHandler = (event) => { + onSubmit && onSubmit(event); + if (event.defaultPrevented) return; + event.preventDefault(); - return ( - - ); - } -); + let submitter = (event as unknown as HTMLSubmitEvent).nativeEvent + .submitter as HTMLFormSubmitter | null; -if (__DEV__) { - Form.displayName = "Form"; + submit(submitter || event.currentTarget, { method, replace, relative }); + }; + + return ( + + ); + } + ); + + return FormImpl; } interface ScrollRestorationProps { @@ -637,21 +651,24 @@ interface ScrollRestorationProps { storageKey?: string; } -/** - * This component will emulate the browser's scroll restoration on location - * changes. - */ -export function ScrollRestoration({ - getKey, - storageKey, -}: ScrollRestorationProps) { - useScrollRestoration({ getKey, storageKey }); - return null; -} +function createScrollRestoration( + useScrollRestoration: ReturnType +) { + /** + * This component will emulate the browser's scroll restoration on location + * changes. + */ + function ScrollRestoration({ getKey, storageKey }: ScrollRestorationProps) { + useScrollRestoration({ getKey, storageKey }); + return null; + } -if (__DEV__) { - ScrollRestoration.displayName = "ScrollRestoration"; + if (__DEV__) { + ScrollRestoration.displayName = "ScrollRestoration"; + } + return ScrollRestoration; } + //#endregion //////////////////////////////////////////////////////////////////////////////// @@ -687,103 +704,111 @@ function useDataRouterState(hookName: DataRouterStateHook) { return state; } -/** - * Handles the click behavior for router `` components. This is useful if - * you need to create custom `` components with the same click behavior we - * use in our exported ``. - */ -export function useLinkClickHandler( - to: To, - { - target, - replace: replaceProp, - state, - preventScrollReset, - relative, - }: { - target?: React.HTMLAttributeAnchorTarget; - replace?: boolean; - state?: any; - preventScrollReset?: boolean; - relative?: RelativeRoutingType; - } = {} -): (event: React.MouseEvent) => void { - let navigate = useNavigate(); - let location = useLocation(); - let path = useResolvedPath(to, { relative }); - - return React.useCallback( - (event: React.MouseEvent) => { - if (shouldProcessLinkClick(event, target)) { - event.preventDefault(); - - // If the URL hasn't changed, a regular will do a replace instead of - // a push, so do the same here unless the replace prop is explicitly set - let replace = - replaceProp !== undefined - ? replaceProp - : createPath(location) === createPath(path); - - navigate(to, { replace, state, preventScrollReset, relative }); - } - }, - [ - location, - navigate, - path, - replaceProp, - state, +function createLinkClickHandlerHook({ + useNavigate, + useLocation, + useResolvedPath, +}: Hooks) { + /** + * Handles the click behavior for router `` components. This is useful if + * you need to create custom `` components with the same click behavior we + * use in our exported ``. + */ + return function useLinkClickHandler( + to: To, + { target, - to, + replace: replaceProp, + state, preventScrollReset, relative, - ] - ); + }: { + target?: React.HTMLAttributeAnchorTarget; + replace?: boolean; + state?: any; + preventScrollReset?: boolean; + relative?: RelativeRoutingType; + } = {} + ): (event: React.MouseEvent) => void { + let navigate = useNavigate(); + let location = useLocation(); + let path = useResolvedPath(to, { relative }); + + return React.useCallback( + (event: React.MouseEvent) => { + if (shouldProcessLinkClick(event, target)) { + event.preventDefault(); + + // If the URL hasn't changed, a regular will do a replace instead of + // a push, so do the same here unless the replace prop is explicitly set + let replace = + replaceProp !== undefined + ? replaceProp + : createPath(location) === createPath(path); + + navigate(to, { replace, state, preventScrollReset, relative }); + } + }, + [ + location, + navigate, + path, + replaceProp, + state, + target, + to, + preventScrollReset, + relative, + ] + ); + }; } -/** - * A convenient wrapper for reading and writing search parameters via the - * URLSearchParams interface. - */ -export function useSearchParams( - defaultInit?: URLSearchParamsInit -): [URLSearchParams, SetURLSearchParams] { - warning( - typeof URLSearchParams !== "undefined", - `You cannot use the \`useSearchParams\` hook in a browser that does not ` + - `support the URLSearchParams API. If you need to support Internet ` + - `Explorer 11, we recommend you load a polyfill such as ` + - `https://github.com/ungap/url-search-params\n\n` + - `If you're unsure how to load polyfills, we recommend you check out ` + - `https://polyfill.io/v3/ which provides some recommendations about how ` + - `to load polyfills only for users that need them, instead of for every ` + - `user.` - ); +function createSearchParamsHook({ useLocation, useNavigate }: Hooks) { + /** + * A convenient wrapper for reading and writing search parameters via the + * URLSearchParams interface. + */ + return function useSearchParams( + defaultInit?: URLSearchParamsInit + ): [URLSearchParams, SetURLSearchParams] { + warning( + typeof URLSearchParams !== "undefined", + `You cannot use the \`useSearchParams\` hook in a browser that does not ` + + `support the URLSearchParams API. If you need to support Internet ` + + `Explorer 11, we recommend you load a polyfill such as ` + + `https://github.com/ungap/url-search-params\n\n` + + `If you're unsure how to load polyfills, we recommend you check out ` + + `https://polyfill.io/v3/ which provides some recommendations about how ` + + `to load polyfills only for users that need them, instead of for every ` + + `user.` + ); - let defaultSearchParamsRef = React.useRef(createSearchParams(defaultInit)); + let defaultSearchParamsRef = React.useRef(createSearchParams(defaultInit)); - let location = useLocation(); - let searchParams = React.useMemo( - () => - getSearchParamsForLocation( - location.search, - defaultSearchParamsRef.current - ), - [location.search] - ); + let location = useLocation(); + let searchParams = React.useMemo( + () => + getSearchParamsForLocation( + location.search, + defaultSearchParamsRef.current + ), + [location.search] + ); - let navigate = useNavigate(); - let setSearchParams = React.useCallback( - (nextInit, navigateOptions) => { - const newSearchParams = createSearchParams( - typeof nextInit === "function" ? nextInit(searchParams) : nextInit - ); - navigate("?" + newSearchParams, navigateOptions); - }, - [navigate, searchParams] - ); + let navigate = useNavigate(); + let setSearchParams = React.useCallback( + (nextInit, navigateOptions) => { + const newSearchParams = createSearchParams( + typeof nextInit === "function" ? nextInit(searchParams) : nextInit + ); + navigate("?" + newSearchParams, navigateOptions); + }, + [navigate, searchParams] + ); - return [searchParams, setSearchParams]; + return [searchParams, setSearchParams]; + }; } type SetURLSearchParams = ( @@ -825,129 +850,148 @@ export interface SubmitFunction { ): void; } -/** - * Returns a function that may be used to programmatically submit a form (or - * some arbitrary data) to the server. - */ -export function useSubmit(): SubmitFunction { - return useSubmitImpl(); +function createSubmitHook( + useSubmitImpl: ReturnType +) { + /** + * Returns a function that may be used to programmatically submit a form (or + * some arbitrary data) to the server. + */ + return function useSubmit(): SubmitFunction { + return useSubmitImpl(); + }; } -function useSubmitImpl(fetcherKey?: string, routeId?: string): SubmitFunction { - let { router } = useDataRouterContext(DataRouterHook.UseSubmitImpl); - let defaultAction = useFormAction(); - - return React.useCallback( - (target, options = {}) => { - if (typeof document === "undefined") { - throw new Error( - "You are calling submit during the server render. " + - "Try calling submit within a `useEffect` or callback instead." +function createSubmitImplHook( + { DataRouterContext }: typeof UNSAFE_reactRouterContexts, + useFormAction: ReturnType +) { + return function useSubmitImpl( + fetcherKey?: string, + routeId?: string + ): SubmitFunction { + let { router } = useDataRouterContext(DataRouterHook.UseSubmitImpl); + let defaultAction = useFormAction(); + + return React.useCallback( + (target, options = {}) => { + if (typeof document === "undefined") { + throw new Error( + "You are calling submit during the server render. " + + "Try calling submit within a `useEffect` or callback instead." + ); + } + + let { method, encType, formData, url } = getFormSubmissionInfo( + target, + defaultAction, + options ); - } - let { method, encType, formData, url } = getFormSubmissionInfo( - target, - defaultAction, - options - ); - - let href = url.pathname + url.search; - let opts = { - replace: options.replace, - formData, - formMethod: method as FormMethod, - formEncType: encType as FormEncType, - }; - if (fetcherKey) { - invariant(routeId != null, "No routeId available for useFetcher()"); - router.fetch(fetcherKey, routeId, href, opts); - } else { - router.navigate(href, opts); - } - }, - [defaultAction, router, fetcherKey, routeId] - ); + let href = url.pathname + url.search; + let opts = { + replace: options.replace, + formData, + formMethod: method as FormMethod, + formEncType: encType as FormEncType, + }; + if (fetcherKey) { + invariant(routeId != null, "No routeId available for useFetcher()"); + router.fetch(fetcherKey, routeId, href, opts); + } else { + router.navigate(href, opts); + } + }, + [defaultAction, router, fetcherKey, routeId] + ); + }; } -export function useFormAction( - action?: string, - { relative }: { relative?: RelativeRoutingType } = {} -): string { - let { basename } = React.useContext(NavigationContext); - let routeContext = React.useContext(RouteContext); - invariant(routeContext, "useFormAction must be used inside a RouteContext"); - - let [match] = routeContext.matches.slice(-1); - let resolvedAction = action ?? "."; - // Shallow clone path so we can modify it below, otherwise we modify the - // object referenced by useMemo inside useResolvedPath - let path = { ...useResolvedPath(resolvedAction, { relative }) }; - - // Previously we set the default action to ".". The problem with this is that - // `useResolvedPath(".")` excludes search params and the hash of the resolved - // URL. This is the intended behavior of when "." is specifically provided as - // the form action, but inconsistent w/ browsers when the action is omitted. - // https://github.com/remix-run/remix/issues/927 - let location = useLocation(); - if (action == null) { - // Safe to write to these directly here since if action was undefined, we - // would have called useResolvedPath(".") which will never include a search - // or hash - path.search = location.search; - path.hash = location.hash; - - // When grabbing search params from the URL, remove the automatically - // inserted ?index param so we match the useResolvedPath search behavior - // which would not include ?index - if (match.route.index) { - let params = new URLSearchParams(path.search); - params.delete("index"); - path.search = params.toString() ? `?${params.toString()}` : ""; +function createFormActionHook( + { RouteContext }: typeof UNSAFE_reactRouterContexts, + { useResolvedPath, useLocation }: Hooks +) { + return function useFormAction( + action?: string, + { relative }: { relative?: RelativeRoutingType } = {} + ): string { + let { basename } = React.useContext(NavigationContext); + let routeContext = React.useContext(RouteContext); + invariant(routeContext, "useFormAction must be used inside a RouteContext"); + + let [match] = routeContext.matches.slice(-1); + let resolvedAction = action ?? "."; + // Shallow clone path so we can modify it below, otherwise we modify the + // object referenced by useMemo inside useResolvedPath + let path = { ...useResolvedPath(resolvedAction, { relative }) }; + + // Previously we set the default action to ".". The problem with this is that + // `useResolvedPath(".")` excludes search params and the hash of the resolved + // URL. This is the intended behavior of when "." is specifically provided as + // the form action, but inconsistent w/ browsers when the action is omitted. + // https://github.com/remix-run/remix/issues/927 + let location = useLocation(); + if (action == null) { + // Safe to write to these directly here since if action was undefined, we + // would have called useResolvedPath(".") which will never include a search + // or hash + path.search = location.search; + path.hash = location.hash; + + // When grabbing search params from the URL, remove the automatically + // inserted ?index param so we match the useResolvedPath search behavior + // which would not include ?index + if (match.route.index) { + let params = new URLSearchParams(path.search); + params.delete("index"); + path.search = params.toString() ? `?${params.toString()}` : ""; + } } - } - if ((!action || action === ".") && match.route.index) { - path.search = path.search - ? path.search.replace(/^\?/, "?index&") - : "?index"; - } + if ((!action || action === ".") && match.route.index) { + path.search = path.search + ? path.search.replace(/^\?/, "?index&") + : "?index"; + } - // If we're operating within a basename, prepend it to the pathname prior - // to creating the form action. If this is a root navigation, then just use - // the raw basename which allows the basename to have full control over the - // presence of a trailing slash on root actions - if (basename !== "/") { - path.pathname = - path.pathname === "/" ? basename : joinPaths([basename, path.pathname]); - } + // If we're operating within a basename, prepend it to the pathname prior + // to creating the form action. If this is a root navigation, then just use + // the raw basename which allows the basename to have full control over the + // presence of a trailing slash on root actions + if (basename !== "/") { + path.pathname = + path.pathname === "/" ? basename : joinPaths([basename, path.pathname]); + } - return createPath(path); + return createPath(path); + }; } -function createFetcherForm(fetcherKey: string, routeId: string) { - let FetcherForm = React.forwardRef( - (props, ref) => { - return ( - - ); +function createCreateFetcherForm(FormImpl: ReturnType) { + return function createFetcherForm(fetcherKey: string, routeId: string) { + let FetcherForm = React.forwardRef( + (props, ref) => { + return ( + + ); + } + ); + if (__DEV__) { + FetcherForm.displayName = "fetcher.Form"; } - ); - if (__DEV__) { - FetcherForm.displayName = "fetcher.Form"; - } - return FetcherForm; + return FetcherForm; + }; } let fetcherId = 0; export type FetcherWithComponents = Fetcher & { - Form: ReturnType; + Form: ReturnType>; submit: ( target: SubmitTarget, // Fetchers cannot replace because they are not navigation events @@ -956,169 +1000,185 @@ export type FetcherWithComponents = Fetcher & { load: (href: string) => void; }; -/** - * Interacts with route loaders and actions without causing a navigation. Great - * for any interaction that stays on the same page. - */ -export function useFetcher(): FetcherWithComponents { - let { router } = useDataRouterContext(DataRouterHook.UseFetcher); +function createFetcherHook( + { RouteContext }: typeof UNSAFE_reactRouterContexts, + useSubmitImpl: ReturnType, + createFetcherForm: ReturnType +) { + /** + * Interacts with route loaders and actions without causing a navigation. Great + * for any interaction that stays on the same page. + */ + return function useFetcher(): FetcherWithComponents { + let { router } = useDataRouterContext(DataRouterHook.UseFetcher); - let route = React.useContext(RouteContext); - invariant(route, `useFetcher must be used inside a RouteContext`); + let route = React.useContext(RouteContext); + invariant(route, `useFetcher must be used inside a RouteContext`); - let routeId = route.matches[route.matches.length - 1]?.route.id; - invariant( - routeId != null, - `useFetcher can only be used on routes that contain a unique "id"` - ); + let routeId = route.matches[route.matches.length - 1]?.route.id; + invariant( + routeId != null, + `useFetcher can only be used on routes that contain a unique "id"` + ); - let [fetcherKey] = React.useState(() => String(++fetcherId)); - let [Form] = React.useState(() => { - invariant(routeId, `No routeId available for fetcher.Form()`); - return createFetcherForm(fetcherKey, routeId); - }); - let [load] = React.useState(() => (href: string) => { - invariant(router, "No router available for fetcher.load()"); - invariant(routeId, "No routeId available for fetcher.load()"); - router.fetch(fetcherKey, routeId, href); - }); - let submit = useSubmitImpl(fetcherKey, routeId); + let [fetcherKey] = React.useState(() => String(++fetcherId)); + let [Form] = React.useState(() => { + invariant(routeId, `No routeId available for fetcher.Form()`); + return createFetcherForm(fetcherKey, routeId); + }); + let [load] = React.useState(() => (href: string) => { + invariant(router, "No router available for fetcher.load()"); + invariant(routeId, "No routeId available for fetcher.load()"); + router.fetch(fetcherKey, routeId, href); + }); + let submit = useSubmitImpl(fetcherKey, routeId); - let fetcher = router.getFetcher(fetcherKey); + let fetcher = router.getFetcher(fetcherKey); - let fetcherWithComponents = React.useMemo( - () => ({ - Form, - submit, - load, - ...fetcher, - }), - [fetcher, Form, submit, load] - ); + let fetcherWithComponents = React.useMemo( + () => ({ + Form, + submit, + load, + ...fetcher, + }), + [fetcher, Form, submit, load] + ); - React.useEffect(() => { - // Is this busted when the React team gets real weird and calls effects - // twice on mount? We really just need to garbage collect here when this - // fetcher is no longer around. - return () => { - if (!router) { - console.warn(`No fetcher available to clean up from useFetcher()`); - return; - } - router.deleteFetcher(fetcherKey); - }; - }, [router, fetcherKey]); + React.useEffect(() => { + // Is this busted when the React team gets real weird and calls effects + // twice on mount? We really just need to garbage collect here when this + // fetcher is no longer around. + return () => { + if (!router) { + console.warn(`No fetcher available to clean up from useFetcher()`); + return; + } + router.deleteFetcher(fetcherKey); + }; + }, [router, fetcherKey]); - return fetcherWithComponents; + return fetcherWithComponents; + }; } -/** - * Provides all fetchers currently on the page. Useful for layouts and parent - * routes that need to provide pending/optimistic UI regarding the fetch. - */ -export function useFetchers(): Fetcher[] { - let state = useDataRouterState(DataRouterStateHook.UseFetchers); - return [...state.fetchers.values()]; +function createFetchersHook({ + DataRouterStateContext, +}: typeof UNSAFE_reactRouterContexts) { + /** + * Provides all fetchers currently on the page. Useful for layouts and parent + * routes that need to provide pending/optimistic UI regarding the fetch. + */ + return function useFetchers(): Fetcher[] { + let state = useDataRouterState(DataRouterStateHook.UseFetchers); + return [...state.fetchers.values()]; + }; } -const SCROLL_RESTORATION_STORAGE_KEY = "react-router-scroll-positions"; +export const SCROLL_RESTORATION_STORAGE_KEY = "react-router-scroll-positions"; let savedScrollPositions: Record = {}; -/** - * When rendered inside a RouterProvider, will restore scroll positions on navigations - */ -function useScrollRestoration({ - getKey, - storageKey, -}: { - getKey?: GetScrollRestorationKeyFunction; - storageKey?: string; -} = {}) { - let { router } = useDataRouterContext(DataRouterHook.UseScrollRestoration); - let { restoreScrollPosition, preventScrollReset } = useDataRouterState( - DataRouterStateHook.UseScrollRestoration - ); - let location = useLocation(); - let matches = useMatches(); - let navigation = useNavigation(); +function createScrollRestorationHook({ + useLocation, + useMatches, + useNavigation, +}: Hooks) { + /** + * When rendered inside a RouterProvider, will restore scroll positions on navigations + */ + return function useScrollRestoration({ + getKey, + storageKey, + }: { + getKey?: GetScrollRestorationKeyFunction; + storageKey?: string; + } = {}) { + let { router } = useDataRouterContext(DataRouterHook.UseScrollRestoration); + let { restoreScrollPosition, preventScrollReset } = useDataRouterState( + DataRouterStateHook.UseScrollRestoration + ); + let location = useLocation(); + let matches = useMatches(); + let navigation = useNavigation(); + + // Trigger manual scroll restoration while we're active + React.useEffect(() => { + window.history.scrollRestoration = "manual"; + return () => { + window.history.scrollRestoration = "auto"; + }; + }, []); + + // Save positions on unload + useBeforeUnload( + React.useCallback(() => { + if (navigation.state === "idle") { + let key = (getKey ? getKey(location, matches) : null) || location.key; + savedScrollPositions[key] = window.scrollY; + } + sessionStorage.setItem( + storageKey || SCROLL_RESTORATION_STORAGE_KEY, + JSON.stringify(savedScrollPositions) + ); + window.history.scrollRestoration = "auto"; + }, [storageKey, getKey, navigation.state, location, matches]) + ); - // Trigger manual scroll restoration while we're active - React.useEffect(() => { - window.history.scrollRestoration = "manual"; - return () => { - window.history.scrollRestoration = "auto"; - }; - }, []); - - // Save positions on unload - useBeforeUnload( - React.useCallback(() => { - if (navigation.state === "idle") { - let key = (getKey ? getKey(location, matches) : null) || location.key; - savedScrollPositions[key] = window.scrollY; + // Read in any saved scroll locations + React.useLayoutEffect(() => { + try { + let sessionPositions = sessionStorage.getItem( + storageKey || SCROLL_RESTORATION_STORAGE_KEY + ); + if (sessionPositions) { + savedScrollPositions = JSON.parse(sessionPositions); + } + } catch (e) { + // no-op, use default empty object } - sessionStorage.setItem( - storageKey || SCROLL_RESTORATION_STORAGE_KEY, - JSON.stringify(savedScrollPositions) + }, [storageKey]); + + // Enable scroll restoration in the router + React.useLayoutEffect(() => { + let disableScrollRestoration = router?.enableScrollRestoration( + savedScrollPositions, + () => window.scrollY, + getKey ); - window.history.scrollRestoration = "auto"; - }, [storageKey, getKey, navigation.state, location, matches]) - ); + return () => disableScrollRestoration && disableScrollRestoration(); + }, [router, getKey]); - // Read in any saved scroll locations - React.useLayoutEffect(() => { - try { - let sessionPositions = sessionStorage.getItem( - storageKey || SCROLL_RESTORATION_STORAGE_KEY - ); - if (sessionPositions) { - savedScrollPositions = JSON.parse(sessionPositions); + // Restore scrolling when state.restoreScrollPosition changes + React.useLayoutEffect(() => { + // Explicit false means don't do anything (used for submissions) + if (restoreScrollPosition === false) { + return; } - } catch (e) { - // no-op, use default empty object - } - }, [storageKey]); - - // Enable scroll restoration in the router - React.useLayoutEffect(() => { - let disableScrollRestoration = router?.enableScrollRestoration( - savedScrollPositions, - () => window.scrollY, - getKey - ); - return () => disableScrollRestoration && disableScrollRestoration(); - }, [router, getKey]); - - // Restore scrolling when state.restoreScrollPosition changes - React.useLayoutEffect(() => { - // Explicit false means don't do anything (used for submissions) - if (restoreScrollPosition === false) { - return; - } - - // been here before, scroll to it - if (typeof restoreScrollPosition === "number") { - window.scrollTo(0, restoreScrollPosition); - return; - } - // try to scroll to the hash - if (location.hash) { - let el = document.getElementById(location.hash.slice(1)); - if (el) { - el.scrollIntoView(); + // been here before, scroll to it + if (typeof restoreScrollPosition === "number") { + window.scrollTo(0, restoreScrollPosition); return; } - } - // Opt out of scroll reset if this link requested it - if (preventScrollReset === true) { - return; - } + // try to scroll to the hash + if (location.hash) { + let el = document.getElementById(location.hash.slice(1)); + if (el) { + el.scrollIntoView(); + return; + } + } - // otherwise go to the top on new locations - window.scrollTo(0, 0); - }, [location, restoreScrollPosition, preventScrollReset]); + // Opt out of scroll reset if this link requested it + if (preventScrollReset === true) { + return; + } + + // otherwise go to the top on new locations + window.scrollTo(0, 0); + }, [location, restoreScrollPosition, preventScrollReset]); + }; } function useBeforeUnload(callback: () => any): void { @@ -1153,3 +1213,113 @@ function warning(cond: boolean, message: string): void { } } //#endregion + +function createReactRouterDomEnvironment( + contexts = UNSAFE_reactRouterContexts +) { + const { hooks } = UNSAFE_createReactRouterEnvironment(contexts); + const useLinkClickHandler = createLinkClickHandlerHook(hooks); + const useFormAction = createFormActionHook(contexts, hooks); + const useSubmitImpl = createSubmitImplHook(contexts, useFormAction); + const useSubmit = createSubmitHook(useSubmitImpl); + const useScrollRestoration = createScrollRestorationHook(hooks); + const useSearchParams = createSearchParamsHook(hooks); + const useFetchers = createFetchersHook(contexts); + + const Link = createLink(hooks, useLinkClickHandler); + const NavLink = createNavLink(contexts, hooks, Link); + + const FormImpl = createFormImpl(useSubmitImpl, useFormAction); + const createFetcherForm = createCreateFetcherForm(FormImpl); + const useFetcher = createFetcherHook( + contexts, + useSubmitImpl, + createFetcherForm + ); + const Form = createForm(FormImpl); + const ScrollRestoration = createScrollRestoration(useScrollRestoration); + + return { + hooks: { + useLinkClickHandler, + useFormAction, + useSubmit, + useSearchParams, + + /** + * Interacts with route loaders and actions without causing a navigation. Great + * for any interaction that stays on the same page. + */ + useFetcher, + + /** + * Provides all fetchers currently on the page. Useful for layouts and parent + * routes that need to provide pending/optimistic UI regarding the fetch. + */ + useFetchers, + }, + components: { + /** + * The public API for rendering a history-aware . + */ + Link, + + /** + * A wrapper that knows if it's "active" or not. + */ + NavLink, + + /** + * A `@remix-run/router`-aware ``. It behaves like a normal form except + * that the interaction with the server is with `fetch` instead of new document + * requests, allowing components to add nicer UX to the page as the form is + * submitted and returns with data. + */ + Form, + + /** + * This component will emulate the browser's scroll restoration on location + * changes. + */ + ScrollRestoration, + }, + }; +} + +const { + hooks: { + useLinkClickHandler, + useFetcher, + useFetchers, + useFormAction, + useSearchParams, + useSubmit, + }, + components: { Link, NavLink, Form, ScrollRestoration }, +} = createReactRouterDomEnvironment(); + +export { + useLinkClickHandler, + useFetcher, + useFetchers, + useFormAction, + useSearchParams, + useSubmit, + Link, + NavLink, + Form, + ScrollRestoration, +}; + +export function createScopedMemoryRouterEnvironment() { + const contexts = UNSAFE_createReactRouterContexts(); + const reactRouterEnvironment = + baseCreateScopedMemoryRouterEnvironment(contexts); + const reactRouterDomEnvironment = createReactRouterDomEnvironment(contexts); + + return { + ...reactRouterEnvironment, + ...reactRouterDomEnvironment.hooks, + ...reactRouterDomEnvironment.components, + }; +} diff --git a/packages/react-router-native/__tests__/exports-test.tsx b/packages/react-router-native/__tests__/exports-test.tsx index 62915c62ef..a01cdfbaf0 100644 --- a/packages/react-router-native/__tests__/exports-test.tsx +++ b/packages/react-router-native/__tests__/exports-test.tsx @@ -3,6 +3,12 @@ import * as ReactRouterNative from "react-router-native"; describe("react-router-native", () => { for (let key in ReactRouter) { + // react-router-native wraps the createScopedMemoryRouterEnvironment function from react-router + // to add additional native-related hooks / components. + if (key === "createScopedMemoryRouterEnvironment") { + continue; + } + it(`re-exports ${key} from react-router`, () => { expect(ReactRouterNative[key]).toBe(ReactRouter[key]); }); diff --git a/packages/react-router-native/index.tsx b/packages/react-router-native/index.tsx index b889f17913..80372324a8 100644 --- a/packages/react-router-native/index.tsx +++ b/packages/react-router-native/index.tsx @@ -9,8 +9,15 @@ import type { MemoryRouterProps, NavigateOptions, RelativeRoutingType, + Hooks, +} from "react-router"; +import { + MemoryRouter, + UNSAFE_reactRouterContexts, + UNSAFE_createReactRouterContexts, + UNSAFE_createReactRouterEnvironment, + createScopedMemoryRouterEnvironment as baseCreateScopedMemoryRouterEnvironment, } from "react-router"; -import { MemoryRouter, useLocation, useNavigate } from "react-router"; import URLSearchParams from "@ungap/url-search-params"; @@ -154,26 +161,30 @@ export interface LinkProps extends TouchableHighlightProps { to: To; } -/** - * A that navigates to a different URL when touched. - */ -export function Link({ - onPress, - relative, - replace = false, - state, - to, - ...rest -}: LinkProps) { - let internalOnPress = useLinkPressHandler(to, { replace, state, relative }); - function handlePress(event: GestureResponderEvent) { - if (onPress) onPress(event); - if (!event.defaultPrevented) { - internalOnPress(event); +function createLink( + useLinkPressHandler: ReturnType +) { + /** + * A that navigates to a different URL when touched. + */ + return function Link({ + onPress, + relative, + replace = false, + state, + to, + ...rest + }: LinkProps) { + let internalOnPress = useLinkPressHandler(to, { replace, state, relative }); + function handlePress(event: GestureResponderEvent) { + if (onPress) onPress(event); + if (!event.defaultPrevented) { + internalOnPress(event); + } } - } - return ; + return ; + }; } //////////////////////////////////////////////////////////////////////////////// @@ -183,137 +194,143 @@ export function Link({ const HardwareBackPressEventType = "hardwareBackPress"; const URLEventType = "url"; -/** - * Handles the press behavior for router `` components. This is useful if - * you need to create custom `` components with the same press behavior we - * use in our exported ``. - */ -export function useLinkPressHandler( - to: To, - { - replace, - state, - relative, - }: { - replace?: boolean; - state?: any; - relative?: RelativeRoutingType; - } = {} -): (event: GestureResponderEvent) => void { - let navigate = useNavigate(); - return function handlePress() { - navigate(to, { replace, state, relative }); +function createLinkPressHandlerHook({ useNavigate }: Hooks) { + /** + * Handles the press behavior for router `` components. This is useful if + * you need to create custom `` components with the same press behavior we + * use in our exported ``. + */ + return function useLinkPressHandler( + to: To, + { + replace, + state, + relative, + }: { + replace?: boolean; + state?: any; + relative?: RelativeRoutingType; + } = {} + ): (event: GestureResponderEvent) => void { + let navigate = useNavigate(); + return function handlePress() { + navigate(to, { replace, state, relative }); + }; }; } -/** - * Enables support for the hardware back button on Android. - */ -export function useHardwareBackButton() { - React.useEffect(() => { - function handleHardwardBackPress() { - return undefined; - // TODO: The implementation will be something like this - // if (history.index === 0) { - // return false; // home screen - // } else { - // history.back(); - // return true; - // } - } - - BackHandler.addEventListener( - HardwareBackPressEventType, - handleHardwardBackPress - ); +function createHardwareBackButtonHook() { + /** + * Enables support for the hardware back button on Android. + */ + return function useHardwareBackButton() { + React.useEffect(() => { + function handleHardwardBackPress() { + return undefined; + // TODO: The implementation will be something like this + // if (history.index === 0) { + // return false; // home screen + // } else { + // history.back(); + // return true; + // } + } - return () => { - BackHandler.removeEventListener( + BackHandler.addEventListener( HardwareBackPressEventType, handleHardwardBackPress ); - }; - }, []); -} - -export { useHardwareBackButton as useAndroidBackButton }; - -/** - * Enables deep linking, both on the initial app launch and for - * subsequent incoming links. - */ -export function useDeepLinking() { - let navigate = useNavigate(); - // Get the initial URL - React.useEffect(() => { - let current = true; + return () => { + BackHandler.removeEventListener( + HardwareBackPressEventType, + handleHardwardBackPress + ); + }; + }, []); + }; +} - Linking.getInitialURL().then((url) => { - if (current) { - if (url) navigate(trimScheme(url)); +function createDeepLinkingHook({ useNavigate }: Hooks) { + /** + * Enables deep linking, both on the initial app launch and for + * subsequent incoming links. + */ + return function useDeepLinking() { + let navigate = useNavigate(); + + // Get the initial URL + React.useEffect(() => { + let current = true; + + Linking.getInitialURL().then((url) => { + if (current) { + if (url) navigate(trimScheme(url)); + } + }); + + return () => { + current = false; + }; + }, [navigate]); + + // Listen for URL changes + React.useEffect(() => { + function handleURLChange(event: { url: string }) { + navigate(trimScheme(event.url)); } - }); - - return () => { - current = false; - }; - }, [navigate]); - // Listen for URL changes - React.useEffect(() => { - function handleURLChange(event: { url: string }) { - navigate(trimScheme(event.url)); - } + Linking.addEventListener(URLEventType, handleURLChange); - Linking.addEventListener(URLEventType, handleURLChange); - - return () => { - Linking.removeEventListener(URLEventType, handleURLChange); - }; - }, [navigate]); + return () => { + Linking.removeEventListener(URLEventType, handleURLChange); + }; + }, [navigate]); + }; } function trimScheme(url: string) { return url.replace(/^.*?:\/\//, ""); } -/** - * A convenient wrapper for accessing individual query parameters via the - * URLSearchParams interface. - */ -export function useSearchParams( - defaultInit?: URLSearchParamsInit -): [URLSearchParams, SetURLSearchParams] { - let defaultSearchParamsRef = React.useRef(createSearchParams(defaultInit)); - - let location = useLocation(); - let searchParams = React.useMemo(() => { - let searchParams = createSearchParams(location.search); - - for (let key of defaultSearchParamsRef.current.keys()) { - if (!searchParams.has(key)) { - defaultSearchParamsRef.current.getAll(key).forEach((value) => { - searchParams.append(key, value); - }); +function createSearchParamsHook({ useNavigate, useLocation }: Hooks) { + /** + * A convenient wrapper for accessing individual query parameters via the + * URLSearchParams interface. + */ + return function useSearchParams( + defaultInit?: URLSearchParamsInit + ): [URLSearchParams, SetURLSearchParams] { + let defaultSearchParamsRef = React.useRef(createSearchParams(defaultInit)); + + let location = useLocation(); + let searchParams = React.useMemo(() => { + let searchParams = createSearchParams(location.search); + + for (let key of defaultSearchParamsRef.current.keys()) { + if (!searchParams.has(key)) { + defaultSearchParamsRef.current.getAll(key).forEach((value) => { + searchParams.append(key, value); + }); + } } - } - return searchParams; - }, [location.search]); - - let navigate = useNavigate(); - let setSearchParams = React.useCallback( - (nextInit, navigateOpts) => { - const newSearchParams = createSearchParams( - typeof nextInit === "function" ? nextInit(searchParams) : nextInit - ); - navigate("?" + newSearchParams, navigateOpts); - }, - [navigate, searchParams] - ); + return searchParams; + }, [location.search]); + + let navigate = useNavigate(); + let setSearchParams = React.useCallback( + (nextInit, navigateOpts) => { + const newSearchParams = createSearchParams( + typeof nextInit === "function" ? nextInit(searchParams) : nextInit + ); + navigate("?" + newSearchParams, navigateOpts); + }, + [navigate, searchParams] + ); - return [searchParams, setSearchParams]; + return [searchParams, setSearchParams]; + }; } type SetURLSearchParams = ( @@ -368,3 +385,81 @@ export function createSearchParams( }, [] as ParamKeyValuePair[]) ); } + +function createReactRouterNativeEnvironment( + contexts = UNSAFE_reactRouterContexts +) { + const { hooks } = UNSAFE_createReactRouterEnvironment(contexts); + const useLinkPressHandler = createLinkPressHandlerHook(hooks); + const useAndroidBackButton = createHardwareBackButtonHook(); + const useDeepLinking = createDeepLinkingHook(hooks); + const useSearchParams = createSearchParamsHook(hooks); + + const Link = createLink(useLinkPressHandler); + + return { + hooks: { + /** + * Handles the press behavior for router `` components. This is useful if + * you need to create custom `` components with the same press behavior we + * use in our exported ``. + */ + useLinkPressHandler, + + /** + * Enables support for the hardware back button on Android. + */ + useAndroidBackButton, + + /** + * Enables deep linking, both on the initial app launch and for + * subsequent incoming links. + */ + useDeepLinking, + + /** + * A convenient wrapper for accessing individual query parameters via the + * URLSearchParams interface. + */ + useSearchParams, + }, + components: { + /** + * A that navigates to a different URL when touched. + */ + Link, + }, + }; +} + +const { + hooks: { + useLinkPressHandler, + useAndroidBackButton, + useDeepLinking, + useSearchParams, + }, + components: { Link }, +} = createReactRouterNativeEnvironment(); + +export { + useLinkPressHandler, + useAndroidBackButton, + useDeepLinking, + useSearchParams, + Link, +}; + +export function createScopedMemoryRouterEnvironment() { + const contexts = UNSAFE_createReactRouterContexts(); + const reactRouterEnvironment = + baseCreateScopedMemoryRouterEnvironment(contexts); + const reactRouterDomEnvironment = + createReactRouterNativeEnvironment(contexts); + + return { + ...reactRouterEnvironment, + ...reactRouterDomEnvironment.hooks, + ...reactRouterDomEnvironment.components, + }; +} diff --git a/packages/react-router/__tests__/Routes-test.tsx b/packages/react-router/__tests__/Routes-test.tsx index e45df9e520..cda795853d 100644 --- a/packages/react-router/__tests__/Routes-test.tsx +++ b/packages/react-router/__tests__/Routes-test.tsx @@ -17,6 +17,7 @@ describe("", () => { it("renders null and issues a warning when no routes match the URL", () => { let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { renderer = TestRenderer.create( diff --git a/packages/react-router/__tests__/navigate-test.tsx b/packages/react-router/__tests__/navigate-test.tsx index d39b871593..eb642c8f8c 100644 --- a/packages/react-router/__tests__/navigate-test.tsx +++ b/packages/react-router/__tests__/navigate-test.tsx @@ -9,6 +9,7 @@ import { RouterProvider, createMemoryRouter, useLocation, + createScopedMemoryRouterEnvironment, } from "react-router"; import { prettyDOM, render, screen, waitFor } from "@testing-library/react"; @@ -33,6 +34,47 @@ describe("", () => { `); }); + + it("navigates to the correct URL with NestableMemoryRouter", () => { + const { + MemoryRouter: NestableMemoryRouter, + Routes: NestedRoutes, + Route: NestedRoute, + Navigate: NestedNavigate, + } = createScopedMemoryRouterEnvironment(); + + function NestedMemoryRouter() { + return ( + + + } + /> + Nested About} /> + + + ); + } + + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create( + + + } /> + } /> + + + ); + }); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +

+ Nested About +

+ `); + }); }); describe("with a relative href (relative=route)", () => { diff --git a/packages/react-router/__tests__/nested-memory-router-test.tsx b/packages/react-router/__tests__/nested-memory-router-test.tsx new file mode 100644 index 0000000000..6fe11b9d2f --- /dev/null +++ b/packages/react-router/__tests__/nested-memory-router-test.tsx @@ -0,0 +1,120 @@ +import * as React from "react"; +import { render, prettyDOM } from "@testing-library/react"; +import "@testing-library/jest-dom"; + +import { + Route, + Routes, + MemoryRouter, + createScopedMemoryRouterEnvironment, + Navigate, +} from "react-router"; + +// Private API +import { _resetModuleScope } from "../lib/components"; + +const { + MemoryRouter: NestableMemoryRouter, + Routes: NestedRoutes, + Route: NestedRoute, + Navigate: NestedNavigate, +} = createScopedMemoryRouterEnvironment(); + +// eslint-disable-next-line jest/no-focused-tests +describe.only("", () => { + let consoleWarn: jest.SpyInstance; + let consoleError: jest.SpyInstance; + beforeEach(() => { + consoleWarn = jest.spyOn(console, "warn").mockImplementation(() => {}); + consoleError = jest.spyOn(console, "error").mockImplementation(() => {}); + }); + + afterEach(() => { + consoleWarn.mockRestore(); + consoleError.mockRestore(); + _resetModuleScope(); + }); + + it("renders the first route that matches the URL", () => { + let { container } = render( + + + Home} /> + + + ); + + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+

+ Home +

+
" + `); + }); + + it("can navigate with a NestableNavigate", () => { + function NestedMemoryRouter() { + return ( + + + } /> + + + ); + } + + let { container } = render( + + + } /> + About} /> + + + ); + + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+

+ About +

+
" + `); + }); + + it("can navigate MemoryRouter from NestableMemoryRouter", () => { + function NestedMemoryRouter() { + return ( + + + } /> + Nested About} /> + + + ); + } + + let { container } = render( + + + } /> + About} /> + + + ); + + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+

+ Nested About +

+
" + `); + }); +}); + +function getHtml(container: HTMLElement) { + return prettyDOM(container, undefined, { + highlight: false, + }); +} diff --git a/packages/react-router/__tests__/useLocation-test.tsx b/packages/react-router/__tests__/useLocation-test.tsx index 121073a0d5..6d28cabea6 100644 --- a/packages/react-router/__tests__/useLocation-test.tsx +++ b/packages/react-router/__tests__/useLocation-test.tsx @@ -1,6 +1,19 @@ import * as React from "react"; import * as TestRenderer from "react-test-renderer"; -import { MemoryRouter, Routes, Route, useLocation } from "react-router"; +import { + MemoryRouter, + Routes, + Route, + useLocation, + createScopedMemoryRouterEnvironment, +} from "react-router"; + +const { + MemoryRouter: NestableMemoryRouter, + Routes: NestedRoutes, + Route: NestedRoute, + useLocation: useNestedLocation, +} = createScopedMemoryRouterEnvironment(); function ShowLocation() { let location = useLocation(); @@ -84,4 +97,40 @@ describe("useLocation", () => { `); }); + + it("returns the current location object of NestedMemoryRouter", () => { + function ShowNestedPath() { + let { pathname, search, hash } = useNestedLocation(); + return
{JSON.stringify({ pathname, search, hash })}
; + } + + function NestedMemoryRouter() { + return ( + + + } /> + + + ); + } + + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create( + + + } /> + + + ); + }); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+        {"pathname":"/nested","search":"?the=nested-search","hash":"#the-nested-hash"}
+      
+ `); + }); }); diff --git a/packages/react-router/__tests__/useOutlet-test.tsx b/packages/react-router/__tests__/useOutlet-test.tsx index f283bd4874..e0b08f7b3c 100644 --- a/packages/react-router/__tests__/useOutlet-test.tsx +++ b/packages/react-router/__tests__/useOutlet-test.tsx @@ -6,8 +6,16 @@ import { Route, useOutlet, useOutletContext, + createScopedMemoryRouterEnvironment, } from "react-router"; +const { + MemoryRouter: NestableMemoryRouter, + Routes: NestedRoutes, + Route: NestedRoute, + useOutlet: useNestedOutlet, +} = createScopedMemoryRouterEnvironment(); + describe("useOutlet", () => { describe("when there is no child route", () => { it("returns null", () => { @@ -29,6 +37,35 @@ describe("useOutlet", () => { expect(renderer.toJSON()).toBeNull(); }); + it("returns null on NestableMemoryRouter", () => { + function Home() { + return useNestedOutlet(); + } + + function NestedMemoryRouter() { + return ( + + + } /> + + + ); + } + + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create( + + + } /> + + + ); + }); + + expect(renderer.toJSON()).toBeNull(); + }); + it("renders the fallback", () => { function Home() { let outlet = useOutlet(); @@ -53,6 +90,40 @@ describe("useOutlet", () => { `); }); + it("renders the fallback - NestableMemoryRouter", () => { + function Home() { + let outlet = useNestedOutlet(); + return
{outlet ? "outlet" : "no outlet"}
; + } + + function NestedMemoryRouter() { + return ( + + + } /> + + + ); + } + + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create( + + + } /> + + + ); + }); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ no outlet +
+ `); + }); + it("renders the fallback with context provided", () => { function Home() { let outlet = useOutlet({ some: "context" }); diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts index 999d2d7eeb..bedd723e72 100644 --- a/packages/react-router/index.ts +++ b/packages/react-router/index.ts @@ -49,18 +49,10 @@ import type { RoutesProps, RouterProviderProps, } from "./lib/components"; +import { createComponents } from "./lib/components"; import { enhanceManualRouteObjects, createRoutesFromChildren, - renderMatches, - Await, - MemoryRouter, - Navigate, - Outlet, - Route, - Router, - RouterProvider, - Routes, } from "./lib/components"; import type { DataRouteMatch, @@ -72,38 +64,103 @@ import type { RouteMatch, RouteObject, RelativeRoutingType, + ReactRouterContexts, } from "./lib/context"; -import { - DataRouterContext, - DataRouterStateContext, - DataStaticRouterContext, - LocationContext, - NavigationContext, - RouteContext, -} from "./lib/context"; -import type { NavigateFunction } from "./lib/hooks"; -import { - useHref, - useInRouterContext, - useLocation, - useMatch, - useNavigationType, - useNavigate, - useOutlet, - useOutletContext, - useParams, - useResolvedPath, - useRoutes, - useActionData, - useAsyncError, - useAsyncValue, - useLoaderData, - useMatches, - useNavigation, - useRevalidator, - useRouteError, - useRouteLoaderData, -} from "./lib/hooks"; +import { reactRouterContexts, createReactRouterContexts } from "./lib/context"; +import { DataStaticRouterContext } from "./lib/context"; +import type { Hooks, NavigateFunction } from "./lib/hooks"; +import { createHooks } from "./lib/hooks"; + +function createReactRouterEnvironment(contexts = reactRouterContexts) { + const hooks = createHooks(contexts); + const components = createComponents(contexts, hooks); + + return { + hooks, + components, + }; +} + +export function createScopedMemoryRouterEnvironment( + contexts?: ReactRouterContexts +) { + if (!contexts) { + contexts = createReactRouterContexts(); + } + const { hooks, components } = createReactRouterEnvironment(contexts); + + const { + useHref, + useInRouterContext, + useLocation, + useMatch, + useNavigationType, + useNavigate, + useOutlet, + useOutletContext, + useParams, + useResolvedPath, + useRoutes, + useActionData, + useAsyncError, + useAsyncValue, + useLoaderData, + useMatches, + useNavigation, + useRevalidator, + useRouteError, + useRouteLoaderData, + } = hooks; + + const { + renderMatches, + Await, + MemoryRouter, + Navigate, + Outlet, + Route, + Router, + RouterProvider, + Routes, + } = components; + + return { + useHref, + useInRouterContext, + useLocation, + useMatch, + useNavigationType, + useNavigate, + useOutlet, + useOutletContext, + useParams, + useResolvedPath, + useRoutes, + useActionData, + useAsyncError, + useAsyncValue, + useLoaderData, + useMatches, + useNavigation, + useRevalidator, + useRouteError, + useRouteLoaderData, + renderMatches, + Await, + MemoryRouter, + Navigate, + Outlet, + Route, + Router, + RouterProvider, + Routes, + UNSAFE_NavigationContext: contexts.NavigationContext, + UNSAFE_LocationContext: contexts.LocationContext, + UNSAFE_RouteContext: contexts.RouteContext, + UNSAFE_DataRouterContext: contexts.DataRouterContext, + UNSAFE_DataRouterStateContext: contexts.DataRouterStateContext, + }; +} // Exported for backwards compatibility, but not being used internally anymore type Hash = string; @@ -152,7 +209,45 @@ export type { Search, ShouldRevalidateFunction, To, + Hooks, }; + +const { + hooks: { + useActionData, + useAsyncError, + useAsyncValue, + useHref, + useInRouterContext, + useLoaderData, + useLocation, + useMatch, + useMatches, + useNavigate, + useNavigation, + useNavigationType, + useOutlet, + useOutletContext, + useParams, + useResolvedPath, + useRevalidator, + useRouteError, + useRouteLoaderData, + useRoutes, + }, + components: { + Await, + MemoryRouter, + Navigate, + Outlet, + Route, + Router, + RouterProvider, + Routes, + renderMatches, + }, +} = createReactRouterEnvironment(); + export { AbortedDeferredError, Await, @@ -233,12 +328,22 @@ export function createMemoryRouter( /////////////////////////////////////////////////////////////////////////////// /** @internal */ +const { + NavigationContext: DefaultNavigationContext, + LocationContext: DefaultLocationContext, + RouteContext: DefaultRouteContext, + DataRouterContext: DefaultDataRouterContext, + DataRouterStateContext: DefaultDataRouterStateContext, +} = reactRouterContexts; export { - NavigationContext as UNSAFE_NavigationContext, - LocationContext as UNSAFE_LocationContext, - RouteContext as UNSAFE_RouteContext, - DataRouterContext as UNSAFE_DataRouterContext, - DataRouterStateContext as UNSAFE_DataRouterStateContext, + DefaultNavigationContext as UNSAFE_NavigationContext, + DefaultLocationContext as UNSAFE_LocationContext, + DefaultRouteContext as UNSAFE_RouteContext, + DefaultDataRouterContext as UNSAFE_DataRouterContext, + DefaultDataRouterStateContext as UNSAFE_DataRouterStateContext, DataStaticRouterContext as UNSAFE_DataStaticRouterContext, enhanceManualRouteObjects as UNSAFE_enhanceManualRouteObjects, + createReactRouterEnvironment as UNSAFE_createReactRouterEnvironment, + createReactRouterContexts as UNSAFE_createReactRouterContexts, + reactRouterContexts as UNSAFE_reactRouterContexts, }; diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index 94ad090092..b2d24549bc 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -27,89 +27,82 @@ import type { Navigator, NonIndexRouteObject, RelativeRoutingType, + ReactRouterContexts, } from "./context"; -import { - LocationContext, - NavigationContext, - DataRouterContext, - DataRouterStateContext, - AwaitContext, -} from "./context"; -import { - useAsyncValue, - useInRouterContext, - useNavigate, - useOutlet, - useRoutes, - _renderMatches, -} from "./hooks"; + +import type { Hooks } from "./hooks"; export interface RouterProviderProps { fallbackElement?: React.ReactNode; router: RemixRouter; } -/** - * Given a Remix Router instance, render the appropriate UI - */ -export function RouterProvider({ - fallbackElement, - router, -}: RouterProviderProps): React.ReactElement { - // Sync router state to our component state to force re-renders - let state: RouterState = useSyncExternalStoreShim( - router.subscribe, - () => router.state, - // We have to provide this so React@18 doesn't complain during hydration, - // but we pass our serialized hydration data into the router so state here - // is already synced with what the server saw - () => router.state - ); +function createRouterProvider( + contexts: ReactRouterContexts, + Router: ReturnType, + Routes: ReturnType +) { + /** + * Given a Remix Router instance, render the appropriate UI + */ + return function RouterProvider({ + fallbackElement, + router, + }: RouterProviderProps): React.ReactElement { + // Sync router state to our component state to force re-renders + let state: RouterState = useSyncExternalStoreShim( + router.subscribe, + () => router.state, + // We have to provide this so React@18 doesn't complain during hydration, + // but we pass our serialized hydration data into the router so state here + // is already synced with what the server saw + () => router.state + ); - let navigator = React.useMemo((): Navigator => { - return { - createHref: router.createHref, - go: (n) => router.navigate(n), - push: (to, state, opts) => - router.navigate(to, { - state, - preventScrollReset: opts?.preventScrollReset, - }), - replace: (to, state, opts) => - router.navigate(to, { - replace: true, - state, - preventScrollReset: opts?.preventScrollReset, - }), - }; - }, [router]); - - let basename = router.basename || "/"; - - return ( - - - - {router.state.initialized ? : fallbackElement} - - - - ); + let navigator = React.useMemo((): Navigator => { + return { + createHref: router.createHref, + go: (n) => router.navigate(n), + push: (to, state, opts) => + router.navigate(to, { + state, + preventScrollReset: opts?.preventScrollReset, + }), + replace: (to, state, opts) => + router.navigate(to, { + replace: true, + state, + preventScrollReset: opts?.preventScrollReset, + }), + }; + }, [router]); + + let basename = router.basename || "/"; + + return ( + + + + {router.state.initialized ? : fallbackElement} + + + + ); + }; } - export interface MemoryRouterProps { basename?: string; children?: React.ReactNode; @@ -117,45 +110,46 @@ export interface MemoryRouterProps { initialIndex?: number; } -/** - * A that stores all entries in memory. - * - * @see https://reactrouter.com/docs/en/v6/routers/memory-router - */ -export function MemoryRouter({ - basename, - children, - initialEntries, - initialIndex, -}: MemoryRouterProps): React.ReactElement { - let historyRef = React.useRef(); - if (historyRef.current == null) { - historyRef.current = createMemoryHistory({ - initialEntries, - initialIndex, - v5Compat: true, - }); - } +function createMemoryRouter(Router: ReturnType) { + /** + * A that stores all entries in memory. + * + * @see https://reactrouter.com/docs/en/v6/routers/memory-router + */ + return function MemoryRouter({ + basename, + children, + initialEntries, + initialIndex, + }: MemoryRouterProps): React.ReactElement { + let historyRef = React.useRef(); + if (historyRef.current == null) { + historyRef.current = createMemoryHistory({ + initialEntries, + initialIndex, + v5Compat: true, + }); + } - let history = historyRef.current; - let [state, setState] = React.useState({ - action: history.action, - location: history.location, - }); + let history = historyRef.current; + let [state, setState] = React.useState({ + action: history.action, + location: history.location, + }); - React.useLayoutEffect(() => history.listen(setState), [history]); + React.useLayoutEffect(() => history.listen(setState), [history]); - return ( - - ); + return ( + + ); + }; } - export interface NavigateProps { to: To; replace?: boolean; @@ -163,64 +157,69 @@ export interface NavigateProps { relative?: RelativeRoutingType; } -/** - * Changes the current location. - * - * Note: This API is mostly useful in React.Component subclasses that are not - * able to use hooks. In functional components, we recommend you use the - * `useNavigate` hook instead. - * - * @see https://reactrouter.com/docs/en/v6/components/navigate - */ -export function Navigate({ - to, - replace, - state, - relative, -}: NavigateProps): null { - invariant( - useInRouterContext(), - // TODO: This error is probably because they somehow have 2 versions of - // the router loaded. We can help them understand how to avoid that. - ` may be used only in the context of a component.` - ); - - warning( - !React.useContext(NavigationContext).static, - ` must not be used on the initial render in a . ` + - `This is a no-op, but you should modify your code so the is ` + - `only ever rendered in response to some user interaction or state change.` - ); +function createNavigate( + { NavigationContext, DataRouterStateContext }: ReactRouterContexts, + { useInRouterContext, useNavigate }: Hooks +) { + /** + * Changes the current location. + * + * Note: This API is mostly useful in React.Component subclasses that are not + * able to use hooks. In functional components, we recommend you use the + * `useNavigate` hook instead. + * + * @see https://reactrouter.com/docs/en/v6/components/navigate + */ + return function Navigate({ + to, + replace, + state, + relative, + }: NavigateProps): null { + invariant( + useInRouterContext(), + // TODO: This error is probably because they somehow have 2 versions of + // the router loaded. We can help them understand how to avoid that. + ` may be used only in the context of a component.` + ); - let dataRouterState = React.useContext(DataRouterStateContext); - let navigate = useNavigate(); + warning( + !React.useContext(NavigationContext).static, + ` must not be used on the initial render in a . ` + + `This is a no-op, but you should modify your code so the is ` + + `only ever rendered in response to some user interaction or state change.` + ); - React.useEffect(() => { - // Avoid kicking off multiple navigations if we're in the middle of a - // data-router navigation, since components get re-rendered when we enter - // a submitting/loading state - if (dataRouterState && dataRouterState.navigation.state !== "idle") { - return; - } - navigate(to, { replace, state, relative }); - }); + let dataRouterState = React.useContext(DataRouterStateContext); + let navigate = useNavigate(); + + React.useEffect(() => { + // Avoid kicking off multiple navigations if we're in the middle of a + // data-router navigation, since components get re-rendered when we enter + // a submitting/loading state + if (dataRouterState && dataRouterState.navigation.state !== "idle") { + return; + } + navigate(to, { replace, state, relative }); + }); - return null; + return null; + }; } - export interface OutletProps { context?: unknown; } -/** - * Renders the child route's element, if there is one. - * - * @see https://reactrouter.com/docs/en/v6/components/outlet - */ -export function Outlet(props: OutletProps): React.ReactElement | null { - return useOutlet(props.context); +function createOutlet({ useOutlet }: Hooks) { + /** + * Renders the child route's element, if there is one. + * + * @see https://reactrouter.com/docs/en/v6/components/outlet + */ + return function Outlet(props: OutletProps): React.ReactElement | null { + return useOutlet(props.context); + }; } - export interface PathRouteProps { caseSensitive?: NonIndexRouteObject["caseSensitive"]; path?: NonIndexRouteObject["path"]; @@ -260,7 +259,7 @@ export type RouteProps = PathRouteProps | LayoutRouteProps | IndexRouteProps; * * @see https://reactrouter.com/docs/en/v6/components/route */ -export function Route(_props: RouteProps): React.ReactElement | null { +function Route(_props: RouteProps): React.ReactElement | null { invariant( false, `A is only ever to be used as the child of element, ` + @@ -277,84 +276,89 @@ export interface RouterProps { static?: boolean; } -/** - * Provides location context for the rest of the app. - * - * Note: You usually won't render a directly. Instead, you'll render a - * router that is more specific to your environment such as a - * in web browsers or a for server rendering. - * - * @see https://reactrouter.com/docs/en/v6/routers/router - */ -export function Router({ - basename: basenameProp = "/", - children = null, - location: locationProp, - navigationType = NavigationType.Pop, - navigator, - static: staticProp = false, -}: RouterProps): React.ReactElement | null { - invariant( - !useInRouterContext(), - `You cannot render a inside another .` + - ` You should never have more than one in your app.` - ); - - // Preserve trailing slashes on basename, so we can let the user control - // the enforcement of trailing slashes throughout the app - let basename = basenameProp.replace(/^\/*/, "/"); - let navigationContext = React.useMemo( - () => ({ basename, navigator, static: staticProp }), - [basename, navigator, staticProp] - ); +function createRouter({ + LocationContext, + NavigationContext, +}: ReactRouterContexts) { + /** + * Provides location context for the rest of the app. + * + * Note: You usually won't render a directly. Instead, you'll render a + * router that is more specific to your environment such as a + * in web browsers or a for server rendering. + * + * @see https://reactrouter.com/docs/en/v6/routers/router + */ + return function Router({ + basename: basenameProp = "/", + children = null, + location: locationProp, + navigationType = NavigationType.Pop, + navigator, + static: staticProp = false, + }: RouterProps): React.ReactElement | null { + invariant( + React.useContext(LocationContext) == null, + `You cannot render a inside another .` + + ` You should never have more than one in your app.` + ); - if (typeof locationProp === "string") { - locationProp = parsePath(locationProp); - } + // Preserve trailing slashes on basename, so we can let the user control + // the enforcement of trailing slashes throughout the app + let basename = basenameProp.replace(/^\/*/, "/"); + let navigationContext = React.useMemo( + () => ({ basename, navigator, static: staticProp }), + [basename, navigator, staticProp] + ); - let { - pathname = "/", - search = "", - hash = "", - state = null, - key = "default", - } = locationProp; + if (typeof locationProp === "string") { + locationProp = parsePath(locationProp); + } - let location = React.useMemo(() => { - let trailingPathname = stripBasename(pathname, basename); + let { + pathname = "/", + search = "", + hash = "", + state = null, + key = "default", + } = locationProp; + + let location = React.useMemo(() => { + let trailingPathname = stripBasename(pathname, basename); + + if (trailingPathname == null) { + return null; + } + + return { + pathname: trailingPathname, + search, + hash, + state, + key, + }; + }, [basename, pathname, search, hash, state, key]); + + warning( + location != null, + ` is not able to match the URL ` + + `"${pathname}${search}${hash}" because it does not start with the ` + + `basename, so the won't render anything.` + ); - if (trailingPathname == null) { + if (location == null) { return null; } - return { - pathname: trailingPathname, - search, - hash, - state, - key, - }; - }, [basename, pathname, search, hash, state, key]); - - warning( - location != null, - ` is not able to match the URL ` + - `"${pathname}${search}${hash}" because it does not start with the ` + - `basename, so the won't render anything.` - ); - - if (location == null) { - return null; - } - - return ( - - - - ); + return ( + + + + ); + }; } export interface RoutesProps { @@ -362,27 +366,31 @@ export interface RoutesProps { location?: Partial | string; } -/** - * A container for a nested tree of elements that renders the branch - * that best matches the current location. - * - * @see https://reactrouter.com/docs/en/v6/components/routes - */ -export function Routes({ - children, - location, -}: RoutesProps): React.ReactElement | null { - let dataRouterContext = React.useContext(DataRouterContext); - // When in a DataRouterContext _without_ children, we use the router routes - // directly. If we have children, then we're in a descendant tree and we - // need to use child routes. - let routes = - dataRouterContext && !children - ? (dataRouterContext.router.routes as DataRouteObject[]) - : createRoutesFromChildren(children); - return useRoutes(routes, location); +function createRoutes( + { DataRouterContext }: ReactRouterContexts, + { useRoutes }: Hooks +) { + /** + * A container for a nested tree of elements that renders the branch + * that best matches the current location. + * + * @see https://reactrouter.com/docs/en/v6/components/routes + */ + return function Routes({ + children, + location, + }: RoutesProps): React.ReactElement | null { + let dataRouterContext = React.useContext(DataRouterContext); + // When in a DataRouterContext _without_ children, we use the router routes + // directly. If we have children, then we're in a descendant tree and we + // need to use child routes. + let routes = + dataRouterContext && !children + ? (dataRouterContext.router.routes as DataRouteObject[]) + : createRoutesFromChildren(children); + return useRoutes(routes, location); + }; } - export interface AwaitResolveRenderFunction { (data: Awaited): React.ReactElement; } @@ -393,21 +401,30 @@ export interface AwaitProps { resolve: TrackedPromise | any; } -/** - * Component to use for rendering lazily loaded data from returning defer() - * in a loader function - */ -export function Await({ children, errorElement, resolve }: AwaitProps) { - return ( - - {children} - - ); +function createAwait({ AwaitContext }: ReactRouterContexts, hooks: Hooks) { + const ResolveAwait = createResolveAwait(hooks); + + /** + * Component to use for rendering lazily loaded data from returning defer() + * in a loader function + */ + return function Await({ children, errorElement, resolve }: AwaitProps) { + return ( + + {children} + + ); + }; } type AwaitErrorBoundaryProps = React.PropsWithChildren<{ errorElement?: React.ReactNode; resolve: TrackedPromise | any; + AwaitContext: ReactRouterContexts["AwaitContext"]; }>; type AwaitErrorBoundaryState = { @@ -444,7 +461,7 @@ class AwaitErrorBoundary extends React.Component< } render() { - let { children, errorElement, resolve } = this.props; + let { children, errorElement, resolve, AwaitContext } = this.props; let promise: TrackedPromise | null = null; let status: AwaitRenderStatus = AwaitRenderStatus.pending; @@ -511,20 +528,22 @@ class AwaitErrorBoundary extends React.Component< } } -/** - * @private - * Indirection to leverage useAsyncValue for a render-prop API on - */ -function ResolveAwait({ - children, -}: { - children: React.ReactNode | AwaitResolveRenderFunction; -}) { - let data = useAsyncValue(); - if (typeof children === "function") { - return children(data); - } - return <>{children}; +function createResolveAwait({ useAsyncValue }: Hooks) { + /** + * @private + * Indirection to leverage useAsyncValue for a render-prop API on + */ + return function ResolveAwait({ + children, + }: { + children: React.ReactNode | AwaitResolveRenderFunction; + }) { + let data = useAsyncValue(); + if (typeof children === "function") { + return children(data); + } + return <>{children}; + }; } /////////////////////////////////////////////////////////////////////////////// @@ -600,13 +619,15 @@ export function createRoutesFromChildren( return routes; } -/** - * Renders the result of `matchRoutes()` into a React element. - */ -export function renderMatches( - matches: RouteMatch[] | null -): React.ReactElement | null { - return _renderMatches(matches); +function createRenderMatches({ _renderMatches }: Hooks) { + /** + * Renders the result of `matchRoutes()` into a React element. + */ + return function renderMatches( + matches: RouteMatch[] | null + ): React.ReactElement | null { + return _renderMatches(matches); + }; } /** @@ -628,3 +649,85 @@ export function enhanceManualRouteObjects( return routeClone; }); } + +export function createComponents(contexts: ReactRouterContexts, hooks: Hooks) { + const Router = createRouter(contexts); + const Routes = createRoutes(contexts, hooks); + + const RouterProvider = createRouterProvider(contexts, Router, Routes); + + const MemoryRouter = createMemoryRouter(Router); + + const Navigate = createNavigate(contexts, hooks); + const Await = createAwait(contexts, hooks); + const Outlet = createOutlet(hooks); + + return { + /** + * Provides location context for the rest of the app. + * + * Note: You usually won't render a directly. Instead, you'll render a + * router that is more specific to your environment such as a + * in web browsers or a for server rendering. + * + * @see https://reactrouter.com/docs/en/v6/routers/router + */ + Router, + + /** + * Given a Remix Router instance, render the appropriate UI + */ + RouterProvider, + + /** + * A that stores all entries in memory. + * + * @see https://reactrouter.com/docs/en/v6/routers/memory-router + */ + MemoryRouter, + + /** + * Changes the current location. + * + * Note: This API is mostly useful in React.Component subclasses that are not + * able to use hooks. In functional components, we recommend you use the + * `useNavigate` hook instead. + * + * @see https://reactrouter.com/docs/en/v6/components/navigate + */ + Navigate, + + /** + * A container for a nested tree of elements that renders the branch + * that best matches the current location. + * + * @see https://reactrouter.com/docs/en/v6/components/routes + */ + Routes, + + /** + * Renders the result of `matchRoutes()` into a React element. + */ + renderMatches: createRenderMatches(hooks), + + /** + * Declares an element that should be rendered at a certain URL path. + * + * @see https://reactrouter.com/docs/en/v6/components/route + */ + Route, + + /** + * Renders the child route's element, if there is one. + * + * @see https://reactrouter.com/docs/en/v6/components/outlet + */ + Outlet, + + /** + * Component to use for rendering lazily loaded data from returning defer() + * in a loader function + */ + Await, + }; +} diff --git a/packages/react-router/lib/context.ts b/packages/react-router/lib/context.ts index af7182557d..89f1fe9642 100644 --- a/packages/react-router/lib/context.ts +++ b/packages/react-router/lib/context.ts @@ -69,22 +69,34 @@ export interface DataRouterContextObject extends NavigationContextObject { router: Router; } -export const DataRouterContext = - React.createContext(null); -if (__DEV__) { - DataRouterContext.displayName = "DataRouter"; +function createDataRouterContext() { + const DataRouterContext = React.createContext( + null + ); + if (__DEV__) { + DataRouterContext.displayName = "DataRouter"; + } + return DataRouterContext; } -export const DataRouterStateContext = React.createContext< - Router["state"] | null ->(null); -if (__DEV__) { - DataRouterStateContext.displayName = "DataRouterState"; +function createDataRouterStateContext() { + const DataRouterStateContext = React.createContext( + null + ); + + if (__DEV__) { + DataRouterStateContext.displayName = "DataRouterState"; + } + + return DataRouterStateContext; } -export const AwaitContext = React.createContext(null); -if (__DEV__) { - AwaitContext.displayName = "Await"; +function createAwaitContext() { + const AwaitContext = React.createContext(null); + if (__DEV__) { + AwaitContext.displayName = "Await"; + } + return AwaitContext; } export type RelativeRoutingType = "route" | "path"; @@ -112,49 +124,77 @@ export interface Navigator { replace(to: To, state?: any, opts?: NavigateOptions): void; } -interface NavigationContextObject { +export interface NavigationContextObject { basename: string; navigator: Navigator; static: boolean; } -export const NavigationContext = React.createContext( - null! -); +function createNavigationContext() { + const NavigationContext = React.createContext(null!); -if (__DEV__) { - NavigationContext.displayName = "Navigation"; -} + if (__DEV__) { + NavigationContext.displayName = "Navigation"; + } -interface LocationContextObject { + return NavigationContext; +} +export interface LocationContextObject { location: Location; navigationType: NavigationType; } -export const LocationContext = React.createContext( - null! -); - -if (__DEV__) { - LocationContext.displayName = "Location"; +function createLocationContext() { + const LocationContext = React.createContext(null!); + if (__DEV__) { + LocationContext.displayName = "Location"; + } + return LocationContext; } export interface RouteContextObject { outlet: React.ReactElement | null; matches: RouteMatch[]; } +function createRouteContext() { + const RouteContext = React.createContext({ + outlet: null, + matches: [], + }); -export const RouteContext = React.createContext({ - outlet: null, - matches: [], -}); + if (__DEV__) { + RouteContext.displayName = "Route"; + } -if (__DEV__) { - RouteContext.displayName = "Route"; + return RouteContext; } -export const RouteErrorContext = React.createContext(null); +function createRouteErrorContext() { + const RouteErrorContext = React.createContext(null); -if (__DEV__) { - RouteErrorContext.displayName = "RouteError"; + if (__DEV__) { + RouteErrorContext.displayName = "RouteError"; + } + + return RouteErrorContext; } + +function createOutletContext() { + return React.createContext(null); +} + +export function createReactRouterContexts() { + return { + LocationContext: createLocationContext(), + NavigationContext: createNavigationContext(), + DataRouterStateContext: createDataRouterStateContext(), + RouteContext: createRouteContext(), + OutletContext: createOutletContext(), + RouteErrorContext: createRouteErrorContext(), + AwaitContext: createAwaitContext(), + DataRouterContext: createDataRouterContext(), + }; +} + +export const reactRouterContexts = createReactRouterContexts(); +export type ReactRouterContexts = typeof reactRouterContexts; diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index d0a06f82e4..b2337d6cce 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -29,50 +29,47 @@ import type { RouteObject, DataRouteMatch, RelativeRoutingType, + ReactRouterContexts, } from "./context"; -import { - DataRouterContext, - DataRouterStateContext, - LocationContext, - NavigationContext, - RouteContext, - RouteErrorContext, - AwaitContext, - DataStaticRouterContext, -} from "./context"; +import { DataStaticRouterContext } from "./context"; + +function createHrefHook(contexts: ReactRouterContexts) { + const useInRouterContext = createInRouterContextHook(contexts); + const useResolvedPath = createResolvedPathHook(contexts); + + /** + * Returns the full href for the given "to" value. This is useful for building + * custom links that are also accessible and preserve right-click behavior. + * + * @see https://reactrouter.com/docs/en/v6/hooks/use-href + */ + return function useHref( + to: To, + { relative }: { relative?: RelativeRoutingType } = {} + ): string { + invariant( + useInRouterContext(), + // TODO: This error is probably because they somehow have 2 versions of the + // router loaded. We can help them understand how to avoid that. + `useHref() may be used only in the context of a component.` + ); -/** - * Returns the full href for the given "to" value. This is useful for building - * custom links that are also accessible and preserve right-click behavior. - * - * @see https://reactrouter.com/docs/en/v6/hooks/use-href - */ -export function useHref( - to: To, - { relative }: { relative?: RelativeRoutingType } = {} -): string { - invariant( - useInRouterContext(), - // TODO: This error is probably because they somehow have 2 versions of the - // router loaded. We can help them understand how to avoid that. - `useHref() may be used only in the context of a component.` - ); - - let { basename, navigator } = React.useContext(NavigationContext); - let { hash, pathname, search } = useResolvedPath(to, { relative }); - - let joinedPathname = pathname; - - // If we're operating within a basename, prepend it to the pathname prior - // to creating the href. If this is a root navigation, then just use the raw - // basename which allows the basename to have full control over the presence - // of a trailing slash on root links - if (basename !== "/") { - joinedPathname = - pathname === "/" ? basename : joinPaths([basename, pathname]); - } + let { basename, navigator } = React.useContext(contexts.NavigationContext); + let { hash, pathname, search } = useResolvedPath(to, { relative }); + + let joinedPathname = pathname; - return navigator.createHref({ pathname: joinedPathname, search, hash }); + // If we're operating within a basename, prepend it to the pathname prior + // to creating the href. If this is a root navigation, then just use the raw + // basename which allows the basename to have full control over the presence + // of a trailing slash on root links + if (basename !== "/") { + joinedPathname = + pathname === "/" ? basename : joinPaths([basename, pathname]); + } + + return navigator.createHref({ pathname: joinedPathname, search, hash }); + }; } /** @@ -80,64 +77,76 @@ export function useHref( * * @see https://reactrouter.com/docs/en/v6/hooks/use-in-router-context */ -export function useInRouterContext(): boolean { - return React.useContext(LocationContext) != null; +function createInRouterContextHook({ LocationContext }: ReactRouterContexts) { + return function useInRouterContext(): boolean { + return React.useContext(LocationContext) != null; + }; } -/** - * Returns the current location object, which represents the current URL in web - * browsers. - * - * Note: If you're using this it may mean you're doing some of your own - * "routing" in your app, and we'd like to know what your use case is. We may - * be able to provide something higher-level to better suit your needs. - * - * @see https://reactrouter.com/docs/en/v6/hooks/use-location - */ -export function useLocation(): Location { - invariant( - useInRouterContext(), - // TODO: This error is probably because they somehow have 2 versions of the - // router loaded. We can help them understand how to avoid that. - `useLocation() may be used only in the context of a component.` - ); - - return React.useContext(LocationContext).location; +function createLocationHook(contexts: ReactRouterContexts) { + const useInRouterContext = createInRouterContextHook(contexts); + + /** + * Returns the current location object, which represents the current URL in web + * browsers. + * + * Note: If you're using this it may mean you're doing some of your own + * "routing" in your app, and we'd like to know what your use case is. We may + * be able to provide something higher-level to better suit your needs. + * + * @see https://reactrouter.com/docs/en/v6/hooks/use-location + */ + return function useLocation(): Location { + invariant( + useInRouterContext(), + // TODO: This error is probably because they somehow have 2 versions of the + // router loaded. We can help them understand how to avoid that. + `useLocation() may be used only in the context of a component.` + ); + return React.useContext(contexts.LocationContext).location; + }; } -/** - * Returns the current navigation action which describes how the router came to - * the current location, either by a pop, push, or replace on the history stack. - * - * @see https://reactrouter.com/docs/en/v6/hooks/use-navigation-type - */ -export function useNavigationType(): NavigationType { - return React.useContext(LocationContext).navigationType; +function createNavigationTypeHook({ LocationContext }: ReactRouterContexts) { + /** + * Returns the current navigation action which describes how the router came to + * the current location, either by a pop, push, or replace on the history stack. + * + * @see https://reactrouter.com/docs/en/v6/hooks/use-navigation-type + */ + return function useNavigationType(): NavigationType { + return React.useContext(LocationContext).navigationType; + }; } -/** - * Returns true if the URL for the given "to" value matches the current URL. - * This is useful for components that need to know "active" state, e.g. - * . - * - * @see https://reactrouter.com/docs/en/v6/hooks/use-match - */ -export function useMatch< - ParamKey extends ParamParseKey, - Path extends string ->(pattern: PathPattern | Path): PathMatch | null { - invariant( - useInRouterContext(), - // TODO: This error is probably because they somehow have 2 versions of the - // router loaded. We can help them understand how to avoid that. - `useMatch() may be used only in the context of a component.` - ); - - let { pathname } = useLocation(); - return React.useMemo( - () => matchPath(pattern, pathname), - [pathname, pattern] - ); +function createMatchHook(contexts: ReactRouterContexts) { + const useInRouterContext = createInRouterContextHook(contexts); + const useLocation = createLocationHook(contexts); + + /** + * Returns true if the URL for the given "to" value matches the current URL. + * This is useful for components that need to know "active" state, e.g. + * . + * + * @see https://reactrouter.com/docs/en/v6/hooks/use-match + */ + return function useMatch< + ParamKey extends ParamParseKey, + Path extends string + >(pattern: PathPattern | Path): PathMatch | null { + invariant( + useInRouterContext(), + // TODO: This error is probably because they somehow have 2 versions of the + // router loaded. We can help them understand how to avoid that. + `useMatch() may be used only in the context of a component.` + ); + + let { pathname } = useLocation(); + return React.useMemo( + () => matchPath(pattern, pathname), + [pathname, pattern] + ); + }; } /** @@ -148,327 +157,357 @@ export interface NavigateFunction { (delta: number): void; } -/** - * Returns an imperative method for changing the location. Used by s, but - * may also be used by other elements to change the location. - * - * @see https://reactrouter.com/docs/en/v6/hooks/use-navigate - */ -export function useNavigate(): NavigateFunction { - invariant( - useInRouterContext(), - // TODO: This error is probably because they somehow have 2 versions of the - // router loaded. We can help them understand how to avoid that. - `useNavigate() may be used only in the context of a component.` - ); - - let { basename, navigator } = React.useContext(NavigationContext); - let { matches } = React.useContext(RouteContext); - let { pathname: locationPathname } = useLocation(); - - let routePathnamesJson = JSON.stringify( - getPathContributingMatches(matches).map((match) => match.pathnameBase) - ); - - let activeRef = React.useRef(false); - React.useEffect(() => { - activeRef.current = true; - }); - - let navigate: NavigateFunction = React.useCallback( - (to: To | number, options: NavigateOptions = {}) => { - warning( - activeRef.current, - `You should call navigate() in a React.useEffect(), not when ` + - `your component is first rendered.` - ); - - if (!activeRef.current) return; +function createNavigateHook(contexts: ReactRouterContexts) { + const useInRouterContext = createInRouterContextHook(contexts); + const useLocation = createLocationHook(contexts); + + /** + * Returns an imperative method for changing the location. Used by s, but + * may also be used by other elements to change the location. + * + * @see https://reactrouter.com/docs/en/v6/hooks/use-navigate + */ + return function useNavigate(): NavigateFunction { + invariant( + useInRouterContext(), + // TODO: This error is probably because they somehow have 2 versions of the + // router loaded. We can help them understand how to avoid that. + `useNavigate() may be used only in the context of a component.` + ); - if (typeof to === "number") { - navigator.go(to); - return; - } + let { basename, navigator } = React.useContext(contexts.NavigationContext); + let { matches } = React.useContext(contexts.RouteContext); + let { pathname: locationPathname } = useLocation(); - let path = resolveTo( - to, - JSON.parse(routePathnamesJson), - locationPathname, - options.relative === "path" - ); + let routePathnamesJson = JSON.stringify( + getPathContributingMatches(matches).map((match) => match.pathnameBase) + ); - // If we're operating within a basename, prepend it to the pathname prior - // to handing off to history. If this is a root navigation, then we - // navigate to the raw basename which allows the basename to have full - // control over the presence of a trailing slash on root links - if (basename !== "/") { - path.pathname = - path.pathname === "/" - ? basename - : joinPaths([basename, path.pathname]); - } + let activeRef = React.useRef(false); + React.useEffect(() => { + activeRef.current = true; + }); + + let navigate: NavigateFunction = React.useCallback( + (to: To | number, options: NavigateOptions = {}) => { + warning( + activeRef.current, + `You should call navigate() in a React.useEffect(), not when ` + + `your component is first rendered.` + ); + + if (!activeRef.current) return; + + if (typeof to === "number") { + navigator.go(to); + return; + } + + let path = resolveTo( + to, + JSON.parse(routePathnamesJson), + locationPathname + ); + + // If we're operating within a basename, prepend it to the pathname prior + // to handing off to history. If this is a root navigation, then we + // navigate to the raw basename which allows the basename to have full + // control over the presence of a trailing slash on root links + if (basename !== "/") { + path.pathname = + path.pathname === "/" + ? basename + : joinPaths([basename, path.pathname]); + } + + (!!options.replace ? navigator.replace : navigator.push)( + path, + options.state, + options + ); + }, + [basename, navigator, routePathnamesJson, locationPathname] + ); - (!!options.replace ? navigator.replace : navigator.push)( - path, - options.state, - options - ); - }, - [basename, navigator, routePathnamesJson, locationPathname] - ); + return navigate; + }; +} - return navigate; +function createOutletContextHook({ OutletContext }: ReactRouterContexts) { + /** + * Returns the context (if provided) for the child route at this level of the route + * hierarchy. + * @see https://reactrouter.com/docs/en/v6/hooks/use-outlet-context + */ + return function useOutletContext(): Context { + return React.useContext(OutletContext) as Context; + }; } -const OutletContext = React.createContext(null); +function createOutletHook({ + OutletContext, + RouteContext, +}: ReactRouterContexts) { + /** + * Returns the element for the child route at this level of the route + * hierarchy. Used internally by to render child routes. + * + * @see https://reactrouter.com/docs/en/v6/hooks/use-outlet + */ + return function useOutlet(context?: unknown): React.ReactElement | null { + let outlet = React.useContext(RouteContext).outlet; + if (outlet) { + return ( + + {outlet} + + ); + } + return outlet; + }; +} -/** - * Returns the context (if provided) for the child route at this level of the route - * hierarchy. - * @see https://reactrouter.com/docs/en/v6/hooks/use-outlet-context - */ -export function useOutletContext(): Context { - return React.useContext(OutletContext) as Context; +function createParamsHook({ RouteContext }: ReactRouterContexts) { + /** + * Returns an object of key/value pairs of the dynamic params from the current + * URL that were matched by the route path. + * + * @see https://reactrouter.com/docs/en/v6/hooks/use-params + */ + return function useParams< + ParamsOrKey extends string | Record = string + >(): Readonly< + [ParamsOrKey] extends [string] ? Params : Partial + > { + let { matches } = React.useContext(RouteContext); + let routeMatch = matches[matches.length - 1]; + return routeMatch ? (routeMatch.params as any) : {}; + }; } -/** - * Returns the element for the child route at this level of the route - * hierarchy. Used internally by to render child routes. - * - * @see https://reactrouter.com/docs/en/v6/hooks/use-outlet - */ -export function useOutlet(context?: unknown): React.ReactElement | null { - let outlet = React.useContext(RouteContext).outlet; - if (outlet) { - return ( - {outlet} +function createResolvedPathHook(contexts: ReactRouterContexts) { + const useLocation = createLocationHook(contexts); + + /** + * Resolves the pathname of the given `to` value against the current location. + * + * @see https://reactrouter.com/docs/en/v6/hooks/use-resolved-path + */ + return function useResolvedPath( + to: To, + { relative }: { relative?: RelativeRoutingType } = {} + ): Path { + let { matches } = React.useContext(contexts.RouteContext); + let { pathname: locationPathname } = useLocation(); + + let routePathnamesJson = JSON.stringify( + getPathContributingMatches(matches).map((match) => match.pathnameBase) ); - } - return outlet; -} -/** - * Returns an object of key/value pairs of the dynamic params from the current - * URL that were matched by the route path. - * - * @see https://reactrouter.com/docs/en/v6/hooks/use-params - */ -export function useParams< - ParamsOrKey extends string | Record = string ->(): Readonly< - [ParamsOrKey] extends [string] ? Params : Partial -> { - let { matches } = React.useContext(RouteContext); - let routeMatch = matches[matches.length - 1]; - return routeMatch ? (routeMatch.params as any) : {}; + return React.useMemo( + () => + resolveTo( + to, + JSON.parse(routePathnamesJson), + locationPathname, + relative === "path" + ), + [to, routePathnamesJson, locationPathname, relative] + ); + }; } -/** - * Resolves the pathname of the given `to` value against the current location. - * - * @see https://reactrouter.com/docs/en/v6/hooks/use-resolved-path - */ -export function useResolvedPath( - to: To, - { relative }: { relative?: RelativeRoutingType } = {} -): Path { - let { matches } = React.useContext(RouteContext); - let { pathname: locationPathname } = useLocation(); - - let routePathnamesJson = JSON.stringify( - getPathContributingMatches(matches).map((match) => match.pathnameBase) - ); - - return React.useMemo( - () => - resolveTo( - to, - JSON.parse(routePathnamesJson), - locationPathname, - relative === "path" - ), - [to, routePathnamesJson, locationPathname, relative] - ); -} +function createRoutesHook(contexts: ReactRouterContexts) { + const useInRouterContext = createInRouterContextHook(contexts); + const useLocation = createLocationHook(contexts); + const _renderMatches = _createRenderMatches(contexts); + + /** + * Returns the element of the route that matched the current location, prepared + * with the correct context to render the remainder of the route tree. Route + * elements in the tree must render an to render their child route's + * element. + * + * @see https://reactrouter.com/docs/en/v6/hooks/use-routes + */ + return function useRoutes( + routes: RouteObject[], + locationArg?: Partial | string + ): React.ReactElement | null { + invariant( + useInRouterContext(), + // TODO: This error is probably because they somehow have 2 versions of the + // router loaded. We can help them understand how to avoid that. + `useRoutes() may be used only in the context of a component.` + ); -/** - * Returns the element of the route that matched the current location, prepared - * with the correct context to render the remainder of the route tree. Route - * elements in the tree must render an to render their child route's - * element. - * - * @see https://reactrouter.com/docs/en/v6/hooks/use-routes - */ -export function useRoutes( - routes: RouteObject[], - locationArg?: Partial | string -): React.ReactElement | null { - invariant( - useInRouterContext(), - // TODO: This error is probably because they somehow have 2 versions of the - // router loaded. We can help them understand how to avoid that. - `useRoutes() may be used only in the context of a component.` - ); - - let dataRouterStateContext = React.useContext(DataRouterStateContext); - let { matches: parentMatches } = React.useContext(RouteContext); - let routeMatch = parentMatches[parentMatches.length - 1]; - let parentParams = routeMatch ? routeMatch.params : {}; - let parentPathname = routeMatch ? routeMatch.pathname : "/"; - let parentPathnameBase = routeMatch ? routeMatch.pathnameBase : "/"; - let parentRoute = routeMatch && routeMatch.route; - - if (__DEV__) { - // You won't get a warning about 2 different under a - // without a trailing *, but this is a best-effort warning anyway since we - // cannot even give the warning unless they land at the parent route. - // - // Example: - // - // - // {/* This route path MUST end with /* because otherwise - // it will never match /blog/post/123 */} - // } /> - // } /> - // - // - // function Blog() { - // return ( - // - // } /> - // - // ); - // } - let parentPath = (parentRoute && parentRoute.path) || ""; - warningOnce( - parentPathname, - !parentRoute || parentPath.endsWith("*"), - `You rendered descendant (or called \`useRoutes()\`) at ` + - `"${parentPathname}" (under ) but the ` + - `parent route path has no trailing "*". This means if you navigate ` + - `deeper, the parent won't match anymore and therefore the child ` + - `routes will never render.\n\n` + - `Please change the parent to .` + let dataRouterStateContext = React.useContext( + contexts.DataRouterStateContext ); - } + let { matches: parentMatches } = React.useContext(contexts.RouteContext); + let routeMatch = parentMatches[parentMatches.length - 1]; + let parentParams = routeMatch ? routeMatch.params : {}; + let parentPathname = routeMatch ? routeMatch.pathname : "/"; + let parentPathnameBase = routeMatch ? routeMatch.pathnameBase : "/"; + let parentRoute = routeMatch && routeMatch.route; + + if (__DEV__) { + // You won't get a warning about 2 different under a + // without a trailing *, but this is a best-effort warning anyway since we + // cannot even give the warning unless they land at the parent route. + // + // Example: + // + // + // {/* This route path MUST end with /* because otherwise + // it will never match /blog/post/123 */} + // } /> + // } /> + // + // + // function Blog() { + // return ( + // + // } /> + // + // ); + // } + let parentPath = (parentRoute && parentRoute.path) || ""; + warningOnce( + parentPathname, + !parentRoute || parentPath.endsWith("*"), + `You rendered descendant (or called \`useRoutes()\`) at ` + + `"${parentPathname}" (under ) but the ` + + `parent route path has no trailing "*". This means if you navigate ` + + `deeper, the parent won't match anymore and therefore the child ` + + `routes will never render.\n\n` + + `Please change the parent to .` + ); + } - let locationFromContext = useLocation(); + let locationFromContext = useLocation(); - let location; - if (locationArg) { - let parsedLocationArg = - typeof locationArg === "string" ? parsePath(locationArg) : locationArg; + let location; + if (locationArg) { + let parsedLocationArg = + typeof locationArg === "string" ? parsePath(locationArg) : locationArg; - invariant( - parentPathnameBase === "/" || - parsedLocationArg.pathname?.startsWith(parentPathnameBase), - `When overriding the location using \`\` or \`useRoutes(routes, location)\`, ` + - `the location pathname must begin with the portion of the URL pathname that was ` + - `matched by all parent routes. The current pathname base is "${parentPathnameBase}" ` + - `but pathname "${parsedLocationArg.pathname}" was given in the \`location\` prop.` - ); + invariant( + parentPathnameBase === "/" || + parsedLocationArg.pathname?.startsWith(parentPathnameBase), + `When overriding the location using \`\` or \`useRoutes(routes, location)\`, ` + + `the location pathname must begin with the portion of the URL pathname that was ` + + `matched by all parent routes. The current pathname base is "${parentPathnameBase}" ` + + `but pathname "${parsedLocationArg.pathname}" was given in the \`location\` prop.` + ); - location = parsedLocationArg; - } else { - location = locationFromContext; - } + location = parsedLocationArg; + } else { + location = locationFromContext; + } - let pathname = location.pathname || "/"; - let remainingPathname = - parentPathnameBase === "/" - ? pathname - : pathname.slice(parentPathnameBase.length) || "/"; + let pathname = location.pathname || "/"; + let remainingPathname = + parentPathnameBase === "/" + ? pathname + : pathname.slice(parentPathnameBase.length) || "/"; - let matches = matchRoutes(routes, { pathname: remainingPathname }); + let matches = matchRoutes(routes, { pathname: remainingPathname }); - if (__DEV__) { - warning( - parentRoute || matches != null, - `No routes matched location "${location.pathname}${location.search}${location.hash}" ` - ); + if (__DEV__) { + warning( + parentRoute || matches != null, + `No routes matched location "${location.pathname}${location.search}${location.hash}" ` + ); + + warning( + matches == null || + matches[matches.length - 1].route.element !== undefined, + `Matched leaf route at location "${location.pathname}${location.search}${location.hash}" does not have an element. ` + + `This means it will render an with a null value by default resulting in an "empty" page.` + ); + } - warning( - matches == null || - matches[matches.length - 1].route.element !== undefined, - `Matched leaf route at location "${location.pathname}${location.search}${location.hash}" does not have an element. ` + - `This means it will render an with a null value by default resulting in an "empty" page.` + let renderedMatches = _renderMatches( + matches && + matches.map((match) => + Object.assign({}, match, { + params: Object.assign({}, parentParams, match.params), + pathname: joinPaths([parentPathnameBase, match.pathname]), + pathnameBase: + match.pathnameBase === "/" + ? parentPathnameBase + : joinPaths([parentPathnameBase, match.pathnameBase]), + }) + ), + parentMatches, + dataRouterStateContext || undefined ); - } - let renderedMatches = _renderMatches( - matches && - matches.map((match) => - Object.assign({}, match, { - params: Object.assign({}, parentParams, match.params), - pathname: joinPaths([parentPathnameBase, match.pathname]), - pathnameBase: - match.pathnameBase === "/" - ? parentPathnameBase - : joinPaths([parentPathnameBase, match.pathnameBase]), - }) - ), - parentMatches, - dataRouterStateContext || undefined - ); - - // When a user passes in a `locationArg`, the associated routes need to - // be wrapped in a new `LocationContext.Provider` in order for `useLocation` - // to use the scoped location instead of the global location. - if (locationArg && renderedMatches) { + // When a user passes in a `locationArg`, the associated routes need to + // be wrapped in a new `LocationContext.Provider` in order for `useLocation` + // to use the scoped location instead of the global location. + if (locationArg && renderedMatches) { + return ( + + {renderedMatches} + + ); + } + + return renderedMatches; + }; +} + +function createDefaultErrorElement(contexts: ReactRouterContexts) { + const useRouteError = createRouteErrorHook(contexts); + + return function DefaultErrorElement() { + let error = useRouteError(); + let message = isRouteErrorResponse(error) + ? `${error.status} ${error.statusText}` + : error instanceof Error + ? error.message + : JSON.stringify(error); + let stack = error instanceof Error ? error.stack : null; + let lightgrey = "rgba(200,200,200, 0.5)"; + let preStyles = { padding: "0.5rem", backgroundColor: lightgrey }; + let codeStyles = { padding: "2px 4px", backgroundColor: lightgrey }; return ( - - {renderedMatches} - + <> +

Unhandled Thrown Error!

+

{message}

+ {stack ?
{stack}
: null} +

💿 Hey developer 👋

+

+ You can provide a way better UX than this when your app throws errors + by providing your own  + errorElement props on  + <Route> +

+ ); - } - - return renderedMatches; -} - -function DefaultErrorElement() { - let error = useRouteError(); - let message = isRouteErrorResponse(error) - ? `${error.status} ${error.statusText}` - : error instanceof Error - ? error.message - : JSON.stringify(error); - let stack = error instanceof Error ? error.stack : null; - let lightgrey = "rgba(200,200,200, 0.5)"; - let preStyles = { padding: "0.5rem", backgroundColor: lightgrey }; - let codeStyles = { padding: "2px 4px", backgroundColor: lightgrey }; - return ( - <> -

Unhandled Thrown Error!

-

{message}

- {stack ?
{stack}
: null} -

💿 Hey developer 👋

-

- You can provide a way better UX than this when your app throws errors by - providing your own  - errorElement props on  - <Route> -

- - ); + }; } type RenderErrorBoundaryProps = React.PropsWithChildren<{ location: Location; error: any; component: React.ReactNode; + RouteErrorContext: ReactRouterContexts["RouteErrorContext"]; }>; type RenderErrorBoundaryState = { @@ -530,6 +569,8 @@ export class RenderErrorBoundary extends React.Component< } render() { + const { RouteErrorContext } = this.props; + return this.state.error ? ( + {children} + + ); + }; +} - return ( - - {children} - - ); -} - -export function _renderMatches( - matches: RouteMatch[] | null, - parentMatches: RouteMatch[] = [], - dataRouterState?: RemixRouter["state"] -): React.ReactElement | null { - if (matches == null) { - if (dataRouterState?.errors) { - // Don't bail if we have data router errors so we can render them in the - // boundary. Use the pre-matched (or shimmed) matches - matches = dataRouterState.matches as DataRouteMatch[]; - } else { - return null; +function _createRenderMatches(contexts: ReactRouterContexts) { + const DefaultErrorElement = createDefaultErrorElement(contexts); + const RenderedRoute = createRenderedRoute(contexts); + + return function _renderMatches( + matches: RouteMatch[] | null, + parentMatches: RouteMatch[] = [], + dataRouterState?: RemixRouter["state"] + ): React.ReactElement | null { + if (matches == null) { + if (dataRouterState?.errors) { + // Don't bail if we have data router errors so we can render them in the + // boundary. Use the pre-matched (or shimmed) matches + matches = dataRouterState.matches as DataRouteMatch[]; + } else { + return null; + } } - } - let renderedMatches = matches; + let renderedMatches = matches; - // If we have data errors, trim matches to the highest error boundary - let errors = dataRouterState?.errors; - if (errors != null) { - let errorIndex = renderedMatches.findIndex( - (m) => m.route.id && errors?.[m.route.id] - ); - invariant( - errorIndex >= 0, - `Could not find a matching route for the current errors: ${errors}` - ); - renderedMatches = renderedMatches.slice( - 0, - Math.min(renderedMatches.length, errorIndex + 1) - ); - } + // If we have data errors, trim matches to the highest error boundary + let errors = dataRouterState?.errors; + if (errors != null) { + let errorIndex = renderedMatches.findIndex( + (m) => m.route.id && errors?.[m.route.id] + ); + invariant( + errorIndex >= 0, + `Could not find a matching route for the current errors: ${errors}` + ); + renderedMatches = renderedMatches.slice( + 0, + Math.min(renderedMatches.length, errorIndex + 1) + ); + } - return renderedMatches.reduceRight((outlet, match, index) => { - let error = match.route.id ? errors?.[match.route.id] : null; - // Only data routers handle errors - let errorElement = dataRouterState - ? match.route.errorElement || - : null; - let getChildren = () => ( - - {error - ? errorElement - : match.route.element !== undefined - ? match.route.element - : outlet} - - ); - // Only wrap in an error boundary within data router usages when we have an - // errorElement on this route. Otherwise let it bubble up to an ancestor - // errorElement - return dataRouterState && (match.route.errorElement || index === 0) ? ( - - ) : ( - getChildren() - ); - }, null as React.ReactElement | null); + return renderedMatches.reduceRight((outlet, match, index) => { + let error = match.route.id ? errors?.[match.route.id] : null; + // Only data routers handle errors + let errorElement = dataRouterState + ? match.route.errorElement || + : null; + let getChildren = () => ( + + {error + ? errorElement + : match.route.element !== undefined + ? match.route.element + : outlet} + + ); + // Only wrap in an error boundary within data router usages when we have an + // errorElement on this route. Otherwise let it bubble up to an ancestor + // errorElement + return dataRouterState && (match.route.errorElement || index === 0) ? ( + + ) : ( + getChildren() + ); + }, null as React.ReactElement | null); + }; } enum DataRouterHook { @@ -653,146 +706,184 @@ function getDataRouterConsoleError( return `${hookName} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`; } -function useDataRouterContext(hookName: DataRouterHook) { - let ctx = React.useContext(DataRouterContext); - invariant(ctx, getDataRouterConsoleError(hookName)); - return ctx; +function createDataRouterContextHook({ + DataRouterContext, +}: ReactRouterContexts) { + return function useDataRouterContext(hookName: DataRouterHook) { + let ctx = React.useContext(DataRouterContext); + invariant(ctx, getDataRouterConsoleError(hookName)); + return ctx; + }; } -function useDataRouterState(hookName: DataRouterStateHook) { - let state = React.useContext(DataRouterStateContext); - invariant(state, getDataRouterConsoleError(hookName)); - return state; +function createDataRouterStateHook({ + DataRouterStateContext, +}: ReactRouterContexts) { + return function useDataRouterState(hookName: DataRouterStateHook) { + let state = React.useContext(DataRouterStateContext); + invariant(state, getDataRouterConsoleError(hookName)); + return state; + }; } -/** - * Returns the current navigation, defaulting to an "idle" navigation when - * no navigation is in progress - */ -export function useNavigation() { - let state = useDataRouterState(DataRouterStateHook.UseNavigation); - return state.navigation; +function createNavigationHook(contexts: ReactRouterContexts) { + const useDataRouterState = createDataRouterStateHook(contexts); + + /** + * Returns the current navigation, defaulting to an "idle" navigation when + * no navigation is in progress + */ + return function useNavigation() { + let state = useDataRouterState(DataRouterStateHook.UseNavigation); + return state.navigation; + }; } -/** - * Returns a revalidate function for manually triggering revalidation, as well - * as the current state of any manual revalidations - */ -export function useRevalidator() { - let dataRouterContext = useDataRouterContext(DataRouterHook.UseRevalidator); - let state = useDataRouterState(DataRouterStateHook.UseRevalidator); - return { - revalidate: dataRouterContext.router.revalidate, - state: state.revalidation, +function createRevalidatorHook(contexts: ReactRouterContexts) { + const useDataRouterContext = createDataRouterContextHook(contexts); + const useDataRouterState = createDataRouterStateHook(contexts); + + /** + * Returns a revalidate function for manually triggering revalidation, as well + * as the current state of any manual revalidations + */ + return function useRevalidator() { + let dataRouterContext = useDataRouterContext(DataRouterHook.UseRevalidator); + let state = useDataRouterState(DataRouterStateHook.UseRevalidator); + return { + revalidate: dataRouterContext.router.revalidate, + state: state.revalidation, + }; }; } -/** - * Returns the active route matches, useful for accessing loaderData for - * parent/child routes or the route "handle" property - */ -export function useMatches() { - let { matches, loaderData } = useDataRouterState( - DataRouterStateHook.UseMatches - ); - return React.useMemo( - () => - matches.map((match) => { - let { pathname, params } = match; - // Note: This structure matches that created by createUseMatchesMatch - // in the @remix-run/router , so if you change this please also change - // that :) Eventually we'll DRY this up - return { - id: match.route.id, - pathname, - params, - data: loaderData[match.route.id] as unknown, - handle: match.route.handle as unknown, - }; - }), - [matches, loaderData] - ); +function createMatchesHook(contexts: ReactRouterContexts) { + const useDataRouterState = createDataRouterStateHook(contexts); + + /** + * Returns the active route matches, useful for accessing loaderData for + * parent/child routes or the route "handle" property + */ + return function useMatches() { + let { matches, loaderData } = useDataRouterState( + DataRouterStateHook.UseMatches + ); + return React.useMemo( + () => + matches.map((match) => { + let { pathname, params } = match; + return { + id: match.route.id, + pathname, + params, + data: loaderData[match.route.id] as unknown, + handle: match.route.handle as unknown, + }; + }), + [matches, loaderData] + ); + }; } -/** - * Returns the loader data for the nearest ancestor Route loader - */ -export function useLoaderData(): unknown { - let state = useDataRouterState(DataRouterStateHook.UseLoaderData); +function createLoaderDataHook(contexts: ReactRouterContexts) { + const useDataRouterState = createDataRouterStateHook(contexts); + + /** + * Returns the loader data for the nearest ancestor Route loader + */ + return function useLoaderData(): unknown { + let state = useDataRouterState(DataRouterStateHook.UseLoaderData); - let route = React.useContext(RouteContext); - invariant(route, `useLoaderData must be used inside a RouteContext`); + let route = React.useContext(contexts.RouteContext); + invariant(route, `useLoaderData must be used inside a RouteContext`); - let thisRoute = route.matches[route.matches.length - 1]; - invariant( - thisRoute.route.id, - `useLoaderData can only be used on routes that contain a unique "id"` - ); + let thisRoute = route.matches[route.matches.length - 1]; + invariant( + thisRoute.route.id, + `useLoaderData can only be used on routes that contain a unique "id"` + ); - return state.loaderData[thisRoute.route.id]; + return state.loaderData[thisRoute.route.id]; + }; } -/** - * Returns the loaderData for the given routeId - */ -export function useRouteLoaderData(routeId: string): unknown { - let state = useDataRouterState(DataRouterStateHook.UseRouteLoaderData); - return state.loaderData[routeId]; +function createRouteLoaderDataHook(contexts: ReactRouterContexts) { + const useDataRouterState = createDataRouterStateHook(contexts); + + /** + * Returns the loaderData for the given routeId + */ + return function useRouteLoaderData(routeId: string): unknown { + let state = useDataRouterState(DataRouterStateHook.UseRouteLoaderData); + return state.loaderData[routeId]; + }; } -/** - * Returns the action data for the nearest ancestor Route action - */ -export function useActionData(): unknown { - let state = useDataRouterState(DataRouterStateHook.UseActionData); +function createActionDataHook(contexts: ReactRouterContexts) { + const useDataRouterState = createDataRouterStateHook(contexts); + + /** + * Returns the action data for the nearest ancestor Route action + */ + return function useActionData(): unknown { + let state = useDataRouterState(DataRouterStateHook.UseActionData); - let route = React.useContext(RouteContext); - invariant(route, `useActionData must be used inside a RouteContext`); + let route = React.useContext(contexts.RouteContext); + invariant(route, `useActionData must be used inside a RouteContext`); - return Object.values(state?.actionData || {})[0]; + return Object.values(state?.actionData || {})[0]; + }; } -/** - * Returns the nearest ancestor Route error, which could be a loader/action - * error or a render error. This is intended to be called from your - * errorElement to display a proper error message. - */ -export function useRouteError(): unknown { - let error = React.useContext(RouteErrorContext); - let state = useDataRouterState(DataRouterStateHook.UseRouteError); - let route = React.useContext(RouteContext); - let thisRoute = route.matches[route.matches.length - 1]; - - // If this was a render error, we put it in a RouteError context inside - // of RenderErrorBoundary - if (error) { - return error; - } +function createRouteErrorHook(contexts: ReactRouterContexts) { + const useDataRouterState = createDataRouterStateHook(contexts); + + /** + * Returns the nearest ancestor Route error, which could be a loader/action + * error or a render error. This is intended to be called from your + * errorElement to display a proper error message. + */ + return function useRouteError(): unknown { + let error = React.useContext(contexts.RouteErrorContext); + let state = useDataRouterState(DataRouterStateHook.UseRouteError); + let route = React.useContext(contexts.RouteContext); + let thisRoute = route.matches[route.matches.length - 1]; + + // If this was a render error, we put it in a RouteError context inside + // of RenderErrorBoundary + if (error) { + return error; + } - invariant(route, `useRouteError must be used inside a RouteContext`); - invariant( - thisRoute.route.id, - `useRouteError can only be used on routes that contain a unique "id"` - ); + invariant(route, `useRouteError must be used inside a RouteContext`); + invariant( + thisRoute.route.id, + `useRouteError can only be used on routes that contain a unique "id"` + ); - // Otherwise look for errors from our data router state - return state.errors?.[thisRoute.route.id]; + // Otherwise look for errors from our data router state + return state.errors?.[thisRoute.route.id]; + }; } -/** - * Returns the happy-path data from the nearest ancestor value - */ -export function useAsyncValue(): unknown { - let value = React.useContext(AwaitContext); - return value?._data; +function createAsyncValueHook({ AwaitContext }: ReactRouterContexts) { + /** + * Returns the happy-path data from the nearest ancestor value + */ + return function useAsyncValue(): unknown { + let value = React.useContext(AwaitContext); + return value?._data; + }; } -/** - * Returns the error from the nearest ancestor value - */ -export function useAsyncError(): unknown { - let value = React.useContext(AwaitContext); - return value?._error; +function createAsyncErrorHook({ AwaitContext }: ReactRouterContexts) { + /** + * Returns the error from the nearest ancestor value + */ + return function useAsyncError(): unknown { + let value = React.useContext(AwaitContext); + return value?._error; + }; } const alreadyWarned: Record = {}; @@ -803,3 +894,176 @@ function warningOnce(key: string, cond: boolean, message: string) { warning(false, message); } } + +export function createHooks(contexts: ReactRouterContexts) { + const _renderMatches = _createRenderMatches(contexts); + + const useActionData = createActionDataHook(contexts); + const useAsyncValue = createAsyncValueHook(contexts); + const useAsyncError = createAsyncErrorHook(contexts); + const useHref = createHrefHook(contexts); + const useInRouterContext = createInRouterContextHook(contexts); + const useLoaderData = createLoaderDataHook(contexts); + const useLocation = createLocationHook(contexts); + const useMatch = createMatchHook(contexts); + const useMatches = createMatchesHook(contexts); + const useNavigate = createNavigateHook(contexts); + const useNavigation = createNavigationHook(contexts); + const useNavigationType = createNavigationTypeHook(contexts); + const useOutlet = createOutletHook(contexts); + const useOutletContext = createOutletContextHook(contexts); + const useParams = createParamsHook(contexts); + const useResolvedPath = createResolvedPathHook(contexts); + const useRevalidator = createRevalidatorHook(contexts); + const useRouteError = createRouteErrorHook(contexts); + const useRouteLoaderData = createRouteLoaderDataHook(contexts); + const useRoutes = createRoutesHook(contexts); + + return { + _renderMatches, + + /** + * Returns the action data for the nearest ancestor Route action + */ + useActionData, + + /** + * Returns the happy-path data from the nearest ancestor value + */ + useAsyncValue, + + /** + * Returns the error from the nearest ancestor value + */ + useAsyncError, + + /** + * Returns the full href for the given "to" value. This is useful for building + * custom links that are also accessible and preserve right-click behavior. + * + * @see https://reactrouter.com/docs/en/v6/hooks/use-href + */ + useHref, + + /** + * Returns true if this component is a descendant of a . + * + * @see https://reactrouter.com/docs/en/v6/hooks/use-in-router-context + */ + useInRouterContext, + + /** + * Returns the loader data for the nearest ancestor Route loader + */ + useLoaderData, + + /** + * Returns the current location object, which represents the current URL in web + * browsers. + * + * Note: If you're using this it may mean you're doing some of your own + * "routing" in your app, and we'd like to know what your use case is. We may + * be able to provide something higher-level to better suit your needs. + * + * @see https://reactrouter.com/docs/en/v6/hooks/use-location + */ + useLocation, + + /** + * Returns true if the URL for the given "to" value matches the current URL. + * This is useful for components that need to know "active" state, e.g. + * . + * + * @see https://reactrouter.com/docs/en/v6/hooks/use-match + */ + useMatch, + + /** + * Returns the active route matches, useful for accessing loaderData for + * parent/child routes or the route "handle" property + */ + useMatches, + + /** + * Returns an imperative method for changing the location. Used by s, but + * may also be used by other elements to change the location. + * + * @see https://reactrouter.com/docs/en/v6/hooks/use-navigate + */ + useNavigate, + + /** + * Returns the current navigation, defaulting to an "idle" navigation when + * no navigation is in progress + */ + useNavigation, + + /** + * Returns the current navigation action which describes how the router came to + * the current location, either by a pop, push, or replace on the history stack. + * + * @see https://reactrouter.com/docs/en/v6/hooks/use-navigation-type + */ + useNavigationType, + + /** + * Returns the element for the child route at this level of the route + * hierarchy. Used internally by to render child routes. + * + * @see https://reactrouter.com/docs/en/v6/hooks/use-outlet + */ + useOutlet, + + /** + * Returns the context (if provided) for the child route at this level of the route + * hierarchy. + * @see https://reactrouter.com/docs/en/v6/hooks/use-outlet-context + */ + useOutletContext, + + /** + * Returns an object of key/value pairs of the dynamic params from the current + * URL that were matched by the route path. + * + * @see https://reactrouter.com/docs/en/v6/hooks/use-params + */ + useParams, + + /** + * Resolves the pathname of the given `to` value against the current location. + * + * @see https://reactrouter.com/docs/en/v6/hooks/use-resolved-path + */ + useResolvedPath, + + /** + * Returns a revalidate function for manually triggering revalidation, as well + * as the current state of any manual revalidations + */ + useRevalidator, + + /** + * Returns the nearest ancestor Route error, which could be a loader/action + * error or a render error. This is intended to be called from your + * errorElement to display a proper error message. + */ + useRouteError, + + /** + * Returns the loaderData for the given routeId + */ + useRouteLoaderData, + + /** + * Returns the element of the route that matched the current location, prepared + * with the correct context to render the remainder of the route tree. Route + * elements in the tree must render an to render their child route's + * element. + * + * @see https://reactrouter.com/docs/en/v6/hooks/use-routes + */ + useRoutes, + }; +} + +export type Hooks = ReturnType; diff --git a/scripts/release/comment.mjs b/scripts/release/comment.mjs deleted file mode 100644 index 2e96577042..0000000000 --- a/scripts/release/comment.mjs +++ /dev/null @@ -1,66 +0,0 @@ -import { - commentOnIssue, - commentOnPullRequest, - getIssuesClosedByPullRequests, - prsMergedSinceLast, -} from "./octokit.mjs"; -import { LATEST_RELEASE, OWNER, REPO } from "./constants.mjs"; - -async function commentOnIssuesAndPrsAboutRelease() { - if (LATEST_RELEASE.includes("experimental")) { - return; - } - - let { merged, previousRelease } = await prsMergedSinceLast({ - owner: OWNER, - repo: REPO, - lastRelease: LATEST_RELEASE, - }); - - let suffix = merged.length === 1 ? "" : "s"; - console.log( - `Found ${merged.length} PR${suffix} merged since last release (latest: ${LATEST_RELEASE}, previous: ${previousRelease})` - ); - - let promises = []; - let issuesCommentedOn = new Set(); - - for (let pr of merged) { - console.log(`commenting on pr #${pr.number}`); - - promises.push( - commentOnPullRequest({ - owner: OWNER, - repo: REPO, - pr: pr.number, - version: LATEST_RELEASE, - }) - ); - - let issuesClosed = await getIssuesClosedByPullRequests( - pr.html_url, - pr.body - ); - - for (let issue of issuesClosed) { - if (issuesCommentedOn.has(issue.number)) { - // already commented on this issue - continue; - } - issuesCommentedOn.add(issue.number); - console.log(`commenting on issue #${issue.number}`); - promises.push( - commentOnIssue({ - issue: issue.number, - owner: OWNER, - repo: REPO, - version: LATEST_RELEASE, - }) - ); - } - } - - await Promise.all(promises); -} - -commentOnIssuesAndPrsAboutRelease(); diff --git a/scripts/release/comment.ts b/scripts/release/comment.ts new file mode 100644 index 0000000000..f8ef8f5f2e --- /dev/null +++ b/scripts/release/comment.ts @@ -0,0 +1,114 @@ +import { + VERSION, + OWNER, + REPO, + PR_FILES_STARTS_WITH, + IS_NIGHTLY_RELEASE, + AWAITING_RELEASE_LABEL, +} from "./constants"; +import { + closeIssue, + commentOnIssue, + commentOnPullRequest, + getIssuesClosedByPullRequests, + prsMergedSinceLastTag, + removeLabel, +} from "./github"; +import { getGitHubUrl } from "./utils"; + +async function commentOnIssuesAndPrsAboutRelease() { + if (VERSION.includes("experimental")) { + return; + } + + let { merged, previousTag } = await prsMergedSinceLastTag({ + owner: OWNER, + repo: REPO, + githubRef: VERSION, + }); + + let suffix = merged.length === 1 ? "" : "s"; + let prFilesDirs = PR_FILES_STARTS_WITH.join(", "); + console.log( + `Found ${merged.length} PR${suffix} merged ` + + `that touched \`${prFilesDirs}\` since ` + + `previous release (current: ${VERSION}, previous: ${previousTag})` + ); + + let promises: Array> = []; + let issuesCommentedOn = new Set(); + + for (let pr of merged) { + console.log(`commenting on pr ${getGitHubUrl("pull", pr.number)}`); + + promises.push( + commentOnPullRequest({ + owner: OWNER, + repo: REPO, + pr: pr.number, + version: VERSION, + }) + ); + + let prLabels = pr.labels.map((label) => label.name); + let prIsAwaitingRelease = prLabels.includes(AWAITING_RELEASE_LABEL); + + if (!IS_NIGHTLY_RELEASE && prIsAwaitingRelease) { + promises.push( + removeLabel({ owner: OWNER, repo: REPO, issue: pr.number }) + ); + } + + let issuesClosed = await getIssuesClosedByPullRequests( + pr.html_url, + pr.body + ); + + for (let issue of issuesClosed) { + if (issuesCommentedOn.has(issue.number)) { + // we already commented on this issue + // so we don't need to do it again + continue; + } + + issuesCommentedOn.add(issue.number); + let issueUrl = getGitHubUrl("issue", issue.number); + + if (IS_NIGHTLY_RELEASE || !prIsAwaitingRelease) { + console.log(`commenting on ${issueUrl}`); + promises.push( + commentOnIssue({ + owner: OWNER, + repo: REPO, + issue: issue.number, + version: VERSION, + }) + ); + } else { + console.log(`commenting on and closing ${issueUrl}`); + promises.push( + commentOnIssue({ + owner: OWNER, + repo: REPO, + issue: issue.number, + version: VERSION, + }) + ); + promises.push( + closeIssue({ owner: OWNER, repo: REPO, issue: issue.number }) + ); + } + } + } + + let result = await Promise.allSettled(promises); + let rejected = result.filter((r) => r.status === "rejected"); + if (rejected.length > 0) { + console.log( + "🚨 failed to comment on some issues/prs - the most likely reason is they were issues that were turned into discussions, which don't have an api to comment with" + ); + console.log(rejected); + } +} + +commentOnIssuesAndPrsAboutRelease(); diff --git a/scripts/release/constants.mjs b/scripts/release/constants.mjs deleted file mode 100644 index d64d87264e..0000000000 --- a/scripts/release/constants.mjs +++ /dev/null @@ -1,18 +0,0 @@ -if (!process.env.GITHUB_TOKEN) { - throw new Error("GITHUB_TOKEN is required"); -} -if (!process.env.GITHUB_REPOSITORY) { - throw new Error("GITHUB_REPOSITORY is required"); -} -if (!process.env.VERSION) { - throw new Error("VERSION is required"); -} -if (!process.env.VERSION.startsWith("refs/tags/")) { - throw new Error("VERSION must be a tag, received " + process.env.VERSION); -} - -export const [OWNER, REPO] = process.env.GITHUB_REPOSITORY.split("/"); -export const LATEST_RELEASE = process.env.VERSION.replace("refs/tags/", ""); -export const GITHUB_TOKEN = process.env.GITHUB_TOKEN; -export const GITHUB_REPOSITORY = process.env.GITHUB_REPOSITORY; -export const PR_FILES_STARTS_WITH = ["packages/"]; diff --git a/scripts/release/constants.ts b/scripts/release/constants.ts new file mode 100644 index 0000000000..d78288f362 --- /dev/null +++ b/scripts/release/constants.ts @@ -0,0 +1,34 @@ +import { cleanupRef, cleanupTagName, isNightly } from "./utils"; + +if (!process.env.DEFAULT_BRANCH) { + throw new Error("DEFAULT_BRANCH is required"); +} +if (!process.env.NIGHTLY_BRANCH) { + throw new Error("NIGHTLY_BRANCH is required"); +} +if (!process.env.GITHUB_TOKEN) { + throw new Error("GITHUB_TOKEN is required"); +} +if (!process.env.GITHUB_REPOSITORY) { + throw new Error("GITHUB_REPOSITORY is required"); +} +if (!process.env.VERSION) { + throw new Error("VERSION is required"); +} +if (!/^refs\/tags\//.test(process.env.VERSION)) { + throw new Error("VERSION must start with refs/tags/"); +} +if (!process.env.PACKAGE_VERSION_TO_FOLLOW) { + throw new Error("PACKAGE_VERSION_TO_FOLLOW is required"); +} + +export const [OWNER, REPO] = process.env.GITHUB_REPOSITORY.split("/"); +export const PACKAGE_VERSION_TO_FOLLOW = process.env.PACKAGE_VERSION_TO_FOLLOW; +export const VERSION = cleanupTagName(cleanupRef(process.env.VERSION)); +export const GITHUB_TOKEN = process.env.GITHUB_TOKEN; +export const GITHUB_REPOSITORY = process.env.GITHUB_REPOSITORY; +export const DEFAULT_BRANCH = process.env.DEFAULT_BRANCH; +export const NIGHTLY_BRANCH = process.env.NIGHTLY_BRANCH; +export const PR_FILES_STARTS_WITH = ["packages/"]; +export const IS_NIGHTLY_RELEASE = isNightly(VERSION); +export const AWAITING_RELEASE_LABEL = "awaiting release"; diff --git a/scripts/release/github.ts b/scripts/release/github.ts new file mode 100644 index 0000000000..1b685e9654 --- /dev/null +++ b/scripts/release/github.ts @@ -0,0 +1,448 @@ +import type { RestEndpointMethodTypes } from "@octokit/rest"; +import * as semver from "semver"; + +import { + PR_FILES_STARTS_WITH, + NIGHTLY_BRANCH, + DEFAULT_BRANCH, + PACKAGE_VERSION_TO_FOLLOW, + AWAITING_RELEASE_LABEL, +} from "./constants"; +import { gql, graphqlWithAuth, octokit } from "./octokit"; +import type { MinimalTag } from "./utils"; +import { cleanupTagName } from "./utils"; +import { checkIfStringStartsWith } from "./utils"; + +type PullRequest = + RestEndpointMethodTypes["pulls"]["list"]["response"]["data"][number]; + +type PullRequestFiles = + RestEndpointMethodTypes["pulls"]["listFiles"]["response"]["data"]; + +interface PrsMergedSinceLastTagOptions { + owner: string; + repo: string; + githubRef: string; +} + +interface PrsMergedSinceLastTagResult { + merged: Awaited>; + previousTag: string; +} + +export async function prsMergedSinceLastTag({ + owner, + repo, + githubRef, +}: PrsMergedSinceLastTagOptions): Promise { + let tags = await getTags(owner, repo); + let { currentTag, previousTag } = getPreviousTagFromCurrentTag( + githubRef, + tags + ); + + console.log(`Getting PRs merged ${previousTag.tag}...${currentTag.tag}`); + + /** + nightly > nightly => 'dev' + nightly > stable => 'main' + stable > nightly => 'dev' + */ + let prs: Awaited> = []; + + // if both the current and previous tags are prereleases + // we can just get the PRs for the "dev" branch + // but if one of them is stable, we should wind up all of them from both the main and dev branches + if (currentTag.isPrerelease && previousTag.isPrerelease) { + prs = await getMergedPRsBetweenTags( + owner, + repo, + previousTag, + currentTag, + NIGHTLY_BRANCH + ); + } else { + let [nightly, stable] = await Promise.all([ + getMergedPRsBetweenTags( + owner, + repo, + previousTag, + currentTag, + NIGHTLY_BRANCH + ), + getMergedPRsBetweenTags( + owner, + repo, + previousTag, + currentTag, + DEFAULT_BRANCH + ), + ]); + prs = nightly.concat(stable); + } + + let prsThatTouchedFiles = await getPullRequestWithFiles(owner, repo, prs); + + return { + merged: prsThatTouchedFiles, + previousTag: previousTag.tag, + }; +} + +type PullRequestWithFiles = PullRequest & { + files: PullRequestFiles; +}; + +async function getPullRequestWithFiles( + owner: string, + repo: string, + prs: Array +): Promise> { + let prsWithFiles = await Promise.all( + prs.map(async (pr) => { + let files = await octokit.paginate(octokit.pulls.listFiles, { + owner, + repo, + per_page: 100, + pull_number: pr.number, + }); + + return { ...pr, files }; + }) + ); + + return prsWithFiles.filter((pr) => { + return pr.files.some((file) => { + return checkIfStringStartsWith(file.filename, PR_FILES_STARTS_WITH); + }); + }); +} + +function getPreviousTagFromCurrentTag( + currentTag: string, + tags: Awaited> +): { + previousTag: MinimalTag; + currentTag: MinimalTag; +} { + let validTags = tags + .map((tag) => { + let tagName = cleanupTagName(tag.name); + let isPrerelease = semver.prerelease(tagName) !== null; + + let date = tag.target.committer?.date + ? new Date(tag.target.committer.date) + : tag.target.tagger?.date + ? new Date(tag.target.tagger.date) + : undefined; + + if (!date) return undefined; + + return { tag: tagName, date, isPrerelease }; + }) + .filter((v: any): v is MinimalTag => typeof v !== "undefined"); + + let currentTagIndex = validTags.findIndex((tag) => tag.tag === currentTag); + let currentTagInfo: MinimalTag | undefined = validTags.at(currentTagIndex); + let previousTagInfo: MinimalTag | undefined; + + if (!currentTagInfo) { + throw new Error(`Could not find last tag ${currentTag}`); + } + + // if the currentTag was a stable tag, then we want to find the previous stable tag + if (!currentTagInfo.isPrerelease) { + validTags = validTags + .filter((tag) => !tag.isPrerelease) + .sort((a, b) => semver.rcompare(a.tag, b.tag)); + + currentTagIndex = validTags.findIndex((tag) => tag.tag === currentTag); + currentTagInfo = validTags.at(currentTagIndex); + if (!currentTagInfo) { + throw new Error(`Could not find last stable tag ${currentTag}`); + } + } + + previousTagInfo = validTags.at(currentTagIndex + 1); + if (!previousTagInfo) { + throw new Error( + `Could not find previous prerelease tag from ${currentTag}` + ); + } + + return { + currentTag: currentTagInfo, + previousTag: previousTagInfo, + }; +} + +async function getMergedPRsBetweenTags( + owner: string, + repo: string, + startTag: MinimalTag, + endTag: MinimalTag, + baseRef: string, + page: number = 1, + nodes: Array = [] +): Promise> { + let pulls = await octokit.pulls.list({ + owner, + repo, + state: "closed", + sort: "updated", + direction: "desc", + per_page: 100, + page, + base: baseRef, + }); + + let merged = pulls.data.filter((pull) => { + if (!pull.merged_at) return false; + let mergedDate = new Date(pull.merged_at); + return mergedDate > startTag.date && mergedDate < endTag.date; + }); + + if (pulls.data.length !== 0) { + return getMergedPRsBetweenTags( + owner, + repo, + startTag, + endTag, + baseRef, + page + 1, + [...nodes, ...merged] + ); + } + + return [...nodes, ...merged]; +} + +interface GitHubGraphqlTag { + name: string; + target: { + oid: string; + committer?: { + date: string; + }; + tagger?: { + date: string; + }; + }; +} +interface GitHubGraphqlTagResponse { + repository: { + refs: { + nodes: Array; + }; + }; +} + +async function getTags(owner: string, repo: string) { + let response: GitHubGraphqlTagResponse = await graphqlWithAuth( + gql` + query GET_TAGS($owner: String!, $repo: String!) { + repository(owner: $owner, name: $repo) { + refs( + refPrefix: "refs/tags/" + first: 100 + orderBy: { field: TAG_COMMIT_DATE, direction: DESC } + ) { + nodes { + name + target { + oid + ... on Commit { + committer { + date + } + } + ... on Tag { + tagger { + date + } + } + } + } + } + } + } + `, + { owner, repo } + ); + + return response.repository.refs.nodes.filter((node) => { + return ( + node.name.startsWith(PACKAGE_VERSION_TO_FOLLOW) || + node.name.startsWith("v0.0.0-nightly-") + ); + }); +} + +export async function getIssuesClosedByPullRequests( + prHtmlUrl: string, + prBody: string | null +): Promise> { + let linkedIssues = await getIssuesLinkedToPullRequest(prHtmlUrl); + if (!prBody) { + return linkedIssues.map((issue) => { + return { number: issue.number }; + }); + } + + /** + * This regex matches for one of github's issue references for auto linking an issue to a PR + * as that only happens when the PR is sent to the default branch of the repo + * https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword + */ + let regex = + /(close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)(:)?\s#([0-9]+)/gi; + let matches = prBody.match(regex); + if (!matches) { + return linkedIssues.map((issue) => { + return { number: issue.number }; + }); + } + + let issuesMatch = matches.map((match) => { + let [, issueNumber] = match.split(" #"); + return { number: parseInt(issueNumber, 10) }; + }); + + let issues = await Promise.all( + issuesMatch.map(async (issue) => { + return { number: issue.number }; + }) + ); + + return [...linkedIssues, ...issues.filter((issue) => issue !== null)]; +} + +interface GitHubClosingIssueReference { + resource: { + closingIssuesReferences: { + pageInfo: { + endCursor: string; + hasNextPage: boolean; + }; + nodes: Array<{ number: number }>; + }; + }; +} + +type IssuesLinkedToPullRequest = Array<{ number: number }>; + +async function getIssuesLinkedToPullRequest( + prHtmlUrl: string, + nodes: IssuesLinkedToPullRequest = [], + after?: string +): Promise { + let res: GitHubClosingIssueReference = await graphqlWithAuth( + gql` + query GET_ISSUES_CLOSED_BY_PR($prHtmlUrl: URI!, $after: String) { + resource(url: $prHtmlUrl) { + ... on PullRequest { + closingIssuesReferences(first: 100, after: $after) { + nodes { + number + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `, + { prHtmlUrl, after } + ); + + let newNodes = res?.resource?.closingIssuesReferences?.nodes ?? []; + nodes.push( + ...newNodes.map((node) => { + return { number: node.number }; + }) + ); + + if (res?.resource?.closingIssuesReferences?.pageInfo?.hasNextPage) { + return getIssuesLinkedToPullRequest( + prHtmlUrl, + nodes, + res?.resource?.closingIssuesReferences?.pageInfo?.endCursor + ); + } + + return nodes; +} + +export async function commentOnPullRequest({ + owner, + repo, + pr, + version, +}: { + owner: string; + repo: string; + pr: number; + version: string; +}) { + await octokit.issues.createComment({ + owner, + repo, + issue_number: pr, + body: `🤖 Hello there,\n\nWe just published version \`${version}\` which includes this pull request. If you'd like to take it for a test run please try it out and let us know what you think!\n\nThanks!`, + }); +} + +export async function commentOnIssue({ + owner, + repo, + issue, + version, +}: { + owner: string; + repo: string; + issue: number; + version: string; +}) { + await octokit.issues.createComment({ + owner, + repo, + issue_number: issue, + body: `🤖 Hello there,\n\nWe just published version \`${version}\` which involves this issue. If you'd like to take it for a test run please try it out and let us know what you think!\n\nThanks!`, + }); +} + +export async function closeIssue({ + owner, + repo, + issue, +}: { + owner: string; + repo: string; + issue: number; +}) { + await octokit.issues.update({ + owner, + repo, + issue_number: issue, + state: "closed", + }); +} + +export async function removeLabel({ + owner, + repo, + issue, +}: { + owner: string; + repo: string; + issue: number; +}) { + await octokit.issues.removeLabel({ + owner, + repo, + issue_number: issue, + name: AWAITING_RELEASE_LABEL, + }); +} diff --git a/scripts/release/octokit.mjs b/scripts/release/octokit.mjs deleted file mode 100644 index 4496f40f30..0000000000 --- a/scripts/release/octokit.mjs +++ /dev/null @@ -1,186 +0,0 @@ -import { Octokit as RestOctokit } from "@octokit/rest"; -import { paginateRest } from "@octokit/plugin-paginate-rest"; -import { graphql } from "@octokit/graphql"; - -import { - GITHUB_TOKEN, - GITHUB_REPOSITORY, - PR_FILES_STARTS_WITH, -} from "./constants.mjs"; - -const graphqlWithAuth = graphql.defaults({ - headers: { authorization: `token ${GITHUB_TOKEN}` }, -}); - -const Octokit = RestOctokit.plugin(paginateRest); -const octokit = new Octokit({ auth: GITHUB_TOKEN }); - -const gql = String.raw; - -export async function prsMergedSinceLast({ - owner, - repo, - lastRelease: lastReleaseVersion, -}) { - let releases = await octokit.paginate(octokit.rest.repos.listReleases, { - owner, - repo, - per_page: 100, - }); - - let sorted = releases - .sort((a, b) => { - return new Date(b.published_at) - new Date(a.published_at); - }) - .filter((release) => { - return release.tag_name.includes("experimental") === false; - }); - - let lastReleaseIndex = sorted.findIndex((release) => { - return release.tag_name === lastReleaseVersion; - }); - - let lastRelease = sorted[lastReleaseIndex]; - if (!lastRelease) { - throw new Error( - `Could not find last release ${lastRelease} in ${GITHUB_REPOSITORY}` - ); - } - - // if the lastRelease was a stable release, then we want to find the previous stable release - let previousRelease; - if (lastRelease.prerelease === false) { - let stableReleases = sorted.filter((release) => { - return release.prerelease === false; - }); - previousRelease = stableReleases.at(1); - } else { - previousRelease = sorted.at(lastReleaseIndex + 1); - } - - if (!previousRelease) { - throw new Error(`Could not find previous release in ${GITHUB_REPOSITORY}`); - } - - let startDate = new Date(previousRelease.created_at); - let endDate = new Date(lastRelease.created_at); - - let prs = await octokit.paginate(octokit.pulls.list, { - owner, - repo, - state: "closed", - sort: "updated", - direction: "desc", - }); - - let mergedPullRequestsSinceLastTag = prs.filter((pullRequest) => { - if (!pullRequest.merged_at) return false; - let mergedDate = new Date(pullRequest.merged_at); - return mergedDate > startDate && mergedDate < endDate; - }); - - let prsWithFiles = await Promise.all( - mergedPullRequestsSinceLastTag.map(async (pr) => { - let files = await octokit.paginate(octokit.pulls.listFiles, { - owner, - repo, - per_page: 100, - pull_number: pr.number, - }); - - return { - ...pr, - files, - }; - }) - ); - - return { - previousRelease: previousRelease.tag_name, - merged: prsWithFiles.filter((pr) => { - return pr.files.some((file) => { - return checkIfStringStartsWith(file.filename, PR_FILES_STARTS_WITH); - }); - }), - }; -} - -export async function commentOnPullRequest({ owner, repo, pr, version }) { - await octokit.issues.createComment({ - owner, - repo, - issue_number: pr, - body: `🤖 Hello there,\n\nWe just published version \`${version}\` which includes this pull request. If you'd like to take it for a test run please try it out and let us know what you think!\n\nThanks!`, - }); -} - -export async function commentOnIssue({ owner, repo, issue, version }) { - await octokit.issues.createComment({ - owner, - repo, - issue_number: issue, - body: `🤖 Hello there,\n\nWe just published version \`${version}\` which involves this issue. If you'd like to take it for a test run please try it out and let us know what you think!\n\nThanks!`, - }); -} - -async function getIssuesLinkedToPullRequest(prHtmlUrl, nodes = [], after) { - let res = await graphqlWithAuth( - gql` - query GET_ISSUES_CLOSED_BY_PR($prHtmlUrl: URI!, $after: String) { - resource(url: $prHtmlUrl) { - ... on PullRequest { - closingIssuesReferences(first: 100, after: $after) { - nodes { - number - } - pageInfo { - hasNextPage - endCursor - } - } - } - } - } - `, - { prHtmlUrl, after } - ); - - let newNodes = res?.resource?.closingIssuesReferences?.nodes ?? []; - nodes.push(...newNodes); - - if (res?.resource?.closingIssuesReferences?.pageInfo?.hasNextPage) { - return getIssuesLinkedToPullRequest( - prHtmlUrl, - nodes, - res?.resource?.closingIssuesReferences?.pageInfo?.endCursor - ); - } - - return nodes; -} - -export async function getIssuesClosedByPullRequests(prHtmlUrl, prBody) { - let linked = await getIssuesLinkedToPullRequest(prHtmlUrl); - if (!prBody) return linked; - - /** - * This regex matches for one of github's issue references for auto linking an issue to a PR - * as that only happens when the PR is sent to the default branch of the repo - * https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword - */ - let regex = - /(close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s#([0-9]+)/gi; - let matches = prBody.match(regex); - if (!matches) return linked; - - let issues = matches.map((match) => { - let [, issueNumber] = match.split(" #"); - return { number: parseInt(issueNumber, 10) }; - }); - - return [...linked, ...issues.filter((issue) => issue !== null)]; -} - -function checkIfStringStartsWith(string, substrings) { - return substrings.some((substr) => string.startsWith(substr)); -} diff --git a/scripts/release/octokit.ts b/scripts/release/octokit.ts new file mode 100644 index 0000000000..6be758a13c --- /dev/null +++ b/scripts/release/octokit.ts @@ -0,0 +1,42 @@ +import { Octokit as RestOctokit } from "@octokit/rest"; +import type { Octokit as OctokitType } from "@octokit/rest"; +import { paginateRest } from "@octokit/plugin-paginate-rest"; +import { throttling } from "@octokit/plugin-throttling"; +import { graphql } from "@octokit/graphql"; + +import { GITHUB_TOKEN } from "./constants"; + +export const graphqlWithAuth = graphql.defaults({ + headers: { authorization: `token ${GITHUB_TOKEN}` }, +}); + +const Octokit = RestOctokit.plugin(paginateRest, throttling); + +export const octokit = new Octokit({ + auth: GITHUB_TOKEN, + throttle: { + onRateLimit(retryAfter: number, options: any, octokit: OctokitType) { + octokit.log.warn( + `Request quota exhausted for request ${options.method} ${options.url}` + ); + + if (options.request.retryCount === 0) { + // only retries once + octokit.log.info(`Retrying after ${retryAfter} seconds!`); + return true; + } + }, + onSecondaryRateLimit( + _retryAfter: number, + options: any, + octokit: OctokitType + ) { + // does not retry, only logs a warning + octokit.log.warn( + `SecondaryRateLimit detected for request ${options.method} ${options.url}` + ); + }, + }, +}); + +export const gql = String.raw; diff --git a/scripts/release/package-lock.json b/scripts/release/package-lock.json new file mode 100644 index 0000000000..0493c693bd --- /dev/null +++ b/scripts/release/package-lock.json @@ -0,0 +1,1047 @@ +{ + "name": "release-comments", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "release-comments", + "dependencies": { + "@octokit/plugin-paginate-rest": "^2.17.0", + "@octokit/plugin-throttling": "^3.6.2", + "@octokit/rest": "^18.12.0", + "esbuild": "^0.14.38", + "esbuild-register": "^3.3.2", + "semver": "^7.3.7" + }, + "devDependencies": { + "@tsconfig/node16": "^1.0.2", + "@types/node": "^17.0.23", + "@types/semver": "^7.3.9", + "typescript": "^4.7.4" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz", + "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@octokit/auth-token": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", + "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", + "dependencies": { + "@octokit/types": "^6.0.3" + } + }, + "node_modules/@octokit/core": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz", + "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==", + "dependencies": { + "@octokit/auth-token": "^2.4.4", + "@octokit/graphql": "^4.5.8", + "@octokit/request": "^5.6.3", + "@octokit/request-error": "^2.0.5", + "@octokit/types": "^6.0.3", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/endpoint": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", + "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", + "dependencies": { + "@octokit/types": "^6.0.3", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/graphql": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", + "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", + "dependencies": { + "@octokit/request": "^5.6.0", + "@octokit/types": "^6.0.3", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "12.11.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", + "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "2.21.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.21.3.tgz", + "integrity": "sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw==", + "dependencies": { + "@octokit/types": "^6.40.0" + }, + "peerDependencies": { + "@octokit/core": ">=2" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", + "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "5.16.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.16.2.tgz", + "integrity": "sha512-8QFz29Fg5jDuTPXVtey05BLm7OB+M8fnvE64RNegzX7U+5NUXcOcnpTIK0YfSHBg8gYd0oxIq3IZTe9SfPZiRw==", + "dependencies": { + "@octokit/types": "^6.39.0", + "deprecation": "^2.3.1" + }, + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/plugin-throttling": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-3.7.0.tgz", + "integrity": "sha512-qrKT1Yl/KuwGSC6/oHpLBot3ooC9rq0/ryDYBCpkRtoj+R8T47xTMDT6Tk2CxWopFota/8Pi/2SqArqwC0JPow==", + "dependencies": { + "@octokit/types": "^6.0.1", + "bottleneck": "^2.15.3" + }, + "peerDependencies": { + "@octokit/core": "^3.5.0" + } + }, + "node_modules/@octokit/request": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", + "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", + "dependencies": { + "@octokit/endpoint": "^6.0.1", + "@octokit/request-error": "^2.1.0", + "@octokit/types": "^6.16.1", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/request-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", + "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", + "dependencies": { + "@octokit/types": "^6.0.3", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "node_modules/@octokit/rest": { + "version": "18.12.0", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.12.0.tgz", + "integrity": "sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==", + "dependencies": { + "@octokit/core": "^3.5.1", + "@octokit/plugin-paginate-rest": "^2.16.8", + "@octokit/plugin-request-log": "^1.0.4", + "@octokit/plugin-rest-endpoint-methods": "^5.12.0" + } + }, + "node_modules/@octokit/types": { + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", + "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", + "dependencies": { + "@octokit/openapi-types": "^12.11.0" + } + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.3.12", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.12.tgz", + "integrity": "sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A==", + "dev": true + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + }, + "node_modules/bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==" + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, + "node_modules/esbuild": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz", + "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/linux-loong64": "0.14.54", + "esbuild-android-64": "0.14.54", + "esbuild-android-arm64": "0.14.54", + "esbuild-darwin-64": "0.14.54", + "esbuild-darwin-arm64": "0.14.54", + "esbuild-freebsd-64": "0.14.54", + "esbuild-freebsd-arm64": "0.14.54", + "esbuild-linux-32": "0.14.54", + "esbuild-linux-64": "0.14.54", + "esbuild-linux-arm": "0.14.54", + "esbuild-linux-arm64": "0.14.54", + "esbuild-linux-mips64le": "0.14.54", + "esbuild-linux-ppc64le": "0.14.54", + "esbuild-linux-riscv64": "0.14.54", + "esbuild-linux-s390x": "0.14.54", + "esbuild-netbsd-64": "0.14.54", + "esbuild-openbsd-64": "0.14.54", + "esbuild-sunos-64": "0.14.54", + "esbuild-windows-32": "0.14.54", + "esbuild-windows-64": "0.14.54", + "esbuild-windows-arm64": "0.14.54" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz", + "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz", + "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz", + "integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz", + "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz", + "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz", + "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz", + "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz", + "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz", + "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz", + "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz", + "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz", + "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz", + "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz", + "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz", + "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz", + "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-register": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.3.3.tgz", + "integrity": "sha512-eFHOkutgIMJY5gc8LUp/7c+LLlDqzNi9T6AwCZ2WKKl3HmT+5ef3ZRyPPxDOynInML0fgaC50yszPKfPnjC0NQ==", + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz", + "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz", + "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz", + "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz", + "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/typescript": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + }, + "dependencies": { + "@esbuild/linux-loong64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz", + "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==", + "optional": true + }, + "@octokit/auth-token": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", + "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", + "requires": { + "@octokit/types": "^6.0.3" + } + }, + "@octokit/core": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz", + "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==", + "requires": { + "@octokit/auth-token": "^2.4.4", + "@octokit/graphql": "^4.5.8", + "@octokit/request": "^5.6.3", + "@octokit/request-error": "^2.0.5", + "@octokit/types": "^6.0.3", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/endpoint": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", + "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", + "requires": { + "@octokit/types": "^6.0.3", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/graphql": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", + "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", + "requires": { + "@octokit/request": "^5.6.0", + "@octokit/types": "^6.0.3", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/openapi-types": { + "version": "12.11.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", + "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" + }, + "@octokit/plugin-paginate-rest": { + "version": "2.21.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.21.3.tgz", + "integrity": "sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw==", + "requires": { + "@octokit/types": "^6.40.0" + } + }, + "@octokit/plugin-request-log": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", + "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", + "requires": {} + }, + "@octokit/plugin-rest-endpoint-methods": { + "version": "5.16.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.16.2.tgz", + "integrity": "sha512-8QFz29Fg5jDuTPXVtey05BLm7OB+M8fnvE64RNegzX7U+5NUXcOcnpTIK0YfSHBg8gYd0oxIq3IZTe9SfPZiRw==", + "requires": { + "@octokit/types": "^6.39.0", + "deprecation": "^2.3.1" + } + }, + "@octokit/plugin-throttling": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-3.7.0.tgz", + "integrity": "sha512-qrKT1Yl/KuwGSC6/oHpLBot3ooC9rq0/ryDYBCpkRtoj+R8T47xTMDT6Tk2CxWopFota/8Pi/2SqArqwC0JPow==", + "requires": { + "@octokit/types": "^6.0.1", + "bottleneck": "^2.15.3" + } + }, + "@octokit/request": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", + "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", + "requires": { + "@octokit/endpoint": "^6.0.1", + "@octokit/request-error": "^2.1.0", + "@octokit/types": "^6.16.1", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/request-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", + "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", + "requires": { + "@octokit/types": "^6.0.3", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "@octokit/rest": { + "version": "18.12.0", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.12.0.tgz", + "integrity": "sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==", + "requires": { + "@octokit/core": "^3.5.1", + "@octokit/plugin-paginate-rest": "^2.16.8", + "@octokit/plugin-request-log": "^1.0.4", + "@octokit/plugin-rest-endpoint-methods": "^5.12.0" + } + }, + "@octokit/types": { + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", + "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", + "requires": { + "@octokit/openapi-types": "^12.11.0" + } + }, + "@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, + "@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", + "dev": true + }, + "@types/semver": { + "version": "7.3.12", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.12.tgz", + "integrity": "sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A==", + "dev": true + }, + "before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + }, + "bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==" + }, + "deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, + "esbuild": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz", + "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==", + "requires": { + "@esbuild/linux-loong64": "0.14.54", + "esbuild-android-64": "0.14.54", + "esbuild-android-arm64": "0.14.54", + "esbuild-darwin-64": "0.14.54", + "esbuild-darwin-arm64": "0.14.54", + "esbuild-freebsd-64": "0.14.54", + "esbuild-freebsd-arm64": "0.14.54", + "esbuild-linux-32": "0.14.54", + "esbuild-linux-64": "0.14.54", + "esbuild-linux-arm": "0.14.54", + "esbuild-linux-arm64": "0.14.54", + "esbuild-linux-mips64le": "0.14.54", + "esbuild-linux-ppc64le": "0.14.54", + "esbuild-linux-riscv64": "0.14.54", + "esbuild-linux-s390x": "0.14.54", + "esbuild-netbsd-64": "0.14.54", + "esbuild-openbsd-64": "0.14.54", + "esbuild-sunos-64": "0.14.54", + "esbuild-windows-32": "0.14.54", + "esbuild-windows-64": "0.14.54", + "esbuild-windows-arm64": "0.14.54" + } + }, + "esbuild-android-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz", + "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==", + "optional": true + }, + "esbuild-android-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz", + "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==", + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz", + "integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==", + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz", + "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==", + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz", + "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==", + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz", + "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==", + "optional": true + }, + "esbuild-linux-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz", + "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==", + "optional": true + }, + "esbuild-linux-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz", + "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==", + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz", + "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==", + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz", + "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==", + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz", + "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==", + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz", + "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==", + "optional": true + }, + "esbuild-linux-riscv64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz", + "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==", + "optional": true + }, + "esbuild-linux-s390x": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz", + "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==", + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz", + "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==", + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz", + "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==", + "optional": true + }, + "esbuild-register": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.3.3.tgz", + "integrity": "sha512-eFHOkutgIMJY5gc8LUp/7c+LLlDqzNi9T6AwCZ2WKKl3HmT+5ef3ZRyPPxDOynInML0fgaC50yszPKfPnjC0NQ==", + "requires": {} + }, + "esbuild-sunos-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz", + "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==", + "optional": true + }, + "esbuild-windows-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz", + "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==", + "optional": true + }, + "esbuild-windows-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz", + "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==", + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz", + "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==", + "optional": true + }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "typescript": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "dev": true + }, + "universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/scripts/release/package.json b/scripts/release/package.json new file mode 100644 index 0000000000..db39b8cb77 --- /dev/null +++ b/scripts/release/package.json @@ -0,0 +1,20 @@ +{ + "name": "release-comments", + "description": "deps needed for running our release comment script", + "private": true, + "scripts": {}, + "dependencies": { + "@octokit/plugin-paginate-rest": "^2.17.0", + "@octokit/plugin-throttling": "^3.6.2", + "@octokit/rest": "^18.12.0", + "esbuild": "^0.14.38", + "esbuild-register": "^3.3.2", + "semver": "^7.3.7" + }, + "devDependencies": { + "@tsconfig/node16": "^1.0.2", + "@types/node": "^17.0.23", + "@types/semver": "^7.3.9", + "typescript": "^4.7.4" + } +} diff --git a/scripts/release/tsconfig.json b/scripts/release/tsconfig.json new file mode 100644 index 0000000000..e9c0bafece --- /dev/null +++ b/scripts/release/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@tsconfig/node16/tsconfig.json" +} diff --git a/scripts/release/utils.ts b/scripts/release/utils.ts new file mode 100644 index 0000000000..ea6a86f7b4 --- /dev/null +++ b/scripts/release/utils.ts @@ -0,0 +1,36 @@ +import { GITHUB_REPOSITORY, PACKAGE_VERSION_TO_FOLLOW } from "./constants"; + +export function checkIfStringStartsWith( + string: string, + substrings: Array +): boolean { + return substrings.some((substr) => string.startsWith(substr)); +} + +export interface MinimalTag { + tag: string; + date: Date; + isPrerelease: boolean; +} + +export function getGitHubUrl(type: "pull" | "issue", number: number) { + let segment = type === "pull" ? "pull" : "issues"; + return `https://github.com/${GITHUB_REPOSITORY}/${segment}/${number}`; +} + +export function cleanupTagName(tagName: string) { + if (PACKAGE_VERSION_TO_FOLLOW) { + let regex = new RegExp(`^${PACKAGE_VERSION_TO_FOLLOW}@`); + return tagName.replace(regex, ""); + } + + return tagName; +} + +export function cleanupRef(ref: string) { + return ref.replace(/^refs\/tags\//, ""); +} + +export function isNightly(tagName: string) { + return tagName.startsWith("v0.0.0-nightly-"); +} diff --git a/tutorial/README.md b/tutorial/README.md index bf447edad5..ad26a9c800 100644 --- a/tutorial/README.md +++ b/tutorial/README.md @@ -1,3 +1,3 @@ # React Router v6 Tutorial -To complete this tutorial, we recommend following along with our guide in our [Getting Started documentation](https://github.com/remix-run/react-router/blob/main/docs/getting-started/tutorial.md). +To complete this tutorial, we recommend following along with our guide in our [Getting Started documentation](https://github.com/remix-run/react-router/blob/main/docs/start/tutorial.md).