diff --git a/.github/workflows/actions/test-react-router-e2e/action.yml b/.github/workflows/actions/test-react-router-e2e/action.yml
index 3af841bf83c..65fa79ab3b5 100644
--- a/.github/workflows/actions/test-react-router-e2e/action.yml
+++ b/.github/workflows/actions/test-react-router-e2e/action.yml
@@ -29,7 +29,11 @@ runs:
shell: bash
working-directory: ./packages/react-router/test
- name: πΈοΈ Install Dependencies
- run: npm install
+ run: npm install --legacy-peer-deps
+ shell: bash
+ working-directory: ./packages/react-router/test/build/${{ inputs.app }}
+ - name: π¦ Install Playwright Browsers
+ run: npx playwright install chromium
shell: bash
working-directory: ./packages/react-router/test/build/${{ inputs.app }}
- name: π Sync Built Changes
@@ -40,7 +44,13 @@ runs:
run: npm run build
shell: bash
working-directory: ./packages/react-router/test/build/${{ inputs.app }}
- - name: π§ͺ Run Tests
+ - name: π§ͺ Run Cypress Tests
run: npm run e2e
shell: bash
working-directory: ./packages/react-router/test/build/${{ inputs.app }}
+ - name: π Run Playwright Tests
+ run: npx playwright test --retries=2
+ env:
+ CI: true
+ shell: bash
+ working-directory: ./packages/react-router/test/build/${{ inputs.app }}
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 7772b2c432f..c8dcb2473ba 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -176,7 +176,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- apps: [reactrouter5]
+ apps: [reactrouter6]
needs: [build-react, build-react-router]
runs-on: ubuntu-latest
steps:
diff --git a/.github/workflows/stencil-nightly.yml b/.github/workflows/stencil-nightly.yml
index 3e8d714ccc1..4a577eb98a4 100644
--- a/.github/workflows/stencil-nightly.yml
+++ b/.github/workflows/stencil-nightly.yml
@@ -186,7 +186,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- apps: [reactrouter5]
+ apps: [reactrouter6]
needs: [build-react, build-react-router]
runs-on: ubuntu-latest
steps:
diff --git a/BREAKING.md b/BREAKING.md
index bf44f563dc8..a05c9675826 100644
--- a/BREAKING.md
+++ b/BREAKING.md
@@ -4,290 +4,196 @@ This is a comprehensive list of the breaking changes introduced in the major ver
## Versions
-- [Version 8.x](#version-8x)
+- [Version 9.x](#version-9x)
+- [Version 8.x](./BREAKING_ARCHIVE/v8.md)
- [Version 7.x](./BREAKING_ARCHIVE/v7.md)
- [Version 6.x](./BREAKING_ARCHIVE/v6.md)
- [Version 5.x](./BREAKING_ARCHIVE/v5.md)
- [Version 4.x](./BREAKING_ARCHIVE/v4.md)
- [Legacy](https://github.com/ionic-team/ionic-v3/blob/master/CHANGELOG.md)
-## Version 8.x
-
-- [Browser and Platform Support](#version-8x-browser-platform-support)
-- [Dark Mode](#version-8x-dark-mode)
-- [Global Styles](#version-8x-global-styles)
-- [Haptics](#version-8x-haptics)
-- [Components](#version-8x-components)
- - [Button](#version-8x-button)
- - [Checkbox](#version-8x-checkbox)
- - [Content](#version-8x-content)
- - [Datetime](#version-8x-datetime)
- - [Input](#version-8x-input)
- - [Item](#version-8x-item)
- - [Modal](#version-8x-modal)
- - [Nav](#version-8x-nav)
- - [Picker](#version-8x-picker)
- - [Progress bar](#version-8x-progress-bar)
- - [Radio](#version-8x-radio)
- - [Range](#version-8x-range)
- - [Searchbar](#version-8x-searchbar)
- - [Select](#version-8x-select)
- - [Textarea](#version-8x-textarea)
- - [Toggle](#version-8x-toggle)
-- [Framework Specific](#version-8x-framework-specific)
- - [Angular](#version-8x-angular)
-
-
-
-This section details the desktop browser, JavaScript framework, and mobile platform versions that are supported by Ionic 8.
-
-**Minimum Browser Versions**
-| Desktop Browser | Supported Versions |
-| --------------- | ----------------- |
-| Chrome | 89+ |
-| Safari | 15+ |
-| Firefox | 75+ |
-| Edge | 89+ |
-
-**Minimum JavaScript Framework Versions**
-| Framework | Supported Version |
-| --------- | --------------------- |
-| Angular | 16+ |
-| React | 17+ |
-| Vue | 3.0.6+ |
-
-**Minimum Mobile Platform Versions**
-| Platform | Supported Version |
-| -------- | ---------------------- |
-| iOS | 15+ |
-| Android | 5.1+ with Chromium 89+ |
-
-Ionic Framework v8 removes backwards support for CSS Animations in favor of the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API). All minimum browser versions listed above support the Web Animations API.
-
-Dark Mode
-
-
-In previous versions, it was recommended to define the dark palette in the following way:
-
-```css
-@media (prefers-color-scheme: dark) {
- body {
- /* global app variables */
- }
-
- .ios body {
- /* global ios app variables */
- }
-
- .md body {
- /* global md app variables */
- }
-}
-```
-
-In Ionic Framework version 8, the dark palette is being distributed via css files that can be imported. Below is an example of importing a dark palette file in Angular:
-
-```css
-/* @import '@ionic/angular/css/palettes/dark.always.css'; */
-/* @import "@ionic/angular/css/palettes/dark.class.css"; */
-@import "@ionic/angular/css/palettes/dark.system.css";
-```
-
-By importing the `dark.system.css` file, the dark palette variables will be defined like the following:
-
-```css
-@media (prefers-color-scheme: dark) {
- :root {
- /* global app variables */
- }
+## Version 9.x
- :root.ios {
- /* global ios app variables */
- }
+- [Framework Specific](#version-9x-framework-specific)
+ - [React](#version-9x-react)
- :root.md {
- /* global md app variables */
- }
-}
-```
+Framework Specific
-Notice that the dark palette is now applied to the `:root` selector instead of the `body` selector. The [`:root`](https://developer.mozilla.org/en-US/docs/Web/CSS/:root) selector represents the `` element and is identical to the selector `html`, except that its specificity is higher.
+React
-While migrating to include the new dark palette files is unlikely to cause breaking changes, these new selectors can lead to unexpected overrides if custom CSS variables are being set on the `body` element. We recommend updating any instances where global application variables are set to target the `:root` selector instead.
+The `@ionic/react-router` package now requires React Router v6. React Router v5 is no longer supported.
-For more information on the new dark palette files, refer to the [Dark Mode documentation](https://ionicframework.com/docs/theming/dark-mode).
+**Minimum Version Requirements**
+| Package | Supported Version |
+| ---------------- | ----------------- |
+| react-router | 6.0.0+ |
+| react-router-dom | 6.0.0+ |
-Global Styles
+React Router v6 introduces several API changes that will require updates to your application's routing configuration:
-Text Color
+**Route Definition Changes**
-The `core.css` file has been updated to set the text color on the `body` element:
+The `component` prop has been replaced with the `element` prop, which accepts JSX:
```diff
-body {
-+ color: var(--ion-text-color);
-}
+-
++ } />
```
-This allows components to inherit the color properly when used outside of Ionic Framework and is required for custom themes to work properly. However, it may have unintentional side effects in apps if the color was not expected to inherit.
-
-Dynamic Font
-
-The `core.css` file has been updated to enable dynamic font scaling by default.
-
-The `--ion-default-dynamic-font` variable has been removed and replaced with `--ion-dynamic-font`.
-
-Developers who had previously chosen dynamic font scaling by activating it in their global stylesheets can revert to the default setting by removing their custom CSS. In doing so, their application will seamlessly continue utilizing dynamic font scaling as it did before. It's essential to note that altering the font-size of the html element should be avoided, as it may disrupt the proper functioning of dynamic font scaling.
-
-Developers who want to disable dynamic font scaling can set `--ion-dynamic-font: initial;` in their global stylesheets. However, this is not recommended because it may introduce accessibility challenges for users who depend on enlarged font sizes.
+**Redirect Changes**
-For more information on the dynamic font, refer to the [Dynamic Font Scaling documentation](https://ionicframework.com/docs/layout/dynamic-font-scaling).
+The `` component has been replaced with ``:
-Haptics
-
-- Support for the Cordova Haptics plugin has been removed. Components that integrate with haptics, such as `ion-picker` and `ion-toggle`, will continue to function but will no longer play haptics in Cordova environments. Developers should migrate to Capacitor to continue to have haptics in these components.
-
-Components
-
-
-
-- Button text now wraps by default. If this behavior is not desired, add the `ion-text-nowrap` class from the [CSS Utilities](https://ionicframework.com/docs/layout/css-utilities).
-
-Checkbox
-
- The `legacy` property and support for the legacy syntax, which involved placing an `ion-checkbox` inside of an `ion-item` with an `ion-label`, have been removed. For more information on migrating from the legacy checkbox syntax, refer to the [Checkbox documentation](https://ionicframework.com/docs/api/checkbox#migrating-from-legacy-checkbox-syntax).
-
-Content
-
-- Content no longer sets the `--background` custom property when the `.outer-content` class is set on the host.
-
-Datetime
-
-- The CSS shadow part for `month-year-button` has been changed to target a `button` element instead of `ion-item`. Developers should verify their UI renders as expected for the month/year toggle button inside of `ion-datetime`.
- - Developers using the CSS variables available on `ion-item` will need to migrate their CSS to use CSS properties. For example:
- ```diff
- ion-datetime::part(month-year-button) {
- - --background: red;
-
- + background: red;
- }
- ```
+```diff
+- import { Redirect } from 'react-router-dom';
++ import { Navigate } from 'react-router-dom';
-
+-
++
+```
-- `size` has been removed from the `ion-input` component. Developers should use CSS to specify the visible width of the input.
-- `accept` has been removed from the `ion-input` component. This was previously used in conjunction with the `type="file"`. However, the `file` value for `type` is not a valid value in Ionic Framework.
-- The `legacy` property and support for the legacy syntax, which involved placing an `ion-input` inside of an `ion-item` with an `ion-label`, have been removed. For more information on migrating from the legacy input syntax, refer to the [Input documentation](https://ionicframework.com/docs/api/input#migrating-from-legacy-input-syntax).
+**Nested Route Paths**
-Item
+Routes that contain nested routes or child `IonRouterOutlet` components need a `/*` suffix to match sub-paths:
-- The `helper` slot has been removed. Developers should use the `helperText` property on `ion-input` and `ion-textarea`.
-- The `error` slot has been removed. Developers should use the `errorText` property on `ion-input` and `ion-textarea`.
-- Counter functionality has been removed including the `counter` and `counterFormatter` properties. Developers should use the properties of the same name on `ion-input` and `ion-textarea`.
-- The `fill` property has been removed. Developers should use the property of the same name on `ion-input`, `ion-select`, and `ion-textarea`.
-- The `shape` property has been removed. Developers should use the property of the same name on `ion-input`, `ion-select`, and `ion-textarea`.
-- Item no longer automatically delegates focus to the first focusable element. While most developers should not need to make any changes to account for this update, usages of `ion-item` with interactive elements such as form controls (inputs, textareas, etc) should be evaluated to verify that interactions still work as expected.
+```diff
+- } />
++ } />
+```
-CSS variables
+**Accessing Route Parameters**
-The following deprecated CSS variables have been removed: `--highlight-height`, `--highlight-color-focused`, `--highlight-color-valid`, and `--highlight-color-invalid`. These variables were used on the bottom border highlight of an item when the form control inside of that item was focused. The form control syntax was [simplified in v7](https://ionic.io/blog/ionic-7-is-here#simplified-form-control-syntax) so that inputs, selects, and textareas would no longer be required to be used inside of an item.
+Route parameters are now accessed via the `useParams` hook instead of props:
-If you have not yet migrated to the modern form control syntax, migration guides for each of the form controls that added a highlight to item can be found below:
-- [Input migration documentation](https://ionicframework.com/docs/api/input#migrating-from-legacy-input-syntax)
-- [Select migration documentation](https://ionicframework.com/docs/api/select#migrating-from-legacy-select-syntax)
-- [Textarea migration documentation](https://ionicframework.com/docs/api/textarea#migrating-from-legacy-textarea-syntax)
+```diff
+- import { RouteComponentProps } from 'react-router-dom';
++ import { useParams } from 'react-router-dom';
-Once all form controls are using the modern syntax, the same variables can be used to customize them from the form control itself:
+- const MyComponent: React.FC> = ({ match }) => {
+- const id = match.params.id;
++ const MyComponent: React.FC = () => {
++ const { id } = useParams<{ id: string }>();
+```
-| Name | Description |
-| ----------------------------| ----------------------------------------|
-| `--highlight-color-focused` | The color of the highlight when focused |
-| `--highlight-color-invalid` | The color of the highlight when invalid |
-| `--highlight-color-valid` | The color of the highlight when valid |
-| `--highlight-height` | The height of the highlight indicator |
+**RouteComponentProps Removed**
-The following styles for item:
+The `RouteComponentProps` type and its `history`, `location`, and `match` props are no longer available in React Router v6. Use the equivalent hooks instead:
-```css
-ion-item {
- --highlight-color-focused: purple;
- --highlight-color-valid: blue;
- --highlight-color-invalid: orange;
- --highlight-height: 6px;
-}
-```
+- `history` -> `useNavigate` (see below) or `useIonRouter`
+- `match.params` -> `useParams` (covered above)
+- `location` -> `useLocation`
-will instead be applied on the form controls:
-
-```css
-ion-input,
-ion-textarea,
-ion-select {
- --highlight-color-focused: purple;
- --highlight-color-valid: blue;
- --highlight-color-invalid: orange;
- --highlight-height: 6px;
-}
+```diff
+- import { RouteComponentProps } from 'react-router-dom';
++ import { useNavigate, useLocation } from 'react-router-dom';
++ import { useIonRouter } from '@ionic/react';
+
+- const MyComponent: React.FC = ({ history, location }) => {
+- history.push('/path');
+- history.replace('/path');
+- history.goBack();
+- console.log(location.pathname);
++ const MyComponent: React.FC = () => {
++ const navigate = useNavigate();
++ const router = useIonRouter();
++ const location = useLocation();
++ // In an event handler or useEffect:
++ navigate('/path');
++ navigate('/path', { replace: true });
++ router.goBack();
++ console.log(location.pathname);
```
-> [!NOTE]
-> The input and textarea components are scoped, which means they will automatically scope their CSS by appending each of the styles with an additional class at runtime. Overriding scoped selectors in CSS requires a [higher specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity) selector. Targeting the `ion-input` or `ion-textarea` for customization will not work; therefore we recommend adding a class and customizing it that way.
+**Exact Prop Removed**
-Modal
+The `exact` prop is no longer needed. React Router v6 routes match exactly by default. To match sub-paths, use a `/*` suffix on the path:
-- Detection for Capacitor <= 2 with applying status bar styles has been removed. Developers should ensure they are using Capacitor 3 or later when using the card modal presentation.
+```diff
+-
++
+```
-Nav
+**Render Prop Removed**
-- `getLength` returns `Promise` instead of ``. This method was not previously available in Nav's TypeScript interface, but developers could still access it by casting Nav as `any`. Developers should ensure they `await` their `getLength` call before accessing the returned value.
+The `render` prop has been replaced with the `element` prop:
-Picker
+```diff
+- } />
++ } />
+```
-- `ion-picker` and `ion-picker-column` have been renamed to `ion-picker-legacy` and `ion-picker-legacy-column`, respectively. This change was made to accommodate the new inline picker component while allowing developers to continue to use the legacy picker during this migration period.
- - Only the component names have been changed. Usages such as `ion-picker` or `IonPicker` should be changed to `ion-picker-legacy` and `IonPickerLegacy`, respectively.
- - Non-component usages such as `pickerController` or `useIonPicker` remain unchanged. The new picker displays inline with your page content and does not have equivalents for these non-component usages.
+**Programmatic Navigation**
-Progress bar
+The `useHistory` hook has been replaced with `useNavigate`:
-- The `--buffer-background` CSS variable has been removed. Use `--background` instead.
+```diff
+- import { useHistory } from 'react-router-dom';
++ import { useNavigate } from 'react-router-dom';
++ import { useIonRouter } from '@ionic/react';
-Toast
+- const history = useHistory();
++ const navigate = useNavigate();
++ const router = useIonRouter();
-- `cssClass` has been removed from the `ToastButton` interface. This was previously used to apply a custom class to the toast buttons. Developers can use the "button" shadow part to style the buttons.
+- history.push('/path');
++ navigate('/path');
-For more information on styling toast buttons, refer to the [Toast Theming documentation](https://ionicframework.com/docs/api/toast#theming).
+- history.replace('/path');
++ navigate('/path', { replace: true });
-Radio
+- history.goBack();
++ router.goBack();
+```
-- The `legacy` property and support for the legacy syntax, which involved placing an `ion-radio` inside of an `ion-item` with an `ion-label`, have been removed. For more information on migrating from the legacy radio syntax, refer to the [Radio documentation](https://ionicframework.com/docs/api/radio#migrating-from-legacy-radio-syntax).
+**Custom History Prop Removed**
-Range
+The `history` prop has been removed from `IonReactRouter`, `IonReactHashRouter`, and `IonReactMemoryRouter`. React Router v6's `BrowserRouter`, `HashRouter`, and `MemoryRouter` no longer accept custom `history` objects.
-- The `legacy` property and support for the legacy syntax, which involved placing an `ion-range` inside of an `ion-item` with an `ion-label`, have been removed. Ionic will also no longer attempt to automatically associate form controls with sibling `` elements as these label elements are now used inside the form control. Developers should provide a label (either visible text or `aria-label`) directly to the form control. For more information on migrating from the legacy range syntax, refer to the [Range documentation](https://ionicframework.com/docs/api/range#migrating-from-legacy-range-syntax).
+```diff
+- import { createBrowserHistory } from 'history';
+- const history = createBrowserHistory();
+-
++
+```
-Searchbar
+For `IonReactMemoryRouter` (commonly used in tests), use `initialEntries` instead:
-- The `autocapitalize` property now defaults to `'off'`.
+```diff
+- import { createMemoryHistory } from 'history';
+- const history = createMemoryHistory({ initialEntries: ['/start'] });
+-
++
+```
-Select
+**IonRedirect Removed**
-- The `legacy` property and support for the legacy syntax, which involved placing an `ion-select` inside of an `ion-item` with an `ion-label`, have been removed. Ionic will also no longer attempt to automatically associate form controls with sibling `` elements as these label elements are now used inside the form control. Developers should provide a label (either visible text or `aria-label`) directly to the form control. For more information on migrating from the legacy select syntax, refer to the [Select documentation](https://ionicframework.com/docs/api/select#migrating-from-legacy-select-syntax).
+The `IonRedirect` component has been removed. Use React Router's `` component instead:
-Textarea
+```diff
+- import { IonRedirect } from '@ionic/react';
+-
++ import { Navigate } from 'react-router-dom';
++ } />
+```
-- The `legacy` property and support for the legacy syntax, which involved placing an `ion-textarea` inside of an `ion-item` with an `ion-label`, have been removed. For more information on migrating from the legacy textarea syntax, refer to the [Textarea documentation](https://ionicframework.com/docs/api/textarea#migrating-from-legacy-textarea-syntax).
+**Path Regex Constraints Removed**
-Toggle
+React Router v6 no longer supports regex constraints in path parameters (e.g., `/:tab(sessions)`). Use literal paths instead:
-- The `legacy` property and support for the legacy syntax, which involved placing an `ion-toggle` inside of an `ion-item` with an `ion-label`, have been removed. For more information on migrating from the legacy toggle syntax, refer to the [Toggle documentation](https://ionicframework.com/docs/api/toggle#migrating-from-legacy-toggle-syntax).
+```diff
+-
+-
++ } />
++ } />
+```
-Framework Specific
+**IonRoute API Changes**
-Angular
+The `IonRoute` component follows the same API changes as React Router's ``. The `render` prop has been replaced with `element`, and the `exact` prop has been removed:
-- The `IonBackButtonDelegate` class has been removed in favor of `IonBackButton`.
+```diff
+- } />
++ } />
+```
- ```diff
- - import { IonBackButtonDelegate } from '@ionic/angular';
- + import { IonBackButton } from '@ionic/angular';
- ```
+For more information on migrating from React Router v5 to v6, refer to the [React Router v6 Upgrade Guide](https://reactrouter.com/6.28.0/upgrading/v5).
diff --git a/BREAKING_ARCHIVE/v8.md b/BREAKING_ARCHIVE/v8.md
new file mode 100644
index 00000000000..d0e6b063158
--- /dev/null
+++ b/BREAKING_ARCHIVE/v8.md
@@ -0,0 +1,282 @@
+# Breaking Changes
+
+## Version 8.x
+
+- [Browser and Platform Support](#version-8x-browser-platform-support)
+- [Dark Mode](#version-8x-dark-mode)
+- [Global Styles](#version-8x-global-styles)
+- [Haptics](#version-8x-haptics)
+- [Components](#version-8x-components)
+ - [Button](#version-8x-button)
+ - [Checkbox](#version-8x-checkbox)
+ - [Content](#version-8x-content)
+ - [Datetime](#version-8x-datetime)
+ - [Input](#version-8x-input)
+ - [Item](#version-8x-item)
+ - [Modal](#version-8x-modal)
+ - [Nav](#version-8x-nav)
+ - [Picker](#version-8x-picker)
+ - [Progress bar](#version-8x-progress-bar)
+ - [Radio](#version-8x-radio)
+ - [Range](#version-8x-range)
+ - [Searchbar](#version-8x-searchbar)
+ - [Select](#version-8x-select)
+ - [Textarea](#version-8x-textarea)
+ - [Toggle](#version-8x-toggle)
+- [Framework Specific](#version-8x-framework-specific)
+ - [Angular](#version-8x-angular)
+
+
+
+This section details the desktop browser, JavaScript framework, and mobile platform versions that are supported by Ionic 8.
+
+**Minimum Browser Versions**
+| Desktop Browser | Supported Versions |
+| --------------- | ----------------- |
+| Chrome | 89+ |
+| Safari | 15+ |
+| Firefox | 75+ |
+| Edge | 89+ |
+
+**Minimum JavaScript Framework Versions**
+| Framework | Supported Version |
+| --------- | --------------------- |
+| Angular | 16+ |
+| React | 17+ |
+| Vue | 3.0.6+ |
+
+**Minimum Mobile Platform Versions**
+| Platform | Supported Version |
+| -------- | ---------------------- |
+| iOS | 15+ |
+| Android | 5.1+ with Chromium 89+ |
+
+Ionic Framework v8 removes backwards support for CSS Animations in favor of the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API). All minimum browser versions listed above support the Web Animations API.
+
+Dark Mode
+
+
+In previous versions, it was recommended to define the dark palette in the following way:
+
+```css
+@media (prefers-color-scheme: dark) {
+ body {
+ /* global app variables */
+ }
+
+ .ios body {
+ /* global ios app variables */
+ }
+
+ .md body {
+ /* global md app variables */
+ }
+}
+```
+
+In Ionic Framework version 8, the dark palette is being distributed via css files that can be imported. Below is an example of importing a dark palette file in Angular:
+
+```css
+/* @import '@ionic/angular/css/palettes/dark.always.css'; */
+/* @import "@ionic/angular/css/palettes/dark.class.css"; */
+@import "@ionic/angular/css/palettes/dark.system.css";
+```
+
+By importing the `dark.system.css` file, the dark palette variables will be defined like the following:
+
+```css
+@media (prefers-color-scheme: dark) {
+ :root {
+ /* global app variables */
+ }
+
+ :root.ios {
+ /* global ios app variables */
+ }
+
+ :root.md {
+ /* global md app variables */
+ }
+}
+```
+
+Notice that the dark palette is now applied to the `:root` selector instead of the `body` selector. The [`:root`](https://developer.mozilla.org/en-US/docs/Web/CSS/:root) selector represents the `` element and is identical to the selector `html`, except that its specificity is higher.
+
+While migrating to include the new dark palette files is unlikely to cause breaking changes, these new selectors can lead to unexpected overrides if custom CSS variables are being set on the `body` element. We recommend updating any instances where global application variables are set to target the `:root` selector instead.
+
+For more information on the new dark palette files, refer to the [Dark Mode documentation](https://ionicframework.com/docs/theming/dark-mode).
+
+Global Styles
+
+Text Color
+
+The `core.css` file has been updated to set the text color on the `body` element:
+
+```diff
+body {
++ color: var(--ion-text-color);
+}
+```
+
+This allows components to inherit the color properly when used outside of Ionic Framework and is required for custom themes to work properly. However, it may have unintentional side effects in apps if the color was not expected to inherit.
+
+Dynamic Font
+
+The `core.css` file has been updated to enable dynamic font scaling by default.
+
+The `--ion-default-dynamic-font` variable has been removed and replaced with `--ion-dynamic-font`.
+
+Developers who had previously chosen dynamic font scaling by activating it in their global stylesheets can revert to the default setting by removing their custom CSS. In doing so, their application will seamlessly continue utilizing dynamic font scaling as it did before. It's essential to note that altering the font-size of the html element should be avoided, as it may disrupt the proper functioning of dynamic font scaling.
+
+Developers who want to disable dynamic font scaling can set `--ion-dynamic-font: initial;` in their global stylesheets. However, this is not recommended because it may introduce accessibility challenges for users who depend on enlarged font sizes.
+
+For more information on the dynamic font, refer to the [Dynamic Font Scaling documentation](https://ionicframework.com/docs/layout/dynamic-font-scaling).
+
+Haptics
+
+- Support for the Cordova Haptics plugin has been removed. Components that integrate with haptics, such as `ion-picker` and `ion-toggle`, will continue to function but will no longer play haptics in Cordova environments. Developers should migrate to Capacitor to continue to have haptics in these components.
+
+Components
+
+
+
+- Button text now wraps by default. If this behavior is not desired, add the `ion-text-nowrap` class from the [CSS Utilities](https://ionicframework.com/docs/layout/css-utilities).
+
+Checkbox
+
+ The `legacy` property and support for the legacy syntax, which involved placing an `ion-checkbox` inside of an `ion-item` with an `ion-label`, have been removed. For more information on migrating from the legacy checkbox syntax, refer to the [Checkbox documentation](https://ionicframework.com/docs/api/checkbox#migrating-from-legacy-checkbox-syntax).
+
+Content
+
+- Content no longer sets the `--background` custom property when the `.outer-content` class is set on the host.
+
+Datetime
+
+- The CSS shadow part for `month-year-button` has been changed to target a `button` element instead of `ion-item`. Developers should verify their UI renders as expected for the month/year toggle button inside of `ion-datetime`.
+ - Developers using the CSS variables available on `ion-item` will need to migrate their CSS to use CSS properties. For example:
+ ```diff
+ ion-datetime::part(month-year-button) {
+ - --background: red;
+
+ + background: red;
+ }
+ ```
+
+
+
+- `size` has been removed from the `ion-input` component. Developers should use CSS to specify the visible width of the input.
+- `accept` has been removed from the `ion-input` component. This was previously used in conjunction with the `type="file"`. However, the `file` value for `type` is not a valid value in Ionic Framework.
+- The `legacy` property and support for the legacy syntax, which involved placing an `ion-input` inside of an `ion-item` with an `ion-label`, have been removed. For more information on migrating from the legacy input syntax, refer to the [Input documentation](https://ionicframework.com/docs/api/input#migrating-from-legacy-input-syntax).
+
+Item
+
+- The `helper` slot has been removed. Developers should use the `helperText` property on `ion-input` and `ion-textarea`.
+- The `error` slot has been removed. Developers should use the `errorText` property on `ion-input` and `ion-textarea`.
+- Counter functionality has been removed including the `counter` and `counterFormatter` properties. Developers should use the properties of the same name on `ion-input` and `ion-textarea`.
+- The `fill` property has been removed. Developers should use the property of the same name on `ion-input`, `ion-select`, and `ion-textarea`.
+- The `shape` property has been removed. Developers should use the property of the same name on `ion-input`, `ion-select`, and `ion-textarea`.
+- Item no longer automatically delegates focus to the first focusable element. While most developers should not need to make any changes to account for this update, usages of `ion-item` with interactive elements such as form controls (inputs, textareas, etc) should be evaluated to verify that interactions still work as expected.
+
+CSS variables
+
+The following deprecated CSS variables have been removed: `--highlight-height`, `--highlight-color-focused`, `--highlight-color-valid`, and `--highlight-color-invalid`. These variables were used on the bottom border highlight of an item when the form control inside of that item was focused. The form control syntax was [simplified in v7](https://ionic.io/blog/ionic-7-is-here#simplified-form-control-syntax) so that inputs, selects, and textareas would no longer be required to be used inside of an item.
+
+If you have not yet migrated to the modern form control syntax, migration guides for each of the form controls that added a highlight to item can be found below:
+- [Input migration documentation](https://ionicframework.com/docs/api/input#migrating-from-legacy-input-syntax)
+- [Select migration documentation](https://ionicframework.com/docs/api/select#migrating-from-legacy-select-syntax)
+- [Textarea migration documentation](https://ionicframework.com/docs/api/textarea#migrating-from-legacy-textarea-syntax)
+
+Once all form controls are using the modern syntax, the same variables can be used to customize them from the form control itself:
+
+| Name | Description |
+| ----------------------------| ----------------------------------------|
+| `--highlight-color-focused` | The color of the highlight when focused |
+| `--highlight-color-invalid` | The color of the highlight when invalid |
+| `--highlight-color-valid` | The color of the highlight when valid |
+| `--highlight-height` | The height of the highlight indicator |
+
+The following styles for item:
+
+```css
+ion-item {
+ --highlight-color-focused: purple;
+ --highlight-color-valid: blue;
+ --highlight-color-invalid: orange;
+ --highlight-height: 6px;
+}
+```
+
+will instead be applied on the form controls:
+
+```css
+ion-input,
+ion-textarea,
+ion-select {
+ --highlight-color-focused: purple;
+ --highlight-color-valid: blue;
+ --highlight-color-invalid: orange;
+ --highlight-height: 6px;
+}
+```
+
+> [!NOTE]
+> The input and textarea components are scoped, which means they will automatically scope their CSS by appending each of the styles with an additional class at runtime. Overriding scoped selectors in CSS requires a [higher specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity) selector. Targeting the `ion-input` or `ion-textarea` for customization will not work; therefore we recommend adding a class and customizing it that way.
+
+Modal
+
+- Detection for Capacitor <= 2 with applying status bar styles has been removed. Developers should ensure they are using Capacitor 3 or later when using the card modal presentation.
+
+Nav
+
+- `getLength` returns `Promise` instead of ``. This method was not previously available in Nav's TypeScript interface, but developers could still access it by casting Nav as `any`. Developers should ensure they `await` their `getLength` call before accessing the returned value.
+
+Picker
+
+- `ion-picker` and `ion-picker-column` have been renamed to `ion-picker-legacy` and `ion-picker-legacy-column`, respectively. This change was made to accommodate the new inline picker component while allowing developers to continue to use the legacy picker during this migration period.
+ - Only the component names have been changed. Usages such as `ion-picker` or `IonPicker` should be changed to `ion-picker-legacy` and `IonPickerLegacy`, respectively.
+ - Non-component usages such as `pickerController` or `useIonPicker` remain unchanged. The new picker displays inline with your page content and does not have equivalents for these non-component usages.
+
+Progress bar
+
+- The `--buffer-background` CSS variable has been removed. Use `--background` instead.
+
+Toast
+
+- `cssClass` has been removed from the `ToastButton` interface. This was previously used to apply a custom class to the toast buttons. Developers can use the "button" shadow part to style the buttons.
+
+For more information on styling toast buttons, refer to the [Toast Theming documentation](https://ionicframework.com/docs/api/toast#theming).
+
+Radio
+
+- The `legacy` property and support for the legacy syntax, which involved placing an `ion-radio` inside of an `ion-item` with an `ion-label`, have been removed. For more information on migrating from the legacy radio syntax, refer to the [Radio documentation](https://ionicframework.com/docs/api/radio#migrating-from-legacy-radio-syntax).
+
+Range
+
+- The `legacy` property and support for the legacy syntax, which involved placing an `ion-range` inside of an `ion-item` with an `ion-label`, have been removed. Ionic will also no longer attempt to automatically associate form controls with sibling `` elements as these label elements are now used inside the form control. Developers should provide a label (either visible text or `aria-label`) directly to the form control. For more information on migrating from the legacy range syntax, refer to the [Range documentation](https://ionicframework.com/docs/api/range#migrating-from-legacy-range-syntax).
+
+Searchbar
+
+- The `autocapitalize` property now defaults to `'off'`.
+
+Select
+
+- The `legacy` property and support for the legacy syntax, which involved placing an `ion-select` inside of an `ion-item` with an `ion-label`, have been removed. Ionic will also no longer attempt to automatically associate form controls with sibling `` elements as these label elements are now used inside the form control. Developers should provide a label (either visible text or `aria-label`) directly to the form control. For more information on migrating from the legacy select syntax, refer to the [Select documentation](https://ionicframework.com/docs/api/select#migrating-from-legacy-select-syntax).
+
+Textarea
+
+- The `legacy` property and support for the legacy syntax, which involved placing an `ion-textarea` inside of an `ion-item` with an `ion-label`, have been removed. For more information on migrating from the legacy textarea syntax, refer to the [Textarea documentation](https://ionicframework.com/docs/api/textarea#migrating-from-legacy-textarea-syntax).
+
+Toggle
+
+- The `legacy` property and support for the legacy syntax, which involved placing an `ion-toggle` inside of an `ion-item` with an `ion-label`, have been removed. For more information on migrating from the legacy toggle syntax, refer to the [Toggle documentation](https://ionicframework.com/docs/api/toggle#migrating-from-legacy-toggle-syntax).
+
+Framework Specific
+
+Angular
+
+- The `IonBackButtonDelegate` class has been removed in favor of `IonBackButton`.
+
+ ```diff
+ - import { IonBackButtonDelegate } from '@ionic/angular';
+ + import { IonBackButton } from '@ionic/angular';
+ ```
diff --git a/core/src/components/action-sheet/action-sheet.tsx b/core/src/components/action-sheet/action-sheet.tsx
index beda9b01ac0..691d9a8caea 100644
--- a/core/src/components/action-sheet/action-sheet.tsx
+++ b/core/src/components/action-sheet/action-sheet.tsx
@@ -6,6 +6,7 @@ import { raf } from '@utils/helpers';
import { createLockController } from '@utils/lock-controller';
import {
BACKDROP,
+ cleanupRootFocusTrapAccessibility,
createDelegateController,
createTriggerController,
dismiss,
@@ -460,6 +461,11 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
this.gesture = undefined;
}
this.triggerController.removeClickListener();
+
+ // Clean up aria-hidden if removed without dismiss() being called
+ if (this.presented) {
+ cleanupRootFocusTrapAccessibility();
+ }
}
componentWillLoad() {
diff --git a/core/src/components/alert/alert.tsx b/core/src/components/alert/alert.tsx
index a29f4e0fe13..d5e40666c0a 100644
--- a/core/src/components/alert/alert.tsx
+++ b/core/src/components/alert/alert.tsx
@@ -7,9 +7,10 @@ import { raf } from '@utils/helpers';
import { createLockController } from '@utils/lock-controller';
import { printIonWarning } from '@utils/logging';
import {
+ BACKDROP,
+ cleanupRootFocusTrapAccessibility,
createDelegateController,
createTriggerController,
- BACKDROP,
dismiss,
eventMethod,
isCancel,
@@ -368,6 +369,11 @@ export class Alert implements ComponentInterface, OverlayInterface {
this.gesture.destroy();
this.gesture = undefined;
}
+
+ // Clean up aria-hidden if removed without dismiss() being called
+ if (this.presented) {
+ cleanupRootFocusTrapAccessibility();
+ }
}
componentDidLoad() {
diff --git a/core/src/components/loading/loading.tsx b/core/src/components/loading/loading.tsx
index 1666c869639..80c774fa090 100644
--- a/core/src/components/loading/loading.tsx
+++ b/core/src/components/loading/loading.tsx
@@ -5,12 +5,13 @@ import { raf } from '@utils/helpers';
import { createLockController } from '@utils/lock-controller';
import {
BACKDROP,
+ cleanupRootFocusTrapAccessibility,
+ createDelegateController,
+ createTriggerController,
dismiss,
eventMethod,
prepareOverlay,
present,
- createDelegateController,
- createTriggerController,
setOverlayId,
} from '@utils/overlays';
import { sanitizeDOMString } from '@utils/sanitization';
@@ -242,6 +243,11 @@ export class Loading implements ComponentInterface, OverlayInterface {
disconnectedCallback() {
this.triggerController.removeClickListener();
+
+ // Clean up aria-hidden if removed without dismiss() being called
+ if (this.presented) {
+ cleanupRootFocusTrapAccessibility();
+ }
}
/**
diff --git a/core/src/components/modal/modal.tsx b/core/src/components/modal/modal.tsx
index 9ba3ecbf009..15865b1fb8a 100644
--- a/core/src/components/modal/modal.tsx
+++ b/core/src/components/modal/modal.tsx
@@ -8,15 +8,16 @@ import { createLockController } from '@utils/lock-controller';
import { printIonWarning } from '@utils/logging';
import { Style as StatusBarStyle, StatusBar } from '@utils/native/status-bar';
import {
- GESTURE,
BACKDROP,
+ cleanupRootFocusTrapAccessibility,
+ createTriggerController,
dismiss,
eventMethod,
+ FOCUS_TRAP_DISABLE_CLASS,
+ GESTURE,
prepareOverlay,
present,
- createTriggerController,
setOverlayId,
- FOCUS_TRAP_DISABLE_CLASS,
} from '@utils/overlays';
import { getClassMap } from '@utils/theme';
import { deepReady, waitForMount } from '@utils/transition';
@@ -458,6 +459,11 @@ export class Modal implements ComponentInterface, OverlayInterface {
// Also called in dismiss() β intentional dual cleanup covers both
// dismiss-then-remove and direct DOM removal without dismiss.
this.cleanupSafeAreaOverrides();
+
+ // Clean up aria-hidden if removed without dismiss() being called
+ if (this.presented) {
+ cleanupRootFocusTrapAccessibility();
+ }
}
componentWillLoad() {
diff --git a/core/src/components/picker-legacy/picker.tsx b/core/src/components/picker-legacy/picker.tsx
index fe67cf83fec..aa503fe9b21 100644
--- a/core/src/components/picker-legacy/picker.tsx
+++ b/core/src/components/picker-legacy/picker.tsx
@@ -4,9 +4,10 @@ import { raf } from '@utils/helpers';
import { createLockController } from '@utils/lock-controller';
import { printIonWarning } from '@utils/logging';
import {
+ BACKDROP,
+ cleanupRootFocusTrapAccessibility,
createDelegateController,
createTriggerController,
- BACKDROP,
dismiss,
eventMethod,
isCancel,
@@ -196,6 +197,11 @@ export class Picker implements ComponentInterface, OverlayInterface {
disconnectedCallback() {
this.triggerController.removeClickListener();
+
+ // Clean up aria-hidden if removed without dismiss() being called
+ if (this.presented) {
+ cleanupRootFocusTrapAccessibility();
+ }
}
componentWillLoad() {
diff --git a/core/src/components/popover/popover.tsx b/core/src/components/popover/popover.tsx
index af0e613297d..e106737a453 100644
--- a/core/src/components/popover/popover.tsx
+++ b/core/src/components/popover/popover.tsx
@@ -7,12 +7,13 @@ import { createLockController } from '@utils/lock-controller';
import { printIonWarning } from '@utils/logging';
import {
BACKDROP,
+ cleanupRootFocusTrapAccessibility,
dismiss,
eventMethod,
+ FOCUS_TRAP_DISABLE_CLASS,
prepareOverlay,
present,
setOverlayId,
- FOCUS_TRAP_DISABLE_CLASS,
} from '@utils/overlays';
import { isPlatform } from '@utils/platform';
import { getClassMap } from '@utils/theme';
@@ -367,6 +368,11 @@ export class Popover implements ComponentInterface, PopoverInterface {
this.headerResizeObserver.disconnect();
this.headerResizeObserver = undefined;
}
+
+ // Clean up aria-hidden if removed without dismiss() being called
+ if (this.presented) {
+ cleanupRootFocusTrapAccessibility();
+ }
}
componentWillLoad() {
diff --git a/core/src/utils/overlays.ts b/core/src/utils/overlays.ts
index 59b341a7d52..d5a8ad68910 100644
--- a/core/src/utils/overlays.ts
+++ b/core/src/utils/overlays.ts
@@ -515,6 +515,32 @@ export const setRootAriaHidden = (hidden = false) => {
}
};
+/**
+ * Cleans up root `aria-hidden` and `backdrop-no-scroll` when
+ * an overlay is removed from the DOM without going through
+ * the `dismiss()` flow (e.g., when a framework unmounts the
+ * overlay during a route change).
+ *
+ * Should be called from an overlay's `disconnectedCallback`
+ * when the overlay was still presented at the time of removal.
+ */
+export const cleanupRootFocusTrapAccessibility = () => {
+ if (typeof document === 'undefined') {
+ return;
+ }
+
+ const remainingOverlays = getPresentedOverlays(document);
+ const hasRemainingLocking = remainingOverlays.some((o) => {
+ const el = o as OverlayWithFocusTrapProps;
+ return el.tagName !== 'ION-TOAST' && el.focusTrap !== false && isBackdropAlwaysBlocking(el);
+ });
+
+ if (!hasRemainingLocking) {
+ setRootAriaHidden(false);
+ document.body.classList.remove(BACKDROP_NO_SCROLL);
+ }
+};
+
export const present = async (
overlay: OverlayInterface,
name: keyof IonicConfig,
diff --git a/package-lock.json b/package-lock.json
index d071e527ba9..54ec41085ba 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9288,8 +9288,7 @@
"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==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"@octokit/plugin-rest-endpoint-methods": {
"version": "6.6.2",
diff --git a/packages/react-router/package-lock.json b/packages/react-router/package-lock.json
index f169ffbea99..f21349ad880 100644
--- a/packages/react-router/package-lock.json
+++ b/packages/react-router/package-lock.json
@@ -1,7 +1,7 @@
{
"name": "@ionic/react-router",
"version": "8.8.0",
- "lockfileVersion": 2,
+ "lockfileVersion": 3,
"requires": true,
"packages": {
"": {
@@ -19,16 +19,15 @@
"@types/node": "^14.0.14",
"@types/react": "^17.0.79",
"@types/react-dom": "^17.0.25",
- "@types/react-router": "^5.0.3",
- "@types/react-router-dom": "^5.1.5",
"@typescript-eslint/eslint-plugin": "^5.48.2",
"@typescript-eslint/parser": "^5.48.2",
"eslint": "^7.32.0",
+ "history": "^5.3.0",
"prettier": "^2.8.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
- "react-router": "^5.0.1",
- "react-router-dom": "^5.0.1",
+ "react-router": "^6.30.0",
+ "react-router-dom": "^6.30.0",
"rimraf": "^3.0.2",
"rollup": "^4.2.0",
"typescript": "^4.0.5"
@@ -36,17 +35,8 @@
"peerDependencies": {
"react": ">=16.8.6",
"react-dom": ">=16.8.6",
- "react-router": "^5.0.1",
- "react-router-dom": "^5.0.1"
- }
- },
- "node_modules/@aashutoshrathi/word-wrap": {
- "version": "1.2.6",
- "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
- "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
+ "react-router": ">=6.0.0",
+ "react-router-dom": ">=6.0.0"
}
},
"node_modules/@babel/code-frame": {
@@ -54,28 +44,32 @@
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz",
"integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@babel/highlight": "^7.10.4"
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "7.22.20",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
- "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight": {
- "version": "7.22.20",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
- "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz",
+ "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@babel/helper-validator-identifier": "^7.22.20",
+ "@babel/helper-validator-identifier": "^7.25.9",
"chalk": "^2.4.2",
- "js-tokens": "^4.0.0"
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.0.0"
},
"engines": {
"node": ">=6.9.0"
@@ -86,6 +80,7 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"color-convert": "^1.9.0"
},
@@ -98,6 +93,7 @@
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
@@ -112,6 +108,7 @@
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"color-name": "1.1.3"
}
@@ -120,13 +117,15 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/@babel/highlight/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": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=0.8.0"
}
@@ -136,6 +135,7 @@
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=4"
}
@@ -145,6 +145,7 @@
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"has-flag": "^3.0.0"
},
@@ -153,37 +154,40 @@
}
},
"node_modules/@babel/runtime": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz",
- "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
+ "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"dev": true,
- "dependencies": {
- "regenerator-runtime": "^0.14.0"
- },
+ "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@eslint-community/eslint-utils": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
- "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "version": "4.9.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
+ "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "eslint-visitor-keys": "^3.3.0"
+ "eslint-visitor-keys": "^3.4.3"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
"peerDependencies": {
"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
}
},
"node_modules/@eslint-community/regexpp": {
- "version": "4.10.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
- "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
}
@@ -193,6 +197,7 @@
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz",
"integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.1.1",
@@ -213,6 +218,7 @@
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 4"
}
@@ -221,7 +227,9 @@
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
"integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==",
+ "deprecated": "Use @eslint/config-array instead",
"dev": true,
+ "license": "Apache-2.0",
"dependencies": {
"@humanwhocodes/object-schema": "^1.2.0",
"debug": "^4.1.1",
@@ -235,7 +243,9 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
- "dev": true
+ "deprecated": "Use @eslint/object-schema instead",
+ "dev": true,
+ "license": "BSD-3-Clause"
},
"node_modules/@ionic/core": {
"version": "8.8.0",
@@ -256,6 +266,7 @@
"resolved": "https://registry.npmjs.org/@ionic/eslint-config/-/eslint-config-0.3.0.tgz",
"integrity": "sha512-Uf1hS2YIoHlcvXPF5LnsPM6auMewEdChQhR117Rt3sVEAutbyKMpFP4slNC2a6up3a5Q34zepqlf61Qgkf9XeQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "^4.1.0",
"@typescript-eslint/parser": "^4.1.0",
@@ -271,6 +282,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz",
"integrity": "sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@typescript-eslint/experimental-utils": "4.33.0",
"@typescript-eslint/scope-manager": "4.33.0",
@@ -303,6 +315,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz",
"integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "4.33.0",
"@typescript-eslint/types": "4.33.0",
@@ -330,6 +343,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz",
"integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@typescript-eslint/types": "4.33.0",
"@typescript-eslint/visitor-keys": "4.33.0"
@@ -347,6 +361,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz",
"integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": "^8.10.0 || ^10.13.0 || >=11.10.1"
},
@@ -360,6 +375,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz",
"integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "4.33.0",
"@typescript-eslint/visitor-keys": "4.33.0",
@@ -387,6 +403,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz",
"integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@typescript-eslint/types": "4.33.0",
"eslint-visitor-keys": "^2.0.0"
@@ -404,6 +421,7 @@
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
"dev": true,
+ "license": "Apache-2.0",
"engines": {
"node": ">=10"
}
@@ -413,6 +431,7 @@
"resolved": "https://registry.npmjs.org/@ionic/prettier-config/-/prettier-config-2.1.2.tgz",
"integrity": "sha512-lpjXnu5XmzxDrHinjGa9z/bNe7KgXaehk6NyasyXqwzvE9EyhOSdSrkw6wS2q0HRyw8+x1GZNs2JDJ5cYq39Jw==",
"dev": true,
+ "license": "MIT",
"peerDependencies": {
"prettier": "^2.4.0"
}
@@ -437,6 +456,7 @@
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
"run-parallel": "^1.1.9"
@@ -450,6 +470,7 @@
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 8"
}
@@ -459,6 +480,7 @@
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
"fastq": "^1.6.0"
@@ -467,13 +489,24 @@
"node": ">= 8"
}
},
+ "node_modules/@remix-run/router": {
+ "version": "1.23.1",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz",
+ "integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/@rollup/plugin-typescript": {
- "version": "11.1.5",
- "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.5.tgz",
- "integrity": "sha512-rnMHrGBB0IUEv69Q8/JGRD/n4/n6b3nfpufUu26axhUcboUzv/twfZU8fIBbTOphRAe0v8EyxzeDpKXqGHfyDA==",
+ "version": "11.1.6",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.6.tgz",
+ "integrity": "sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@rollup/pluginutils": "^5.0.1",
+ "@rollup/pluginutils": "^5.1.0",
"resolve": "^1.22.1"
},
"engines": {
@@ -494,14 +527,15 @@
}
},
"node_modules/@rollup/pluginutils": {
- "version": "5.0.5",
- "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.5.tgz",
- "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==",
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
+ "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
- "picomatch": "^2.3.1"
+ "picomatch": "^4.0.2"
},
"engines": {
"node": ">=14.0.0"
@@ -516,237 +550,212 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.3.0.tgz",
- "integrity": "sha512-/4pns6BYi8MXdwnXM44yoGAcFYVHL/BYlB2q1HXZ6AzH++LaiEVWFpBWQ/glXhbMbv3E3o09igrHFbP/snhAvA==",
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz",
+ "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==",
"cpu": [
"arm"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.3.0.tgz",
- "integrity": "sha512-nLO/JsL9idr416vzi3lHm3Xm+QZh4qHij8k3Er13kZr5YhL7/+kBAx84kDmPc7HMexLmwisjDCeDIKNFp8mDlQ==",
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz",
+ "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.3.0.tgz",
- "integrity": "sha512-dGhVBlllt4iHwTGy21IEoMOTN5wZoid19zEIxsdY29xcEiOEHqzDa7Sqrkh5OE7LKCowL61eFJXxYe/+pYa7ZQ==",
+ "version": "4.34.9",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz",
+ "integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==",
"cpu": [
"arm64"
],
- "dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.3.0.tgz",
- "integrity": "sha512-h8wRfHeLEbU3NzaP1Oku7BYXCJQiTRr+8U0lklyOQXxXiEpHLL8tk1hFl+tezoRKLcPJD7joKaK74ASsqt3Ekg==",
+ "version": "4.34.9",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz",
+ "integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==",
"cpu": [
"x64"
],
- "dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
- "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.3.0.tgz",
- "integrity": "sha512-wP4VgR/gfV18sylTuym3sxRTkAgUR2vh6YLeX/GEznk5jCYcYSlx585XlcUcl0c8UffIZlRJ09raWSX3JDb4GA==",
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz",
+ "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==",
"cpu": [
- "arm"
+ "arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
- "linux"
+ "freebsd"
]
},
- "node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.3.0.tgz",
- "integrity": "sha512-v/14JCYVkqRSJeQbxFx4oUkwVQQw6lFMN7bd4vuARBc3X2lmomkxBsc+BFiIDL/BK+CTx5AOh/k9XmqDnKWRVg==",
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz",
+ "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==",
"cpu": [
- "arm64"
+ "x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
- "linux"
+ "freebsd"
]
},
- "node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.3.0.tgz",
- "integrity": "sha512-tNhfYqFH5OxtRzfkTOKdgFYlPSZnlDLNW4+leNEvQZhwTJxoTwsZAAhR97l3qVry/kkLyJPBK+Q8EAJLPinDIg==",
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz",
+ "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==",
"cpu": [
- "arm64"
+ "arm"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
- "node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.3.0.tgz",
- "integrity": "sha512-pw77m8QywdsoFdFOgmc8roF1inBI0rciqzO8ffRUgLoq7+ee9o5eFqtEcS6hHOOplgifAUUisP8cAnwl9nUYPw==",
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz",
+ "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==",
"cpu": [
- "x64"
+ "arm"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
- "node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.3.0.tgz",
- "integrity": "sha512-tJs7v2MnV2F8w6X1UpPHl/43OfxjUy9SuJ2ZPoxn79v9vYteChVYO/ueLHCpRMmyTUIVML3N9z4azl9ENH8Xxg==",
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.34.9",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz",
+ "integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==",
"cpu": [
- "x64"
+ "arm64"
],
- "dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
- "node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.3.0.tgz",
- "integrity": "sha512-OKGxp6kATQdTyI2DF+e9s+hB3/QZB45b6e+dzcfW1SUqiF6CviWyevhmT4USsMEdP3mlpC9zxLz3Oh+WaTMOSw==",
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.34.9",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz",
+ "integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==",
"cpu": [
"arm64"
],
- "dev": true,
+ "license": "MIT",
"optional": true,
"os": [
- "win32"
+ "linux"
]
},
- "node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.3.0.tgz",
- "integrity": "sha512-DDZ5AH68JJ2ClQFEA1aNnfA7Ybqyeh0644rGbrLOdNehTmzfICHiWSn0OprzYi9HAshTPQvlwrM+bi2kuaIOjQ==",
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz",
+ "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==",
"cpu": [
- "ia32"
+ "loong64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
- "win32"
+ "linux"
]
},
- "node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.3.0.tgz",
- "integrity": "sha512-dMvGV8p92GQ8jhNlGIKpyhVZPzJlT258pPrM5q2F8lKcc9Iv9BbfdnhX1OfinYWnb9ms5zLw6MlaMnqLfUkKnQ==",
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz",
+ "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==",
"cpu": [
- "x64"
+ "ppc64"
],
"dev": true,
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@stencil/core": {
- "version": "4.43.0",
- "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.43.0.tgz",
- "integrity": "sha512-6Uj2Z3lzLuufYAE7asZ6NLKgSwsB9uxl84Eh34PASnUjfj32GkrP4DtKK7fNeh1WFGGyffsTDka3gwtl+4reUg==",
- "license": "MIT",
- "bin": {
- "stencil": "bin/stencil"
- },
- "engines": {
- "node": ">=16.0.0",
- "npm": ">=7.10.0"
- },
- "optionalDependencies": {
- "@rollup/rollup-darwin-arm64": "4.34.9",
- "@rollup/rollup-darwin-x64": "4.34.9",
- "@rollup/rollup-linux-arm64-gnu": "4.34.9",
- "@rollup/rollup-linux-arm64-musl": "4.34.9",
- "@rollup/rollup-linux-x64-gnu": "4.34.9",
- "@rollup/rollup-linux-x64-musl": "4.34.9",
- "@rollup/rollup-win32-arm64-msvc": "4.34.9",
- "@rollup/rollup-win32-x64-msvc": "4.34.9"
- }
- },
- "node_modules/@stencil/core/node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.34.9",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz",
- "integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==",
- "cpu": [
- "arm64"
- ],
"license": "MIT",
"optional": true,
"os": [
- "darwin"
+ "linux"
]
},
- "node_modules/@stencil/core/node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.34.9",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz",
- "integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==",
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz",
+ "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==",
"cpu": [
- "x64"
+ "riscv64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
- "darwin"
+ "linux"
]
},
- "node_modules/@stencil/core/node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.34.9",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz",
- "integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==",
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz",
+ "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==",
"cpu": [
- "arm64"
+ "riscv64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
- "node_modules/@stencil/core/node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.34.9",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz",
- "integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==",
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz",
+ "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==",
"cpu": [
- "arm64"
+ "s390x"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
- "node_modules/@stencil/core/node_modules/@rollup/rollup-linux-x64-gnu": {
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz",
"integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==",
@@ -759,7 +768,7 @@
"linux"
]
},
- "node_modules/@stencil/core/node_modules/@rollup/rollup-linux-x64-musl": {
+ "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz",
"integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==",
@@ -772,7 +781,21 @@
"linux"
]
},
- "node_modules/@stencil/core/node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz",
+ "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz",
"integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==",
@@ -785,7 +808,35 @@
"win32"
]
},
- "node_modules/@stencil/core/node_modules/@rollup/rollup-win32-x64-msvc": {
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz",
+ "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz",
+ "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz",
"integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==",
@@ -798,100 +849,113 @@
"win32"
]
},
- "node_modules/@types/estree": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.4.tgz",
- "integrity": "sha512-2JwWnHK9H+wUZNorf2Zr6ves96WHoWDJIftkcxPKsS7Djta6Zu519LarhRNljPXkpsZR2ZMwNCPeW7omW07BJw==",
- "dev": true
+ "node_modules/@rtsao/scc": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
+ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@stencil/core": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.43.0.tgz",
+ "integrity": "sha512-6Uj2Z3lzLuufYAE7asZ6NLKgSwsB9uxl84Eh34PASnUjfj32GkrP4DtKK7fNeh1WFGGyffsTDka3gwtl+4reUg==",
+ "license": "MIT",
+ "bin": {
+ "stencil": "bin/stencil"
+ },
+ "engines": {
+ "node": ">=16.0.0",
+ "npm": ">=7.10.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-darwin-arm64": "4.34.9",
+ "@rollup/rollup-darwin-x64": "4.34.9",
+ "@rollup/rollup-linux-arm64-gnu": "4.34.9",
+ "@rollup/rollup-linux-arm64-musl": "4.34.9",
+ "@rollup/rollup-linux-x64-gnu": "4.34.9",
+ "@rollup/rollup-linux-x64-musl": "4.34.9",
+ "@rollup/rollup-win32-arm64-msvc": "4.34.9",
+ "@rollup/rollup-win32-x64-msvc": "4.34.9"
+ }
},
- "node_modules/@types/history": {
- "version": "4.7.11",
- "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
- "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==",
- "dev": true
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/@types/json-schema": {
- "version": "7.0.14",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz",
- "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==",
- "dev": true
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/@types/node": {
"version": "14.18.63",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz",
"integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/@types/prop-types": {
- "version": "15.7.9",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz",
- "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==",
- "dev": true
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/@types/react": {
- "version": "17.0.79",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.79.tgz",
- "integrity": "sha512-gavKA8AwJAML9zWHuiQRASjrrPJHbT/zrUDHiUGUf+l5a3pkEd6atvjjq+8y2vfRHBJLQJjFpxSa9I8qe9zHAw==",
+ "version": "17.0.90",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.90.tgz",
+ "integrity": "sha512-P9beVR/x06U9rCJzSxtENnOr4BrbJ6VrsrDTc+73TtHv9XHhryXKbjGRB+6oooB2r0G/pQkD/S4dHo/7jUfwFw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@types/prop-types": "*",
- "@types/scheduler": "*",
- "csstype": "^3.0.2"
+ "@types/scheduler": "^0.16",
+ "csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
- "version": "17.0.25",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.25.tgz",
- "integrity": "sha512-urx7A7UxkZQmThYA4So0NelOVjx3V4rNFVJwp0WZlbIK5eM4rNJDiN3R/E9ix0MBh6kAEojk/9YL+Te6D9zHNA==",
- "dev": true,
- "dependencies": {
- "@types/react": "^17"
- }
- },
- "node_modules/@types/react-router": {
- "version": "5.1.20",
- "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
- "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==",
- "dev": true,
- "dependencies": {
- "@types/history": "^4.7.11",
- "@types/react": "*"
- }
- },
- "node_modules/@types/react-router-dom": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz",
- "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==",
+ "version": "17.0.26",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.26.tgz",
+ "integrity": "sha512-Z+2VcYXJwOqQ79HreLU/1fyQ88eXSSFh6I3JdrEHQIfYSI0kCQpTGvOrbE6jFGGYXKsHuwY9tBa/w5Uo6KzrEg==",
"dev": true,
- "dependencies": {
- "@types/history": "^4.7.11",
- "@types/react": "*",
- "@types/react-router": "*"
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^17.0.0"
}
},
"node_modules/@types/scheduler": {
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/@types/semver": {
- "version": "7.5.4",
- "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz",
- "integrity": "sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==",
- "dev": true
+ "version": "7.7.1",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz",
+ "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
"integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.4.0",
"@typescript-eslint/scope-manager": "5.62.0",
@@ -926,6 +990,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz",
"integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.7",
"@typescript-eslint/scope-manager": "4.33.0",
@@ -950,6 +1015,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz",
"integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@typescript-eslint/types": "4.33.0",
"@typescript-eslint/visitor-keys": "4.33.0"
@@ -967,6 +1033,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz",
"integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": "^8.10.0 || ^10.13.0 || >=11.10.1"
},
@@ -980,6 +1047,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz",
"integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "4.33.0",
"@typescript-eslint/visitor-keys": "4.33.0",
@@ -1007,6 +1075,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz",
"integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@typescript-eslint/types": "4.33.0",
"eslint-visitor-keys": "^2.0.0"
@@ -1024,6 +1093,7 @@
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
"integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"eslint-visitor-keys": "^2.0.0"
},
@@ -1042,6 +1112,7 @@
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
"dev": true,
+ "license": "Apache-2.0",
"engines": {
"node": ">=10"
}
@@ -1051,6 +1122,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "5.62.0",
"@typescript-eslint/types": "5.62.0",
@@ -1078,6 +1150,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
"integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@typescript-eslint/types": "5.62.0",
"@typescript-eslint/visitor-keys": "5.62.0"
@@ -1095,6 +1168,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
"integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "5.62.0",
"@typescript-eslint/utils": "5.62.0",
@@ -1122,6 +1196,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
"integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
@@ -1135,6 +1210,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz",
"integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "5.62.0",
"@typescript-eslint/visitor-keys": "5.62.0",
@@ -1162,6 +1238,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz",
"integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@types/json-schema": "^7.0.9",
@@ -1188,6 +1265,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz",
"integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@typescript-eslint/types": "5.62.0",
"eslint-visitor-keys": "^3.3.0"
@@ -1205,6 +1283,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"dev": true,
+ "license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
@@ -1217,6 +1296,7 @@
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true,
+ "license": "MIT",
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
@@ -1226,6 +1306,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -1242,6 +1323,7 @@
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=6"
}
@@ -1251,6 +1333,7 @@
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
}
@@ -1260,6 +1343,7 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -1275,34 +1359,43 @@
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/array-buffer-byte-length": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz",
- "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+ "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "is-array-buffer": "^3.0.1"
+ "call-bound": "^1.0.3",
+ "is-array-buffer": "^3.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/array-includes": {
- "version": "3.1.7",
- "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz",
- "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==",
+ "version": "3.1.9",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
+ "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1",
- "get-intrinsic": "^1.2.1",
- "is-string": "^1.0.7"
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.0",
+ "es-object-atoms": "^1.1.1",
+ "get-intrinsic": "^1.3.0",
+ "is-string": "^1.1.1",
+ "math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
@@ -1316,21 +1409,25 @@
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/array.prototype.findlastindex": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz",
- "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==",
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz",
+ "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1",
- "es-shim-unscopables": "^1.0.0",
- "get-intrinsic": "^1.2.1"
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-shim-unscopables": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
@@ -1340,15 +1437,16 @@
}
},
"node_modules/array.prototype.flat": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz",
- "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==",
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
+ "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1",
- "es-shim-unscopables": "^1.0.0"
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -1358,15 +1456,16 @@
}
},
"node_modules/array.prototype.flatmap": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz",
- "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==",
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
+ "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1",
- "es-shim-unscopables": "^1.0.0"
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -1376,18 +1475,19 @@
}
},
"node_modules/arraybuffer.prototype.slice": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz",
- "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==",
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
+ "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "array-buffer-byte-length": "^1.0.0",
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1",
- "get-intrinsic": "^1.2.1",
- "is-array-buffer": "^3.0.2",
- "is-shared-array-buffer": "^1.0.2"
+ "array-buffer-byte-length": "^1.0.1",
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "is-array-buffer": "^3.0.4"
},
"engines": {
"node": ">= 0.4"
@@ -1401,15 +1501,30 @@
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
}
},
+ "node_modules/async-function": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
+ "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/available-typed-arrays": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
- "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
"dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
"engines": {
"node": ">= 0.4"
},
@@ -1421,39 +1536,78 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/braces": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
- "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "fill-range": "^7.0.1"
+ "fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/call-bind": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
- "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+ "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "function-bind": "^1.1.2",
- "get-intrinsic": "^1.2.1",
- "set-function-length": "^1.1.1"
+ "call-bind-apply-helpers": "^1.0.0",
+ "es-define-property": "^1.0.0",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -1464,6 +1618,7 @@
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=6"
}
@@ -1473,6 +1628,7 @@
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -1489,6 +1645,7 @@
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
@@ -1500,19 +1657,22 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/cross-spawn": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
- "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -1523,18 +1683,74 @@
}
},
"node_modules/csstype": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
- "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
- "dev": true
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/data-view-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
+ "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/data-view-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
+ "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/inspect-js"
+ }
+ },
+ "node_modules/data-view-byte-offset": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
+ "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
},
"node_modules/debug": {
- "version": "4.3.4",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
- "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "ms": "2.1.2"
+ "ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
@@ -1549,20 +1765,25 @@
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/define-data-property": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
- "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "get-intrinsic": "^1.2.1",
- "gopd": "^1.0.1",
- "has-property-descriptors": "^1.0.0"
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/define-properties": {
@@ -1570,6 +1791,7 @@
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"define-data-property": "^1.0.1",
"has-property-descriptors": "^1.0.0",
@@ -1587,6 +1809,7 @@
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"path-type": "^4.0.0"
},
@@ -1599,6 +1822,7 @@
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
"dev": true,
+ "license": "Apache-2.0",
"dependencies": {
"esutils": "^2.0.2"
},
@@ -1606,17 +1830,34 @@
"node": ">=6.0.0"
}
},
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/enquirer": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
"integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"ansi-colors": "^4.1.1",
"strip-ansi": "^6.0.1"
@@ -1626,50 +1867,66 @@
}
},
"node_modules/es-abstract": {
- "version": "1.22.3",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz",
- "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==",
+ "version": "1.24.0",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
+ "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "array-buffer-byte-length": "^1.0.0",
- "arraybuffer.prototype.slice": "^1.0.2",
- "available-typed-arrays": "^1.0.5",
- "call-bind": "^1.0.5",
- "es-set-tostringtag": "^2.0.1",
- "es-to-primitive": "^1.2.1",
- "function.prototype.name": "^1.1.6",
- "get-intrinsic": "^1.2.2",
- "get-symbol-description": "^1.0.0",
- "globalthis": "^1.0.3",
- "gopd": "^1.0.1",
- "has-property-descriptors": "^1.0.0",
- "has-proto": "^1.0.1",
- "has-symbols": "^1.0.3",
- "hasown": "^2.0.0",
- "internal-slot": "^1.0.5",
- "is-array-buffer": "^3.0.2",
+ "array-buffer-byte-length": "^1.0.2",
+ "arraybuffer.prototype.slice": "^1.0.4",
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "data-view-buffer": "^1.0.2",
+ "data-view-byte-length": "^1.0.2",
+ "data-view-byte-offset": "^1.0.1",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-set-tostringtag": "^2.1.0",
+ "es-to-primitive": "^1.3.0",
+ "function.prototype.name": "^1.1.8",
+ "get-intrinsic": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "get-symbol-description": "^1.1.0",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "internal-slot": "^1.1.0",
+ "is-array-buffer": "^3.0.5",
"is-callable": "^1.2.7",
- "is-negative-zero": "^2.0.2",
- "is-regex": "^1.1.4",
- "is-shared-array-buffer": "^1.0.2",
- "is-string": "^1.0.7",
- "is-typed-array": "^1.1.12",
- "is-weakref": "^1.0.2",
- "object-inspect": "^1.13.1",
+ "is-data-view": "^1.0.2",
+ "is-negative-zero": "^2.0.3",
+ "is-regex": "^1.2.1",
+ "is-set": "^2.0.3",
+ "is-shared-array-buffer": "^1.0.4",
+ "is-string": "^1.1.1",
+ "is-typed-array": "^1.1.15",
+ "is-weakref": "^1.1.1",
+ "math-intrinsics": "^1.1.0",
+ "object-inspect": "^1.13.4",
"object-keys": "^1.1.1",
- "object.assign": "^4.1.4",
- "regexp.prototype.flags": "^1.5.1",
- "safe-array-concat": "^1.0.1",
- "safe-regex-test": "^1.0.0",
- "string.prototype.trim": "^1.2.8",
- "string.prototype.trimend": "^1.0.7",
- "string.prototype.trimstart": "^1.0.7",
- "typed-array-buffer": "^1.0.0",
- "typed-array-byte-length": "^1.0.0",
- "typed-array-byte-offset": "^1.0.0",
- "typed-array-length": "^1.0.4",
- "unbox-primitive": "^1.0.2",
- "which-typed-array": "^1.1.13"
+ "object.assign": "^4.1.7",
+ "own-keys": "^1.0.1",
+ "regexp.prototype.flags": "^1.5.4",
+ "safe-array-concat": "^1.1.3",
+ "safe-push-apply": "^1.0.0",
+ "safe-regex-test": "^1.1.0",
+ "set-proto": "^1.0.0",
+ "stop-iteration-iterator": "^1.1.0",
+ "string.prototype.trim": "^1.2.10",
+ "string.prototype.trimend": "^1.0.9",
+ "string.prototype.trimstart": "^1.0.8",
+ "typed-array-buffer": "^1.0.3",
+ "typed-array-byte-length": "^1.0.3",
+ "typed-array-byte-offset": "^1.0.4",
+ "typed-array-length": "^1.0.7",
+ "unbox-primitive": "^1.1.0",
+ "which-typed-array": "^1.1.19"
},
"engines": {
"node": ">= 0.4"
@@ -1678,38 +1935,78 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/es-set-tostringtag": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz",
- "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "get-intrinsic": "^1.2.2",
- "has-tostringtag": "^1.0.0",
- "hasown": "^2.0.0"
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-shim-unscopables": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz",
- "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz",
+ "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "hasown": "^2.0.0"
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
}
},
"node_modules/es-to-primitive": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
- "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
+ "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "is-callable": "^1.1.4",
- "is-date-object": "^1.0.1",
- "is-symbol": "^1.0.2"
+ "is-callable": "^1.2.7",
+ "is-date-object": "^1.0.5",
+ "is-symbol": "^1.0.4"
},
"engines": {
"node": ">= 0.4"
@@ -1723,6 +2020,7 @@
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=10"
},
@@ -1734,7 +2032,9 @@
"version": "7.32.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz",
"integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==",
+ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@babel/code-frame": "7.12.11",
"@eslint/eslintrc": "^0.4.3",
@@ -1792,6 +2092,7 @@
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz",
"integrity": "sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"get-stdin": "^6.0.0"
},
@@ -1807,6 +2108,7 @@
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
"integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"debug": "^3.2.7",
"is-core-module": "^2.13.0",
@@ -1818,15 +2120,17 @@
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"ms": "^2.1.1"
}
},
"node_modules/eslint-module-utils": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz",
- "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==",
+ "version": "2.12.1",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz",
+ "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"debug": "^3.2.7"
},
@@ -1844,39 +2148,43 @@
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"ms": "^2.1.1"
}
},
"node_modules/eslint-plugin-import": {
- "version": "2.29.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz",
- "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==",
+ "version": "2.32.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
+ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "array-includes": "^3.1.7",
- "array.prototype.findlastindex": "^1.2.3",
- "array.prototype.flat": "^1.3.2",
- "array.prototype.flatmap": "^1.3.2",
+ "@rtsao/scc": "^1.1.0",
+ "array-includes": "^3.1.9",
+ "array.prototype.findlastindex": "^1.2.6",
+ "array.prototype.flat": "^1.3.3",
+ "array.prototype.flatmap": "^1.3.3",
"debug": "^3.2.7",
"doctrine": "^2.1.0",
"eslint-import-resolver-node": "^0.3.9",
- "eslint-module-utils": "^2.8.0",
- "hasown": "^2.0.0",
- "is-core-module": "^2.13.1",
+ "eslint-module-utils": "^2.12.1",
+ "hasown": "^2.0.2",
+ "is-core-module": "^2.16.1",
"is-glob": "^4.0.3",
"minimatch": "^3.1.2",
- "object.fromentries": "^2.0.7",
- "object.groupby": "^1.0.1",
- "object.values": "^1.1.7",
+ "object.fromentries": "^2.0.8",
+ "object.groupby": "^1.0.3",
+ "object.values": "^1.2.1",
"semver": "^6.3.1",
- "tsconfig-paths": "^3.14.2"
+ "string.prototype.trimend": "^1.0.9",
+ "tsconfig-paths": "^3.15.0"
},
"engines": {
"node": ">=4"
},
"peerDependencies": {
- "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
}
},
"node_modules/eslint-plugin-import/node_modules/debug": {
@@ -1884,6 +2192,7 @@
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"ms": "^2.1.1"
}
@@ -1893,6 +2202,7 @@
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
"integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
"dev": true,
+ "license": "Apache-2.0",
"dependencies": {
"esutils": "^2.0.2"
},
@@ -1905,6 +2215,7 @@
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
+ "license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
@@ -1914,6 +2225,7 @@
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^4.1.1"
@@ -1927,6 +2239,7 @@
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
"integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"eslint-visitor-keys": "^1.1.0"
},
@@ -1942,6 +2255,7 @@
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
"integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
"dev": true,
+ "license": "Apache-2.0",
"engines": {
"node": ">=4"
}
@@ -1951,6 +2265,7 @@
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
+ "license": "Apache-2.0",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
@@ -1963,6 +2278,7 @@
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
"dev": true,
+ "license": "Apache-2.0",
"engines": {
"node": ">=10"
}
@@ -1972,6 +2288,7 @@
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 4"
}
@@ -1981,6 +2298,7 @@
"resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
"integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
"acorn": "^7.4.0",
"acorn-jsx": "^5.3.1",
@@ -1995,6 +2313,7 @@
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
"integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
"dev": true,
+ "license": "Apache-2.0",
"engines": {
"node": ">=4"
}
@@ -2004,6 +2323,7 @@
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true,
+ "license": "BSD-2-Clause",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
@@ -2013,10 +2333,11 @@
}
},
"node_modules/esquery": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
- "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
"dev": true,
+ "license": "BSD-3-Clause",
"dependencies": {
"estraverse": "^5.1.0"
},
@@ -2029,6 +2350,7 @@
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
+ "license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
@@ -2038,6 +2360,7 @@
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
"estraverse": "^5.2.0"
},
@@ -2050,6 +2373,7 @@
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
+ "license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
@@ -2059,6 +2383,7 @@
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"dev": true,
+ "license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
@@ -2067,13 +2392,15 @@
"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
+ "dev": true,
+ "license": "MIT"
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
+ "license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
}
@@ -2082,19 +2409,21 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/fast-glob": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
- "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
"glob-parent": "^5.1.2",
"merge2": "^1.3.0",
- "micromatch": "^4.0.4"
+ "micromatch": "^4.0.8"
},
"engines": {
"node": ">=8.6.0"
@@ -2104,19 +2433,39 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
+ "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
},
"node_modules/fastq": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
- "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
+ "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"reusify": "^1.0.4"
}
@@ -2126,6 +2475,7 @@
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
"integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"flat-cache": "^3.0.4"
},
@@ -2134,10 +2484,11 @@
}
},
"node_modules/fill-range": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
- "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
@@ -2146,39 +2497,49 @@
}
},
"node_modules/flat-cache": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz",
- "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==",
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
+ "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"flatted": "^3.2.9",
"keyv": "^4.5.3",
"rimraf": "^3.0.2"
},
"engines": {
- "node": ">=12.0.0"
+ "node": "^10.12.0 || >=12.0.0"
}
},
"node_modules/flatted": {
- "version": "3.2.9",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
- "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
- "dev": true
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
},
"node_modules/for-each": {
- "version": "0.3.3",
- "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
- "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "is-callable": "^1.1.3"
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
- "dev": true
+ "dev": true,
+ "license": "ISC"
},
"node_modules/fsevents": {
"version": "2.3.3",
@@ -2186,6 +2547,7 @@
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -2199,20 +2561,24 @@
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
+ "license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/function.prototype.name": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz",
- "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==",
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz",
+ "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1",
- "functions-have-names": "^1.2.3"
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "functions-have-names": "^1.2.3",
+ "hasown": "^2.0.2",
+ "is-callable": "^1.2.7"
},
"engines": {
"node": ">= 0.4"
@@ -2225,49 +2591,88 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
"integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/functions-have-names": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
"dev": true,
+ "license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/generator-function": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
+ "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/get-intrinsic": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
- "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
- "has-proto": "^1.0.1",
- "has-symbols": "^1.0.3",
- "hasown": "^2.0.0"
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/get-stdin": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz",
"integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/get-symbol-description": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
- "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
+ "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "get-intrinsic": "^1.1.1"
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6"
},
"engines": {
"node": ">= 0.4"
@@ -2280,7 +2685,9 @@
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
+ "license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -2301,6 +2708,7 @@
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
@@ -2309,10 +2717,11 @@
}
},
"node_modules/globals": {
- "version": "13.23.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz",
- "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==",
+ "version": "13.24.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"type-fest": "^0.20.2"
},
@@ -2324,12 +2733,14 @@
}
},
"node_modules/globalthis": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
- "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "define-properties": "^1.1.3"
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
@@ -2343,6 +2754,7 @@
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"array-union": "^2.1.0",
"dir-glob": "^3.0.1",
@@ -2359,12 +2771,13 @@
}
},
"node_modules/gopd": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
- "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
- "dependencies": {
- "get-intrinsic": "^1.1.3"
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -2374,13 +2787,18 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/has-bigints": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
- "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
+ "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
"dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -2390,27 +2808,33 @@
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/has-property-descriptors": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
- "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "get-intrinsic": "^1.2.2"
+ "es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
- "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz",
+ "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
"dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.0"
+ },
"engines": {
"node": ">= 0.4"
},
@@ -2419,10 +2843,11 @@
}
},
"node_modules/has-symbols": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
- "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -2431,12 +2856,13 @@
}
},
"node_modules/has-tostringtag": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
- "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "has-symbols": "^1.0.2"
+ "has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
@@ -2446,10 +2872,11 @@
}
},
"node_modules/hasown": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
- "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
@@ -2458,42 +2885,31 @@
}
},
"node_modules/history": {
- "version": "4.10.1",
- "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
- "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
- "dev": true,
- "dependencies": {
- "@babel/runtime": "^7.1.2",
- "loose-envify": "^1.2.0",
- "resolve-pathname": "^3.0.0",
- "tiny-invariant": "^1.0.2",
- "tiny-warning": "^1.0.0",
- "value-equal": "^1.0.1"
- }
- },
- "node_modules/hoist-non-react-statics": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
- "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
+ "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "react-is": "^16.7.0"
+ "@babel/runtime": "^7.7.6"
}
},
"node_modules/ignore": {
- "version": "5.2.4",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
- "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/import-fresh": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
- "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
@@ -2510,6 +2926,7 @@
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=0.8.19"
}
@@ -2518,7 +2935,9 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dev": true,
+ "license": "ISC",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@@ -2528,17 +2947,19 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "dev": true
+ "dev": true,
+ "license": "ISC"
},
"node_modules/internal-slot": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz",
- "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
+ "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "get-intrinsic": "^1.2.2",
- "hasown": "^2.0.0",
- "side-channel": "^1.0.4"
+ "es-errors": "^1.3.0",
+ "hasown": "^2.0.2",
+ "side-channel": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
@@ -2554,39 +2975,68 @@
}
},
"node_modules/is-array-buffer": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
- "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==",
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
+ "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-async-function": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
+ "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "get-intrinsic": "^1.2.0",
- "is-typed-array": "^1.1.10"
+ "async-function": "^1.0.0",
+ "call-bound": "^1.0.3",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-bigint": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
- "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
+ "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "has-bigints": "^1.0.1"
+ "has-bigints": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-boolean-object": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
- "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
+ "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "has-tostringtag": "^1.0.0"
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -2600,6 +3050,7 @@
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -2608,24 +3059,48 @@
}
},
"node_modules/is-core-module": {
- "version": "2.13.1",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
- "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-data-view": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz",
+ "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "hasown": "^2.0.0"
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-date-object": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
- "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
+ "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "has-tostringtag": "^1.0.0"
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -2639,24 +3114,63 @@
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
+ "node_modules/is-finalizationregistry": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
+ "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
}
},
+ "node_modules/is-generator-function": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
+ "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.4",
+ "generator-function": "^2.0.0",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
@@ -2664,11 +3178,25 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+ "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-negative-zero": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
- "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
+ "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -2681,17 +3209,20 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/is-number-object": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
- "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
+ "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "has-tostringtag": "^1.0.0"
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -2701,14 +3232,30 @@
}
},
"node_modules/is-regex": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
- "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "has-tostringtag": "^1.0.0"
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-set": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+ "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+ "dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -2717,24 +3264,30 @@
}
},
"node_modules/is-shared-array-buffer": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
- "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
+ "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2"
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-string": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
- "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
+ "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "has-tostringtag": "^1.0.0"
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -2744,12 +3297,15 @@
}
},
"node_modules/is-symbol": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
- "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
+ "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "has-symbols": "^1.0.2"
+ "call-bound": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "safe-regex-test": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
@@ -2759,13 +3315,27 @@
}
},
"node_modules/is-typed-array": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
- "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
+ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "which-typed-array": "^1.1.11"
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakmap": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+ "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+ "dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -2774,39 +3344,64 @@
}
},
"node_modules/is-weakref": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
- "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz",
+ "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakset": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
+ "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2"
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/isarray": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
- "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
- "dev": true
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "dev": true
+ "dev": true,
+ "license": "ISC"
},
"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=="
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
},
"node_modules/js-yaml": {
- "version": "3.14.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
- "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "version": "3.14.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
+ "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
@@ -2819,25 +3414,29 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/json5": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"minimist": "^1.2.0"
},
@@ -2850,6 +3449,7 @@
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"json-buffer": "3.0.1"
}
@@ -2859,6 +3459,7 @@
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"prelude-ls": "^1.2.1",
"type-check": "~0.4.0"
@@ -2871,18 +3472,21 @@
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/lodash.truncate": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
"integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"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==",
+ "license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
@@ -2890,16 +3494,14 @@
"loose-envify": "cli.js"
}
},
- "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==",
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
- "dependencies": {
- "yallist": "^4.0.0"
- },
+ "license": "MIT",
"engines": {
- "node": ">=10"
+ "node": ">= 0.4"
}
},
"node_modules/merge2": {
@@ -2907,28 +3509,44 @@
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/micromatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
- "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "braces": "^3.0.2",
+ "braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
+ "node_modules/micromatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -2941,41 +3559,50 @@
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
+ "license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"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
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/natural-compare-lite": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
"integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
- "version": "1.13.1",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
- "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -2985,19 +3612,23 @@
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/object.assign": {
- "version": "4.1.4",
- "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
- "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
+ "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.4",
- "has-symbols": "^1.0.3",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0",
+ "has-symbols": "^1.1.0",
"object-keys": "^1.1.1"
},
"engines": {
@@ -3008,14 +3639,16 @@
}
},
"node_modules/object.fromentries": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz",
- "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==",
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
+ "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1"
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
@@ -3025,26 +3658,31 @@
}
},
"node_modules/object.groupby": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz",
- "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz",
+ "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1",
- "get-intrinsic": "^1.2.1"
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
}
},
"node_modules/object.values": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz",
- "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz",
+ "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1"
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
@@ -3058,32 +3696,53 @@
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/optionator": {
- "version": "0.9.3",
- "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
- "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@aashutoshrathi/word-wrap": "^1.2.3",
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
"levn": "^0.4.1",
"prelude-ls": "^1.2.1",
- "type-check": "^0.4.0"
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
+ "node_modules/own-keys": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
+ "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.2.6",
+ "object-keys": "^1.1.1",
+ "safe-push-apply": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"callsites": "^3.0.0"
},
@@ -3096,6 +3755,7 @@
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@@ -3105,6 +3765,7 @@
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
}
@@ -3113,43 +3774,55 @@
"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/path-to-regexp": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
- "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"dev": true,
- "dependencies": {
- "isarray": "0.0.1"
- }
+ "license": "MIT"
},
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
}
},
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
+ "license": "MIT",
"engines": {
- "node": ">=8.6"
+ "node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/possible-typed-array-names": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 0.8.0"
}
@@ -3159,6 +3832,7 @@
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true,
+ "license": "MIT",
"bin": {
"prettier": "bin-prettier.js"
},
@@ -3174,26 +3848,17 @@
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
- "node_modules/prop-types": {
- "version": "15.8.1",
- "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
- "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
- "dev": true,
- "dependencies": {
- "loose-envify": "^1.4.0",
- "object-assign": "^4.1.1",
- "react-is": "^16.13.1"
- }
- },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=6"
}
@@ -3216,12 +3881,14 @@
"type": "consulting",
"url": "https://feross.org/support"
}
- ]
+ ],
+ "license": "MIT"
},
"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==",
+ "license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
@@ -3234,6 +3901,7 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
"integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
+ "license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
@@ -3243,65 +3911,76 @@
"react": "17.0.2"
}
},
- "node_modules/react-is": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "dev": true
- },
"node_modules/react-router": {
- "version": "5.3.4",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
- "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
+ "version": "6.30.2",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz",
+ "integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.12.13",
- "history": "^4.9.0",
- "hoist-non-react-statics": "^3.1.0",
- "loose-envify": "^1.3.1",
- "path-to-regexp": "^1.7.0",
- "prop-types": "^15.6.2",
- "react-is": "^16.6.0",
- "tiny-invariant": "^1.0.2",
- "tiny-warning": "^1.0.0"
+ "@remix-run/router": "1.23.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
},
"peerDependencies": {
- "react": ">=15"
+ "react": ">=16.8"
}
},
"node_modules/react-router-dom": {
- "version": "5.3.4",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz",
- "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==",
+ "version": "6.30.2",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz",
+ "integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.12.13",
- "history": "^4.9.0",
- "loose-envify": "^1.3.1",
- "prop-types": "^15.6.2",
- "react-router": "5.3.4",
- "tiny-invariant": "^1.0.2",
- "tiny-warning": "^1.0.0"
+ "@remix-run/router": "1.23.1",
+ "react-router": "6.30.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
},
"peerDependencies": {
- "react": ">=15"
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
}
},
- "node_modules/regenerator-runtime": {
- "version": "0.14.0",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
- "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==",
- "dev": true
+ "node_modules/reflect.getprototypeof": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
+ "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.7",
+ "get-proto": "^1.0.1",
+ "which-builtin-type": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
},
"node_modules/regexp.prototype.flags": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
- "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==",
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
+ "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "set-function-name": "^2.0.0"
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-errors": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "set-function-name": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -3315,6 +3994,7 @@
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
"integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
},
@@ -3327,23 +4007,28 @@
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/resolve": {
- "version": "1.22.8",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
- "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "is-core-module": "^2.13.0",
+ "is-core-module": "^2.16.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
+ "engines": {
+ "node": ">= 0.4"
+ },
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -3353,21 +4038,17 @@
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=4"
}
},
- "node_modules/resolve-pathname": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
- "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==",
- "dev": true
- },
"node_modules/reusify": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
- "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
"dev": true,
+ "license": "MIT",
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
@@ -3377,7 +4058,9 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true,
+ "license": "ISC",
"dependencies": {
"glob": "^7.1.3"
},
@@ -3389,10 +4072,14 @@
}
},
"node_modules/rollup": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.3.0.tgz",
- "integrity": "sha512-scIi1NrKLDIYSPK66jjECtII7vIgdAMFmFo8h6qm++I6nN9qDSV35Ku6erzGVqYjx+lj+j5wkusRMr++8SyDZg==",
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
+ "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
"dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
"bin": {
"rollup": "dist/bin/rollup"
},
@@ -3401,21 +4088,143 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.3.0",
- "@rollup/rollup-android-arm64": "4.3.0",
- "@rollup/rollup-darwin-arm64": "4.3.0",
- "@rollup/rollup-darwin-x64": "4.3.0",
- "@rollup/rollup-linux-arm-gnueabihf": "4.3.0",
- "@rollup/rollup-linux-arm64-gnu": "4.3.0",
- "@rollup/rollup-linux-arm64-musl": "4.3.0",
- "@rollup/rollup-linux-x64-gnu": "4.3.0",
- "@rollup/rollup-linux-x64-musl": "4.3.0",
- "@rollup/rollup-win32-arm64-msvc": "4.3.0",
- "@rollup/rollup-win32-ia32-msvc": "4.3.0",
- "@rollup/rollup-win32-x64-msvc": "4.3.0",
+ "@rollup/rollup-android-arm-eabi": "4.53.3",
+ "@rollup/rollup-android-arm64": "4.53.3",
+ "@rollup/rollup-darwin-arm64": "4.53.3",
+ "@rollup/rollup-darwin-x64": "4.53.3",
+ "@rollup/rollup-freebsd-arm64": "4.53.3",
+ "@rollup/rollup-freebsd-x64": "4.53.3",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.53.3",
+ "@rollup/rollup-linux-arm-musleabihf": "4.53.3",
+ "@rollup/rollup-linux-arm64-gnu": "4.53.3",
+ "@rollup/rollup-linux-arm64-musl": "4.53.3",
+ "@rollup/rollup-linux-loong64-gnu": "4.53.3",
+ "@rollup/rollup-linux-ppc64-gnu": "4.53.3",
+ "@rollup/rollup-linux-riscv64-gnu": "4.53.3",
+ "@rollup/rollup-linux-riscv64-musl": "4.53.3",
+ "@rollup/rollup-linux-s390x-gnu": "4.53.3",
+ "@rollup/rollup-linux-x64-gnu": "4.53.3",
+ "@rollup/rollup-linux-x64-musl": "4.53.3",
+ "@rollup/rollup-openharmony-arm64": "4.53.3",
+ "@rollup/rollup-win32-arm64-msvc": "4.53.3",
+ "@rollup/rollup-win32-ia32-msvc": "4.53.3",
+ "@rollup/rollup-win32-x64-gnu": "4.53.3",
+ "@rollup/rollup-win32-x64-msvc": "4.53.3",
"fsevents": "~2.3.2"
}
},
+ "node_modules/rollup/node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz",
+ "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz",
+ "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz",
+ "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz",
+ "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz",
+ "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz",
+ "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz",
+ "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz",
+ "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -3435,19 +4244,22 @@
"url": "https://feross.org/support"
}
],
+ "license": "MIT",
"dependencies": {
"queue-microtask": "^1.2.2"
}
},
"node_modules/safe-array-concat": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz",
- "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
+ "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "get-intrinsic": "^1.2.1",
- "has-symbols": "^1.0.3",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "has-symbols": "^1.1.0",
"isarray": "^2.0.5"
},
"engines": {
@@ -3457,21 +4269,36 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/safe-array-concat/node_modules/isarray": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
- "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
- "dev": true
+ "node_modules/safe-push-apply": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
+ "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
},
"node_modules/safe-regex-test": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
- "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "get-intrinsic": "^1.1.3",
- "is-regex": "^1.1.4"
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -3481,19 +4308,18 @@
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
"integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
+ "license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
}
},
"node_modules/semver": {
- "version": "7.5.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
- "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
+ "license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
@@ -3502,29 +4328,49 @@
}
},
"node_modules/set-function-length": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
- "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "define-data-property": "^1.1.1",
- "get-intrinsic": "^1.2.1",
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
- "has-property-descriptors": "^1.0.0"
+ "has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/set-function-name": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz",
- "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+ "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "define-data-property": "^1.0.1",
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
"functions-have-names": "^1.2.3",
- "has-property-descriptors": "^1.0.0"
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-proto": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz",
+ "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
@@ -3535,6 +4381,7 @@
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
@@ -3547,19 +4394,82 @@
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/side-channel": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
- "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.0",
- "get-intrinsic": "^1.0.2",
- "object-inspect": "^1.9.0"
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -3570,6 +4480,7 @@
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
}
@@ -3579,6 +4490,7 @@
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
"integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
@@ -3595,13 +4507,29 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
- "dev": true
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/stop-iteration-iterator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
+ "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "internal-slot": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -3612,14 +4540,19 @@
}
},
"node_modules/string.prototype.trim": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz",
- "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==",
+ "version": "1.2.10",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz",
+ "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1"
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "define-data-property": "^1.1.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-object-atoms": "^1.0.0",
+ "has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -3629,28 +4562,37 @@
}
},
"node_modules/string.prototype.trimend": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz",
- "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==",
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz",
+ "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1"
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
},
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/string.prototype.trimstart": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz",
- "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==",
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
+ "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1"
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -3661,6 +4603,7 @@
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -3673,6 +4616,7 @@
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
"integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=4"
}
@@ -3682,6 +4626,7 @@
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
},
@@ -3694,6 +4639,7 @@
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -3706,6 +4652,7 @@
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -3714,10 +4661,11 @@
}
},
"node_modules/table": {
- "version": "6.8.1",
- "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
- "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==",
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz",
+ "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==",
"dev": true,
+ "license": "BSD-3-Clause",
"dependencies": {
"ajv": "^8.0.1",
"lodash.truncate": "^4.4.2",
@@ -3730,15 +4678,16 @@
}
},
"node_modules/table/node_modules/ajv": {
- "version": "8.12.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
- "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "fast-deep-equal": "^3.1.1",
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2",
- "uri-js": "^4.2.2"
+ "require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
@@ -3749,31 +4698,22 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
- "dev": true
- },
- "node_modules/tiny-invariant": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
- "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==",
- "dev": true
- },
- "node_modules/tiny-warning": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
- "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
@@ -3782,10 +4722,11 @@
}
},
"node_modules/tsconfig-paths": {
- "version": "3.14.2",
- "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
- "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==",
+ "version": "3.15.0",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
+ "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@types/json5": "^0.0.29",
"json5": "^1.0.2",
@@ -3794,15 +4735,17 @@
}
},
"node_modules/tslib": {
- "version": "2.6.2",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
- "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
},
"node_modules/tsutils": {
"version": "3.21.0",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
"integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"tslib": "^1.8.1"
},
@@ -3817,13 +4760,15 @@
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
- "dev": true
+ "dev": true,
+ "license": "0BSD"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"prelude-ls": "^1.2.1"
},
@@ -3836,6 +4781,7 @@
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true,
+ "license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=10"
},
@@ -3844,29 +4790,32 @@
}
},
"node_modules/typed-array-buffer": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz",
- "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+ "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "get-intrinsic": "^1.2.1",
- "is-typed-array": "^1.1.10"
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.14"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/typed-array-byte-length": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz",
- "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz",
+ "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
+ "call-bind": "^1.0.8",
"for-each": "^0.3.3",
- "has-proto": "^1.0.1",
- "is-typed-array": "^1.1.10"
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.14"
},
"engines": {
"node": ">= 0.4"
@@ -3876,16 +4825,19 @@
}
},
"node_modules/typed-array-byte-offset": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz",
- "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==",
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz",
+ "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "available-typed-arrays": "^1.0.5",
- "call-bind": "^1.0.2",
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
"for-each": "^0.3.3",
- "has-proto": "^1.0.1",
- "is-typed-array": "^1.1.10"
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.15",
+ "reflect.getprototypeof": "^1.0.9"
},
"engines": {
"node": ">= 0.4"
@@ -3895,14 +4847,21 @@
}
},
"node_modules/typed-array-length": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz",
- "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==",
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz",
+ "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
+ "call-bind": "^1.0.7",
"for-each": "^0.3.3",
- "is-typed-array": "^1.1.9"
+ "gopd": "^1.0.1",
+ "is-typed-array": "^1.1.13",
+ "possible-typed-array-names": "^1.0.0",
+ "reflect.getprototypeof": "^1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -3913,6 +4872,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
+ "license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -3922,15 +4882,19 @@
}
},
"node_modules/unbox-primitive": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
- "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
+ "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
+ "call-bound": "^1.0.3",
"has-bigints": "^1.0.2",
- "has-symbols": "^1.0.3",
- "which-boxed-primitive": "^1.0.2"
+ "has-symbols": "^1.1.0",
+ "which-boxed-primitive": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -3941,6 +4905,7 @@
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
}
@@ -3949,19 +4914,15 @@
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz",
"integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==",
- "dev": true
- },
- "node_modules/value-equal": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
- "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
@@ -3973,32 +4934,45 @@
}
},
"node_modules/which-boxed-primitive": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
- "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
+ "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "is-bigint": "^1.0.1",
- "is-boolean-object": "^1.1.0",
- "is-number-object": "^1.0.4",
- "is-string": "^1.0.5",
- "is-symbol": "^1.0.3"
+ "is-bigint": "^1.1.0",
+ "is-boolean-object": "^1.2.1",
+ "is-number-object": "^1.1.1",
+ "is-string": "^1.1.1",
+ "is-symbol": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/which-typed-array": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz",
- "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==",
+ "node_modules/which-builtin-type": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz",
+ "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "available-typed-arrays": "^1.0.5",
- "call-bind": "^1.0.4",
- "for-each": "^0.3.3",
- "gopd": "^1.0.1",
- "has-tostringtag": "^1.0.0"
+ "call-bound": "^1.0.2",
+ "function.prototype.name": "^1.1.6",
+ "has-tostringtag": "^1.0.2",
+ "is-async-function": "^2.0.0",
+ "is-date-object": "^1.1.0",
+ "is-finalizationregistry": "^1.1.0",
+ "is-generator-function": "^1.0.10",
+ "is-regex": "^1.2.1",
+ "is-weakref": "^1.0.2",
+ "isarray": "^2.0.5",
+ "which-boxed-primitive": "^1.1.0",
+ "which-collection": "^1.0.2",
+ "which-typed-array": "^1.1.16"
},
"engines": {
"node": ">= 0.4"
@@ -4007,2844 +4981,63 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/wrappy": {
+ "node_modules/which-collection": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "dev": true
- },
- "node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true
- }
- },
- "dependencies": {
- "@aashutoshrathi/word-wrap": {
- "version": "1.2.6",
- "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
- "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
- "dev": true
- },
- "@babel/code-frame": {
- "version": "7.12.11",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz",
- "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==",
- "dev": true,
- "requires": {
- "@babel/highlight": "^7.10.4"
- }
- },
- "@babel/helper-validator-identifier": {
- "version": "7.22.20",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
- "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
- "dev": true
- },
- "@babel/highlight": {
- "version": "7.22.20",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
- "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
+ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
+ "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
"dev": true,
- "requires": {
- "@babel/helper-validator-identifier": "^7.22.20",
- "chalk": "^2.4.2",
- "js-tokens": "^4.0.0"
- },
+ "license": "MIT",
"dependencies": {
- "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"
- }
- },
- "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": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
- "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": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
- "dev": true
- },
- "has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
- "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"
- }
- }
- }
- },
- "@babel/runtime": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz",
- "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==",
- "dev": true,
- "requires": {
- "regenerator-runtime": "^0.14.0"
- }
- },
- "@eslint-community/eslint-utils": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
- "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
- "dev": true,
- "requires": {
- "eslint-visitor-keys": "^3.3.0"
- }
- },
- "@eslint-community/regexpp": {
- "version": "4.10.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
- "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
- "dev": true
- },
- "@eslint/eslintrc": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz",
- "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==",
- "dev": true,
- "requires": {
- "ajv": "^6.12.4",
- "debug": "^4.1.1",
- "espree": "^7.3.0",
- "globals": "^13.9.0",
- "ignore": "^4.0.6",
- "import-fresh": "^3.2.1",
- "js-yaml": "^3.13.1",
- "minimatch": "^3.0.4",
- "strip-json-comments": "^3.1.1"
+ "is-map": "^2.0.3",
+ "is-set": "^2.0.3",
+ "is-weakmap": "^2.0.2",
+ "is-weakset": "^2.0.3"
},
- "dependencies": {
- "ignore": {
- "version": "4.0.6",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
- "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
- "dev": true
- }
- }
- },
- "@humanwhocodes/config-array": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
- "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==",
- "dev": true,
- "requires": {
- "@humanwhocodes/object-schema": "^1.2.0",
- "debug": "^4.1.1",
- "minimatch": "^3.0.4"
- }
- },
- "@humanwhocodes/object-schema": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
- "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
- "dev": true
- },
- "@ionic/core": {
- "version": "8.8.0",
- "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.8.0.tgz",
- "integrity": "sha512-DNmTMK26EquKiCYqYCJHBjTZTeoDxY8hpgPQTinQEJMAoFthstcAhyWMDBwDvo/aEHEvE4tlCZV2XUMxTlTIEw==",
- "requires": {
- "@stencil/core": "4.43.0",
- "ionicons": "^8.0.13",
- "tslib": "^2.1.0"
- }
- },
- "@ionic/eslint-config": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/@ionic/eslint-config/-/eslint-config-0.3.0.tgz",
- "integrity": "sha512-Uf1hS2YIoHlcvXPF5LnsPM6auMewEdChQhR117Rt3sVEAutbyKMpFP4slNC2a6up3a5Q34zepqlf61Qgkf9XeQ==",
- "dev": true,
- "requires": {
- "@typescript-eslint/eslint-plugin": "^4.1.0",
- "@typescript-eslint/parser": "^4.1.0",
- "eslint-config-prettier": "^6.11.0",
- "eslint-plugin-import": "^2.22.0"
+ "engines": {
+ "node": ">= 0.4"
},
- "dependencies": {
- "@typescript-eslint/eslint-plugin": {
- "version": "4.33.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz",
- "integrity": "sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==",
- "dev": true,
- "requires": {
- "@typescript-eslint/experimental-utils": "4.33.0",
- "@typescript-eslint/scope-manager": "4.33.0",
- "debug": "^4.3.1",
- "functional-red-black-tree": "^1.0.1",
- "ignore": "^5.1.8",
- "regexpp": "^3.1.0",
- "semver": "^7.3.5",
- "tsutils": "^3.21.0"
- }
- },
- "@typescript-eslint/parser": {
- "version": "4.33.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz",
- "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==",
- "dev": true,
- "requires": {
- "@typescript-eslint/scope-manager": "4.33.0",
- "@typescript-eslint/types": "4.33.0",
- "@typescript-eslint/typescript-estree": "4.33.0",
- "debug": "^4.3.1"
- }
- },
- "@typescript-eslint/scope-manager": {
- "version": "4.33.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz",
- "integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==",
- "dev": true,
- "requires": {
- "@typescript-eslint/types": "4.33.0",
- "@typescript-eslint/visitor-keys": "4.33.0"
- }
- },
- "@typescript-eslint/types": {
- "version": "4.33.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz",
- "integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==",
- "dev": true
- },
- "@typescript-eslint/typescript-estree": {
- "version": "4.33.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz",
- "integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==",
- "dev": true,
- "requires": {
- "@typescript-eslint/types": "4.33.0",
- "@typescript-eslint/visitor-keys": "4.33.0",
- "debug": "^4.3.1",
- "globby": "^11.0.3",
- "is-glob": "^4.0.1",
- "semver": "^7.3.5",
- "tsutils": "^3.21.0"
- }
- },
- "@typescript-eslint/visitor-keys": {
- "version": "4.33.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz",
- "integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==",
- "dev": true,
- "requires": {
- "@typescript-eslint/types": "4.33.0",
- "eslint-visitor-keys": "^2.0.0"
- }
- },
- "eslint-visitor-keys": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
- "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
- "dev": true
- }
- }
- },
- "@ionic/prettier-config": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/@ionic/prettier-config/-/prettier-config-2.1.2.tgz",
- "integrity": "sha512-lpjXnu5XmzxDrHinjGa9z/bNe7KgXaehk6NyasyXqwzvE9EyhOSdSrkw6wS2q0HRyw8+x1GZNs2JDJ5cYq39Jw==",
- "dev": true,
- "requires": {}
- },
- "@ionic/react": {
- "version": "8.8.0",
- "resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.8.0.tgz",
- "integrity": "sha512-APU5KOhi2qlAXV9WGTJCurwRwvCL20c0dcStbS+Fa2dGIbzjM0CZk+k8pzl1ffh2URYdCP9gse2XnQln8DkVtA==",
- "requires": {
- "@ionic/core": "8.8.0",
- "ionicons": "^8.0.13",
- "tslib": "*"
- }
- },
- "@nodelib/fs.scandir": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
- "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
- "dev": true,
- "requires": {
- "@nodelib/fs.stat": "2.0.5",
- "run-parallel": "^1.1.9"
- }
- },
- "@nodelib/fs.stat": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
- "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
- "dev": true
- },
- "@nodelib/fs.walk": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
- "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
- "dev": true,
- "requires": {
- "@nodelib/fs.scandir": "2.1.5",
- "fastq": "^1.6.0"
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "@rollup/plugin-typescript": {
- "version": "11.1.5",
- "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.5.tgz",
- "integrity": "sha512-rnMHrGBB0IUEv69Q8/JGRD/n4/n6b3nfpufUu26axhUcboUzv/twfZU8fIBbTOphRAe0v8EyxzeDpKXqGHfyDA==",
+ "node_modules/which-typed-array": {
+ "version": "1.1.19",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
+ "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
"dev": true,
- "requires": {
- "@rollup/pluginutils": "^5.0.1",
- "resolve": "^1.22.1"
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "for-each": "^0.3.5",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "@rollup/pluginutils": {
- "version": "5.0.5",
- "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.5.tgz",
- "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==",
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"dev": true,
- "requires": {
- "@types/estree": "^1.0.0",
- "estree-walker": "^2.0.2",
- "picomatch": "^2.3.1"
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
}
},
- "@rollup/rollup-android-arm-eabi": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.3.0.tgz",
- "integrity": "sha512-/4pns6BYi8MXdwnXM44yoGAcFYVHL/BYlB2q1HXZ6AzH++LaiEVWFpBWQ/glXhbMbv3E3o09igrHFbP/snhAvA==",
- "dev": true,
- "optional": true
- },
- "@rollup/rollup-android-arm64": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.3.0.tgz",
- "integrity": "sha512-nLO/JsL9idr416vzi3lHm3Xm+QZh4qHij8k3Er13kZr5YhL7/+kBAx84kDmPc7HMexLmwisjDCeDIKNFp8mDlQ==",
- "dev": true,
- "optional": true
- },
- "@rollup/rollup-darwin-arm64": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.3.0.tgz",
- "integrity": "sha512-dGhVBlllt4iHwTGy21IEoMOTN5wZoid19zEIxsdY29xcEiOEHqzDa7Sqrkh5OE7LKCowL61eFJXxYe/+pYa7ZQ==",
- "dev": true,
- "optional": true
- },
- "@rollup/rollup-darwin-x64": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.3.0.tgz",
- "integrity": "sha512-h8wRfHeLEbU3NzaP1Oku7BYXCJQiTRr+8U0lklyOQXxXiEpHLL8tk1hFl+tezoRKLcPJD7joKaK74ASsqt3Ekg==",
- "dev": true,
- "optional": true
- },
- "@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.3.0.tgz",
- "integrity": "sha512-wP4VgR/gfV18sylTuym3sxRTkAgUR2vh6YLeX/GEznk5jCYcYSlx585XlcUcl0c8UffIZlRJ09raWSX3JDb4GA==",
- "dev": true,
- "optional": true
- },
- "@rollup/rollup-linux-arm64-gnu": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.3.0.tgz",
- "integrity": "sha512-v/14JCYVkqRSJeQbxFx4oUkwVQQw6lFMN7bd4vuARBc3X2lmomkxBsc+BFiIDL/BK+CTx5AOh/k9XmqDnKWRVg==",
- "dev": true,
- "optional": true
- },
- "@rollup/rollup-linux-arm64-musl": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.3.0.tgz",
- "integrity": "sha512-tNhfYqFH5OxtRzfkTOKdgFYlPSZnlDLNW4+leNEvQZhwTJxoTwsZAAhR97l3qVry/kkLyJPBK+Q8EAJLPinDIg==",
- "dev": true,
- "optional": true
- },
- "@rollup/rollup-linux-x64-gnu": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.3.0.tgz",
- "integrity": "sha512-pw77m8QywdsoFdFOgmc8roF1inBI0rciqzO8ffRUgLoq7+ee9o5eFqtEcS6hHOOplgifAUUisP8cAnwl9nUYPw==",
- "dev": true,
- "optional": true
- },
- "@rollup/rollup-linux-x64-musl": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.3.0.tgz",
- "integrity": "sha512-tJs7v2MnV2F8w6X1UpPHl/43OfxjUy9SuJ2ZPoxn79v9vYteChVYO/ueLHCpRMmyTUIVML3N9z4azl9ENH8Xxg==",
- "dev": true,
- "optional": true
- },
- "@rollup/rollup-win32-arm64-msvc": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.3.0.tgz",
- "integrity": "sha512-OKGxp6kATQdTyI2DF+e9s+hB3/QZB45b6e+dzcfW1SUqiF6CviWyevhmT4USsMEdP3mlpC9zxLz3Oh+WaTMOSw==",
- "dev": true,
- "optional": true
- },
- "@rollup/rollup-win32-ia32-msvc": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.3.0.tgz",
- "integrity": "sha512-DDZ5AH68JJ2ClQFEA1aNnfA7Ybqyeh0644rGbrLOdNehTmzfICHiWSn0OprzYi9HAshTPQvlwrM+bi2kuaIOjQ==",
- "dev": true,
- "optional": true
- },
- "@rollup/rollup-win32-x64-msvc": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.3.0.tgz",
- "integrity": "sha512-dMvGV8p92GQ8jhNlGIKpyhVZPzJlT258pPrM5q2F8lKcc9Iv9BbfdnhX1OfinYWnb9ms5zLw6MlaMnqLfUkKnQ==",
+ "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==",
"dev": true,
- "optional": true
- },
- "@stencil/core": {
- "version": "4.43.0",
- "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.43.0.tgz",
- "integrity": "sha512-6Uj2Z3lzLuufYAE7asZ6NLKgSwsB9uxl84Eh34PASnUjfj32GkrP4DtKK7fNeh1WFGGyffsTDka3gwtl+4reUg==",
- "requires": {
- "@rollup/rollup-darwin-arm64": "4.34.9",
- "@rollup/rollup-darwin-x64": "4.34.9",
- "@rollup/rollup-linux-arm64-gnu": "4.34.9",
- "@rollup/rollup-linux-arm64-musl": "4.34.9",
- "@rollup/rollup-linux-x64-gnu": "4.34.9",
- "@rollup/rollup-linux-x64-musl": "4.34.9",
- "@rollup/rollup-win32-arm64-msvc": "4.34.9",
- "@rollup/rollup-win32-x64-msvc": "4.34.9"
- },
- "dependencies": {
- "@rollup/rollup-darwin-arm64": {
- "version": "4.34.9",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz",
- "integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==",
- "optional": true
- },
- "@rollup/rollup-darwin-x64": {
- "version": "4.34.9",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz",
- "integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==",
- "optional": true
- },
- "@rollup/rollup-linux-arm64-gnu": {
- "version": "4.34.9",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz",
- "integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==",
- "optional": true
- },
- "@rollup/rollup-linux-arm64-musl": {
- "version": "4.34.9",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz",
- "integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==",
- "optional": true
- },
- "@rollup/rollup-linux-x64-gnu": {
- "version": "4.34.9",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz",
- "integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==",
- "optional": true
- },
- "@rollup/rollup-linux-x64-musl": {
- "version": "4.34.9",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz",
- "integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==",
- "optional": true
- },
- "@rollup/rollup-win32-arm64-msvc": {
- "version": "4.34.9",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz",
- "integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==",
- "optional": true
- },
- "@rollup/rollup-win32-x64-msvc": {
- "version": "4.34.9",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz",
- "integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==",
- "optional": true
- }
- }
- },
- "@types/estree": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.4.tgz",
- "integrity": "sha512-2JwWnHK9H+wUZNorf2Zr6ves96WHoWDJIftkcxPKsS7Djta6Zu519LarhRNljPXkpsZR2ZMwNCPeW7omW07BJw==",
- "dev": true
- },
- "@types/history": {
- "version": "4.7.11",
- "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
- "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==",
- "dev": true
- },
- "@types/json-schema": {
- "version": "7.0.14",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz",
- "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==",
- "dev": true
- },
- "@types/json5": {
- "version": "0.0.29",
- "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
- "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
- "dev": true
- },
- "@types/node": {
- "version": "14.18.63",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz",
- "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==",
- "dev": true
- },
- "@types/prop-types": {
- "version": "15.7.9",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz",
- "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==",
- "dev": true
- },
- "@types/react": {
- "version": "17.0.79",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.79.tgz",
- "integrity": "sha512-gavKA8AwJAML9zWHuiQRASjrrPJHbT/zrUDHiUGUf+l5a3pkEd6atvjjq+8y2vfRHBJLQJjFpxSa9I8qe9zHAw==",
- "dev": true,
- "requires": {
- "@types/prop-types": "*",
- "@types/scheduler": "*",
- "csstype": "^3.0.2"
- }
- },
- "@types/react-dom": {
- "version": "17.0.25",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.25.tgz",
- "integrity": "sha512-urx7A7UxkZQmThYA4So0NelOVjx3V4rNFVJwp0WZlbIK5eM4rNJDiN3R/E9ix0MBh6kAEojk/9YL+Te6D9zHNA==",
- "dev": true,
- "requires": {
- "@types/react": "^17"
- }
- },
- "@types/react-router": {
- "version": "5.1.20",
- "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
- "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==",
- "dev": true,
- "requires": {
- "@types/history": "^4.7.11",
- "@types/react": "*"
- }
- },
- "@types/react-router-dom": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz",
- "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==",
- "dev": true,
- "requires": {
- "@types/history": "^4.7.11",
- "@types/react": "*",
- "@types/react-router": "*"
- }
- },
- "@types/scheduler": {
- "version": "0.16.8",
- "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
- "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
- "dev": true
- },
- "@types/semver": {
- "version": "7.5.4",
- "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz",
- "integrity": "sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==",
- "dev": true
- },
- "@typescript-eslint/eslint-plugin": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
- "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
- "dev": true,
- "requires": {
- "@eslint-community/regexpp": "^4.4.0",
- "@typescript-eslint/scope-manager": "5.62.0",
- "@typescript-eslint/type-utils": "5.62.0",
- "@typescript-eslint/utils": "5.62.0",
- "debug": "^4.3.4",
- "graphemer": "^1.4.0",
- "ignore": "^5.2.0",
- "natural-compare-lite": "^1.4.0",
- "semver": "^7.3.7",
- "tsutils": "^3.21.0"
- }
- },
- "@typescript-eslint/experimental-utils": {
- "version": "4.33.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz",
- "integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==",
- "dev": true,
- "requires": {
- "@types/json-schema": "^7.0.7",
- "@typescript-eslint/scope-manager": "4.33.0",
- "@typescript-eslint/types": "4.33.0",
- "@typescript-eslint/typescript-estree": "4.33.0",
- "eslint-scope": "^5.1.1",
- "eslint-utils": "^3.0.0"
- },
- "dependencies": {
- "@typescript-eslint/scope-manager": {
- "version": "4.33.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz",
- "integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==",
- "dev": true,
- "requires": {
- "@typescript-eslint/types": "4.33.0",
- "@typescript-eslint/visitor-keys": "4.33.0"
- }
- },
- "@typescript-eslint/types": {
- "version": "4.33.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz",
- "integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==",
- "dev": true
- },
- "@typescript-eslint/typescript-estree": {
- "version": "4.33.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz",
- "integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==",
- "dev": true,
- "requires": {
- "@typescript-eslint/types": "4.33.0",
- "@typescript-eslint/visitor-keys": "4.33.0",
- "debug": "^4.3.1",
- "globby": "^11.0.3",
- "is-glob": "^4.0.1",
- "semver": "^7.3.5",
- "tsutils": "^3.21.0"
- }
- },
- "@typescript-eslint/visitor-keys": {
- "version": "4.33.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz",
- "integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==",
- "dev": true,
- "requires": {
- "@typescript-eslint/types": "4.33.0",
- "eslint-visitor-keys": "^2.0.0"
- }
- },
- "eslint-utils": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
- "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
- "dev": true,
- "requires": {
- "eslint-visitor-keys": "^2.0.0"
- }
- },
- "eslint-visitor-keys": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
- "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
- "dev": true
- }
- }
- },
- "@typescript-eslint/parser": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
- "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
- "dev": true,
- "requires": {
- "@typescript-eslint/scope-manager": "5.62.0",
- "@typescript-eslint/types": "5.62.0",
- "@typescript-eslint/typescript-estree": "5.62.0",
- "debug": "^4.3.4"
- }
- },
- "@typescript-eslint/scope-manager": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
- "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==",
- "dev": true,
- "requires": {
- "@typescript-eslint/types": "5.62.0",
- "@typescript-eslint/visitor-keys": "5.62.0"
- }
- },
- "@typescript-eslint/type-utils": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
- "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
- "dev": true,
- "requires": {
- "@typescript-eslint/typescript-estree": "5.62.0",
- "@typescript-eslint/utils": "5.62.0",
- "debug": "^4.3.4",
- "tsutils": "^3.21.0"
- }
- },
- "@typescript-eslint/types": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
- "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==",
- "dev": true
- },
- "@typescript-eslint/typescript-estree": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz",
- "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==",
- "dev": true,
- "requires": {
- "@typescript-eslint/types": "5.62.0",
- "@typescript-eslint/visitor-keys": "5.62.0",
- "debug": "^4.3.4",
- "globby": "^11.1.0",
- "is-glob": "^4.0.3",
- "semver": "^7.3.7",
- "tsutils": "^3.21.0"
- }
- },
- "@typescript-eslint/utils": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz",
- "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==",
- "dev": true,
- "requires": {
- "@eslint-community/eslint-utils": "^4.2.0",
- "@types/json-schema": "^7.0.9",
- "@types/semver": "^7.3.12",
- "@typescript-eslint/scope-manager": "5.62.0",
- "@typescript-eslint/types": "5.62.0",
- "@typescript-eslint/typescript-estree": "5.62.0",
- "eslint-scope": "^5.1.1",
- "semver": "^7.3.7"
- }
- },
- "@typescript-eslint/visitor-keys": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz",
- "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==",
- "dev": true,
- "requires": {
- "@typescript-eslint/types": "5.62.0",
- "eslint-visitor-keys": "^3.3.0"
- }
- },
- "acorn": {
- "version": "7.4.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
- "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
- "dev": true
- },
- "acorn-jsx": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
- "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
- "dev": true,
- "requires": {}
- },
- "ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "dev": true,
- "requires": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- }
- },
- "ansi-colors": {
- "version": "4.1.3",
- "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
- "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
- "dev": true
- },
- "ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true
- },
- "ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "requires": {
- "color-convert": "^2.0.1"
- }
- },
- "argparse": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
- "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
- "dev": true,
- "requires": {
- "sprintf-js": "~1.0.2"
- }
- },
- "array-buffer-byte-length": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz",
- "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "is-array-buffer": "^3.0.1"
- }
- },
- "array-includes": {
- "version": "3.1.7",
- "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz",
- "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1",
- "get-intrinsic": "^1.2.1",
- "is-string": "^1.0.7"
- }
- },
- "array-union": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
- "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
- "dev": true
- },
- "array.prototype.findlastindex": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz",
- "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1",
- "es-shim-unscopables": "^1.0.0",
- "get-intrinsic": "^1.2.1"
- }
- },
- "array.prototype.flat": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz",
- "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1",
- "es-shim-unscopables": "^1.0.0"
- }
- },
- "array.prototype.flatmap": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz",
- "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1",
- "es-shim-unscopables": "^1.0.0"
- }
- },
- "arraybuffer.prototype.slice": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz",
- "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==",
- "dev": true,
- "requires": {
- "array-buffer-byte-length": "^1.0.0",
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1",
- "get-intrinsic": "^1.2.1",
- "is-array-buffer": "^3.0.2",
- "is-shared-array-buffer": "^1.0.2"
- }
- },
- "astral-regex": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
- "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
- "dev": true
- },
- "available-typed-arrays": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
- "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
- "dev": true
- },
- "balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true
- },
- "brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
- "requires": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "braces": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
- "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
- "dev": true,
- "requires": {
- "fill-range": "^7.0.1"
- }
- },
- "call-bind": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
- "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
- "dev": true,
- "requires": {
- "function-bind": "^1.1.2",
- "get-intrinsic": "^1.2.1",
- "set-function-length": "^1.1.1"
- }
- },
- "callsites": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
- "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "dev": true
- },
- "chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
- "dev": true,
- "requires": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- }
- },
- "color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "requires": {
- "color-name": "~1.1.4"
- }
- },
- "color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true
- },
- "concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "dev": true
- },
- "cross-spawn": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
- "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
- "dev": true,
- "requires": {
- "path-key": "^3.1.0",
- "shebang-command": "^2.0.0",
- "which": "^2.0.1"
- }
- },
- "csstype": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
- "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
- "dev": true
- },
- "debug": {
- "version": "4.3.4",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
- "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
- "dev": true,
- "requires": {
- "ms": "2.1.2"
- }
- },
- "deep-is": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
- "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
- "dev": true
- },
- "define-data-property": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
- "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
- "dev": true,
- "requires": {
- "get-intrinsic": "^1.2.1",
- "gopd": "^1.0.1",
- "has-property-descriptors": "^1.0.0"
- }
- },
- "define-properties": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
- "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
- "dev": true,
- "requires": {
- "define-data-property": "^1.0.1",
- "has-property-descriptors": "^1.0.0",
- "object-keys": "^1.1.1"
- }
- },
- "dir-glob": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
- "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
- "dev": true,
- "requires": {
- "path-type": "^4.0.0"
- }
- },
- "doctrine": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
- "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2"
- }
- },
- "emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true
- },
- "enquirer": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
- "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==",
- "dev": true,
- "requires": {
- "ansi-colors": "^4.1.1",
- "strip-ansi": "^6.0.1"
- }
- },
- "es-abstract": {
- "version": "1.22.3",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz",
- "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==",
- "dev": true,
- "requires": {
- "array-buffer-byte-length": "^1.0.0",
- "arraybuffer.prototype.slice": "^1.0.2",
- "available-typed-arrays": "^1.0.5",
- "call-bind": "^1.0.5",
- "es-set-tostringtag": "^2.0.1",
- "es-to-primitive": "^1.2.1",
- "function.prototype.name": "^1.1.6",
- "get-intrinsic": "^1.2.2",
- "get-symbol-description": "^1.0.0",
- "globalthis": "^1.0.3",
- "gopd": "^1.0.1",
- "has-property-descriptors": "^1.0.0",
- "has-proto": "^1.0.1",
- "has-symbols": "^1.0.3",
- "hasown": "^2.0.0",
- "internal-slot": "^1.0.5",
- "is-array-buffer": "^3.0.2",
- "is-callable": "^1.2.7",
- "is-negative-zero": "^2.0.2",
- "is-regex": "^1.1.4",
- "is-shared-array-buffer": "^1.0.2",
- "is-string": "^1.0.7",
- "is-typed-array": "^1.1.12",
- "is-weakref": "^1.0.2",
- "object-inspect": "^1.13.1",
- "object-keys": "^1.1.1",
- "object.assign": "^4.1.4",
- "regexp.prototype.flags": "^1.5.1",
- "safe-array-concat": "^1.0.1",
- "safe-regex-test": "^1.0.0",
- "string.prototype.trim": "^1.2.8",
- "string.prototype.trimend": "^1.0.7",
- "string.prototype.trimstart": "^1.0.7",
- "typed-array-buffer": "^1.0.0",
- "typed-array-byte-length": "^1.0.0",
- "typed-array-byte-offset": "^1.0.0",
- "typed-array-length": "^1.0.4",
- "unbox-primitive": "^1.0.2",
- "which-typed-array": "^1.1.13"
- }
- },
- "es-set-tostringtag": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz",
- "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==",
- "dev": true,
- "requires": {
- "get-intrinsic": "^1.2.2",
- "has-tostringtag": "^1.0.0",
- "hasown": "^2.0.0"
- }
- },
- "es-shim-unscopables": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz",
- "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==",
- "dev": true,
- "requires": {
- "hasown": "^2.0.0"
- }
- },
- "es-to-primitive": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
- "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
- "dev": true,
- "requires": {
- "is-callable": "^1.1.4",
- "is-date-object": "^1.0.1",
- "is-symbol": "^1.0.2"
- }
- },
- "escape-string-regexp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "dev": true
- },
- "eslint": {
- "version": "7.32.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz",
- "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==",
- "dev": true,
- "requires": {
- "@babel/code-frame": "7.12.11",
- "@eslint/eslintrc": "^0.4.3",
- "@humanwhocodes/config-array": "^0.5.0",
- "ajv": "^6.10.0",
- "chalk": "^4.0.0",
- "cross-spawn": "^7.0.2",
- "debug": "^4.0.1",
- "doctrine": "^3.0.0",
- "enquirer": "^2.3.5",
- "escape-string-regexp": "^4.0.0",
- "eslint-scope": "^5.1.1",
- "eslint-utils": "^2.1.0",
- "eslint-visitor-keys": "^2.0.0",
- "espree": "^7.3.1",
- "esquery": "^1.4.0",
- "esutils": "^2.0.2",
- "fast-deep-equal": "^3.1.3",
- "file-entry-cache": "^6.0.1",
- "functional-red-black-tree": "^1.0.1",
- "glob-parent": "^5.1.2",
- "globals": "^13.6.0",
- "ignore": "^4.0.6",
- "import-fresh": "^3.0.0",
- "imurmurhash": "^0.1.4",
- "is-glob": "^4.0.0",
- "js-yaml": "^3.13.1",
- "json-stable-stringify-without-jsonify": "^1.0.1",
- "levn": "^0.4.1",
- "lodash.merge": "^4.6.2",
- "minimatch": "^3.0.4",
- "natural-compare": "^1.4.0",
- "optionator": "^0.9.1",
- "progress": "^2.0.0",
- "regexpp": "^3.1.0",
- "semver": "^7.2.1",
- "strip-ansi": "^6.0.0",
- "strip-json-comments": "^3.1.0",
- "table": "^6.0.9",
- "text-table": "^0.2.0",
- "v8-compile-cache": "^2.0.3"
- },
- "dependencies": {
- "eslint-visitor-keys": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
- "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
- "dev": true
- },
- "ignore": {
- "version": "4.0.6",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
- "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
- "dev": true
- }
- }
- },
- "eslint-config-prettier": {
- "version": "6.15.0",
- "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz",
- "integrity": "sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw==",
- "dev": true,
- "requires": {
- "get-stdin": "^6.0.0"
- }
- },
- "eslint-import-resolver-node": {
- "version": "0.3.9",
- "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
- "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
- "dev": true,
- "requires": {
- "debug": "^3.2.7",
- "is-core-module": "^2.13.0",
- "resolve": "^1.22.4"
- },
- "dependencies": {
- "debug": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
- "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
- "dev": true,
- "requires": {
- "ms": "^2.1.1"
- }
- }
- }
- },
- "eslint-module-utils": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz",
- "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==",
- "dev": true,
- "requires": {
- "debug": "^3.2.7"
- },
- "dependencies": {
- "debug": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
- "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
- "dev": true,
- "requires": {
- "ms": "^2.1.1"
- }
- }
- }
- },
- "eslint-plugin-import": {
- "version": "2.29.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz",
- "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==",
- "dev": true,
- "requires": {
- "array-includes": "^3.1.7",
- "array.prototype.findlastindex": "^1.2.3",
- "array.prototype.flat": "^1.3.2",
- "array.prototype.flatmap": "^1.3.2",
- "debug": "^3.2.7",
- "doctrine": "^2.1.0",
- "eslint-import-resolver-node": "^0.3.9",
- "eslint-module-utils": "^2.8.0",
- "hasown": "^2.0.0",
- "is-core-module": "^2.13.1",
- "is-glob": "^4.0.3",
- "minimatch": "^3.1.2",
- "object.fromentries": "^2.0.7",
- "object.groupby": "^1.0.1",
- "object.values": "^1.1.7",
- "semver": "^6.3.1",
- "tsconfig-paths": "^3.14.2"
- },
- "dependencies": {
- "debug": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
- "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
- "dev": true,
- "requires": {
- "ms": "^2.1.1"
- }
- },
- "doctrine": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
- "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2"
- }
- },
- "semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true
- }
- }
- },
- "eslint-scope": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
- "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
- "dev": true,
- "requires": {
- "esrecurse": "^4.3.0",
- "estraverse": "^4.1.1"
- }
- },
- "eslint-utils": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
- "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
- "dev": true,
- "requires": {
- "eslint-visitor-keys": "^1.1.0"
- },
- "dependencies": {
- "eslint-visitor-keys": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
- "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
- "dev": true
- }
- }
- },
- "eslint-visitor-keys": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
- "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
- "dev": true
- },
- "espree": {
- "version": "7.3.1",
- "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
- "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==",
- "dev": true,
- "requires": {
- "acorn": "^7.4.0",
- "acorn-jsx": "^5.3.1",
- "eslint-visitor-keys": "^1.3.0"
- },
- "dependencies": {
- "eslint-visitor-keys": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
- "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
- "dev": true
- }
- }
- },
- "esprima": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
- "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
- "dev": true
- },
- "esquery": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
- "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
- "dev": true,
- "requires": {
- "estraverse": "^5.1.0"
- },
- "dependencies": {
- "estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "dev": true
- }
- }
- },
- "esrecurse": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
- "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
- "dev": true,
- "requires": {
- "estraverse": "^5.2.0"
- },
- "dependencies": {
- "estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "dev": true
- }
- }
- },
- "estraverse": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
- "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
- "dev": true
- },
- "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
- },
- "esutils": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
- "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
- "dev": true
- },
- "fast-deep-equal": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true
- },
- "fast-glob": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
- "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
- "dev": true,
- "requires": {
- "@nodelib/fs.stat": "^2.0.2",
- "@nodelib/fs.walk": "^1.2.3",
- "glob-parent": "^5.1.2",
- "merge2": "^1.3.0",
- "micromatch": "^4.0.4"
- }
- },
- "fast-json-stable-stringify": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
- "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
- "dev": true
- },
- "fast-levenshtein": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
- "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
- "dev": true
- },
- "fastq": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
- "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
- "dev": true,
- "requires": {
- "reusify": "^1.0.4"
- }
- },
- "file-entry-cache": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
- "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
- "dev": true,
- "requires": {
- "flat-cache": "^3.0.4"
- }
- },
- "fill-range": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
- "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
- "dev": true,
- "requires": {
- "to-regex-range": "^5.0.1"
- }
- },
- "flat-cache": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz",
- "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==",
- "dev": true,
- "requires": {
- "flatted": "^3.2.9",
- "keyv": "^4.5.3",
- "rimraf": "^3.0.2"
- }
- },
- "flatted": {
- "version": "3.2.9",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
- "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
- "dev": true
- },
- "for-each": {
- "version": "0.3.3",
- "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
- "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
- "dev": true,
- "requires": {
- "is-callable": "^1.1.3"
- }
- },
- "fs.realpath": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
- "dev": true
- },
- "fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "optional": true
- },
- "function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "dev": true
- },
- "function.prototype.name": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz",
- "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1",
- "functions-have-names": "^1.2.3"
- }
- },
- "functional-red-black-tree": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
- "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==",
- "dev": true
- },
- "functions-have-names": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
- "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
- "dev": true
- },
- "get-intrinsic": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
- "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
- "dev": true,
- "requires": {
- "function-bind": "^1.1.2",
- "has-proto": "^1.0.1",
- "has-symbols": "^1.0.3",
- "hasown": "^2.0.0"
- }
- },
- "get-stdin": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz",
- "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==",
- "dev": true
- },
- "get-symbol-description": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
- "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "get-intrinsic": "^1.1.1"
- }
- },
- "glob": {
- "version": "7.2.3",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
- "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
- "dev": true,
- "requires": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.1.1",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- }
- },
- "glob-parent": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
- "requires": {
- "is-glob": "^4.0.1"
- }
- },
- "globals": {
- "version": "13.23.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz",
- "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==",
- "dev": true,
- "requires": {
- "type-fest": "^0.20.2"
- }
- },
- "globalthis": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
- "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
- "dev": true,
- "requires": {
- "define-properties": "^1.1.3"
- }
- },
- "globby": {
- "version": "11.1.0",
- "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
- "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
- "dev": true,
- "requires": {
- "array-union": "^2.1.0",
- "dir-glob": "^3.0.1",
- "fast-glob": "^3.2.9",
- "ignore": "^5.2.0",
- "merge2": "^1.4.1",
- "slash": "^3.0.0"
- }
- },
- "gopd": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
- "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
- "dev": true,
- "requires": {
- "get-intrinsic": "^1.1.3"
- }
- },
- "graphemer": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
- "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
- "dev": true
- },
- "has-bigints": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
- "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
- "dev": true
- },
- "has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "dev": true
- },
- "has-property-descriptors": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
- "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
- "dev": true,
- "requires": {
- "get-intrinsic": "^1.2.2"
- }
- },
- "has-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
- "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
- "dev": true
- },
- "has-symbols": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
- "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
- "dev": true
- },
- "has-tostringtag": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
- "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
- "dev": true,
- "requires": {
- "has-symbols": "^1.0.2"
- }
- },
- "hasown": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
- "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
- "dev": true,
- "requires": {
- "function-bind": "^1.1.2"
- }
- },
- "history": {
- "version": "4.10.1",
- "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
- "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
- "dev": true,
- "requires": {
- "@babel/runtime": "^7.1.2",
- "loose-envify": "^1.2.0",
- "resolve-pathname": "^3.0.0",
- "tiny-invariant": "^1.0.2",
- "tiny-warning": "^1.0.0",
- "value-equal": "^1.0.1"
- }
- },
- "hoist-non-react-statics": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
- "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
- "dev": true,
- "requires": {
- "react-is": "^16.7.0"
- }
- },
- "ignore": {
- "version": "5.2.4",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
- "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
- "dev": true
- },
- "import-fresh": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
- "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
- "dev": true,
- "requires": {
- "parent-module": "^1.0.0",
- "resolve-from": "^4.0.0"
- }
- },
- "imurmurhash": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
- "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
- "dev": true
- },
- "inflight": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
- "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
- "dev": true,
- "requires": {
- "once": "^1.3.0",
- "wrappy": "1"
- }
- },
- "inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "dev": true
- },
- "internal-slot": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz",
- "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==",
- "dev": true,
- "requires": {
- "get-intrinsic": "^1.2.2",
- "hasown": "^2.0.0",
- "side-channel": "^1.0.4"
- }
- },
- "ionicons": {
- "version": "8.0.13",
- "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-8.0.13.tgz",
- "integrity": "sha512-2QQVyG2P4wszne79jemMjWYLp0DBbDhr4/yFroPCxvPP1wtMxgdIV3l5n+XZ5E9mgoXU79w7yTWpm2XzJsISxQ==",
- "requires": {
- "@stencil/core": "^4.35.3"
- }
- },
- "is-array-buffer": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
- "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "get-intrinsic": "^1.2.0",
- "is-typed-array": "^1.1.10"
- }
- },
- "is-bigint": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
- "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
- "dev": true,
- "requires": {
- "has-bigints": "^1.0.1"
- }
- },
- "is-boolean-object": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
- "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "has-tostringtag": "^1.0.0"
- }
- },
- "is-callable": {
- "version": "1.2.7",
- "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
- "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
- "dev": true
- },
- "is-core-module": {
- "version": "2.13.1",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
- "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
- "dev": true,
- "requires": {
- "hasown": "^2.0.0"
- }
- },
- "is-date-object": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
- "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
- "dev": true,
- "requires": {
- "has-tostringtag": "^1.0.0"
- }
- },
- "is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "dev": true
- },
- "is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "dev": true
- },
- "is-glob": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
- "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
- "dev": true,
- "requires": {
- "is-extglob": "^2.1.1"
- }
- },
- "is-negative-zero": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
- "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
- "dev": true
- },
- "is-number": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true
- },
- "is-number-object": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
- "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
- "dev": true,
- "requires": {
- "has-tostringtag": "^1.0.0"
- }
- },
- "is-regex": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
- "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "has-tostringtag": "^1.0.0"
- }
- },
- "is-shared-array-buffer": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
- "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2"
- }
- },
- "is-string": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
- "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
- "dev": true,
- "requires": {
- "has-tostringtag": "^1.0.0"
- }
- },
- "is-symbol": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
- "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
- "dev": true,
- "requires": {
- "has-symbols": "^1.0.2"
- }
- },
- "is-typed-array": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
- "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
- "dev": true,
- "requires": {
- "which-typed-array": "^1.1.11"
- }
- },
- "is-weakref": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
- "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2"
- }
- },
- "isarray": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
- "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
- "dev": true
- },
- "isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "dev": true
- },
- "js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
- },
- "js-yaml": {
- "version": "3.14.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
- "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
- "dev": true,
- "requires": {
- "argparse": "^1.0.7",
- "esprima": "^4.0.0"
- }
- },
- "json-buffer": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
- "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
- "dev": true
- },
- "json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true
- },
- "json-stable-stringify-without-jsonify": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
- "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
- "dev": true
- },
- "json5": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
- "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
- "dev": true,
- "requires": {
- "minimist": "^1.2.0"
- }
- },
- "keyv": {
- "version": "4.5.4",
- "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
- "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
- "dev": true,
- "requires": {
- "json-buffer": "3.0.1"
- }
- },
- "levn": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
- "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
- "dev": true,
- "requires": {
- "prelude-ls": "^1.2.1",
- "type-check": "~0.4.0"
- }
- },
- "lodash.merge": {
- "version": "4.6.2",
- "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
- "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
- "dev": true
- },
- "lodash.truncate": {
- "version": "4.4.2",
- "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
- "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==",
- "dev": true
- },
- "loose-envify": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
- "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
- "requires": {
- "js-tokens": "^3.0.0 || ^4.0.0"
- }
- },
- "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==",
- "dev": true,
- "requires": {
- "yallist": "^4.0.0"
- }
- },
- "merge2": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
- "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
- "dev": true
- },
- "micromatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
- "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
- "dev": true,
- "requires": {
- "braces": "^3.0.2",
- "picomatch": "^2.3.1"
- }
- },
- "minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
- "requires": {
- "brace-expansion": "^1.1.7"
- }
- },
- "minimist": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
- "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
- "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
- },
- "natural-compare": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
- "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
- "dev": true
- },
- "natural-compare-lite": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
- "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
- "dev": true
- },
- "object-assign": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
- },
- "object-inspect": {
- "version": "1.13.1",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
- "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
- "dev": true
- },
- "object-keys": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
- "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
- "dev": true
- },
- "object.assign": {
- "version": "4.1.4",
- "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
- "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.4",
- "has-symbols": "^1.0.3",
- "object-keys": "^1.1.1"
- }
- },
- "object.fromentries": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz",
- "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1"
- }
- },
- "object.groupby": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz",
- "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1",
- "get-intrinsic": "^1.2.1"
- }
- },
- "object.values": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz",
- "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1"
- }
- },
- "once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
- "dev": true,
- "requires": {
- "wrappy": "1"
- }
- },
- "optionator": {
- "version": "0.9.3",
- "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
- "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
- "dev": true,
- "requires": {
- "@aashutoshrathi/word-wrap": "^1.2.3",
- "deep-is": "^0.1.3",
- "fast-levenshtein": "^2.0.6",
- "levn": "^0.4.1",
- "prelude-ls": "^1.2.1",
- "type-check": "^0.4.0"
- }
- },
- "parent-module": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
- "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
- "dev": true,
- "requires": {
- "callsites": "^3.0.0"
- }
- },
- "path-is-absolute": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
- "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
- "dev": true
- },
- "path-key": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "dev": true
- },
- "path-parse": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
- "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
- "dev": true
- },
- "path-to-regexp": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
- "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
- "dev": true,
- "requires": {
- "isarray": "0.0.1"
- }
- },
- "path-type": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
- "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
- "dev": true
- },
- "picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
- "dev": true
- },
- "prelude-ls": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
- "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
- "dev": true
- },
- "prettier": {
- "version": "2.8.8",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
- "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
- "dev": true
- },
- "progress": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
- "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
- "dev": true
- },
- "prop-types": {
- "version": "15.8.1",
- "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
- "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
- "dev": true,
- "requires": {
- "loose-envify": "^1.4.0",
- "object-assign": "^4.1.1",
- "react-is": "^16.13.1"
- }
- },
- "punycode": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
- "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
- "dev": true
- },
- "queue-microtask": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
- "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
- "dev": true
- },
- "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-is": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "dev": true
- },
- "react-router": {
- "version": "5.3.4",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
- "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
- "dev": true,
- "requires": {
- "@babel/runtime": "^7.12.13",
- "history": "^4.9.0",
- "hoist-non-react-statics": "^3.1.0",
- "loose-envify": "^1.3.1",
- "path-to-regexp": "^1.7.0",
- "prop-types": "^15.6.2",
- "react-is": "^16.6.0",
- "tiny-invariant": "^1.0.2",
- "tiny-warning": "^1.0.0"
- }
- },
- "react-router-dom": {
- "version": "5.3.4",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz",
- "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==",
- "dev": true,
- "requires": {
- "@babel/runtime": "^7.12.13",
- "history": "^4.9.0",
- "loose-envify": "^1.3.1",
- "prop-types": "^15.6.2",
- "react-router": "5.3.4",
- "tiny-invariant": "^1.0.2",
- "tiny-warning": "^1.0.0"
- }
- },
- "regenerator-runtime": {
- "version": "0.14.0",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
- "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==",
- "dev": true
- },
- "regexp.prototype.flags": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
- "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "set-function-name": "^2.0.0"
- }
- },
- "regexpp": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
- "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
- "dev": true
- },
- "require-from-string": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
- "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
- "dev": true
- },
- "resolve": {
- "version": "1.22.8",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
- "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
- "dev": true,
- "requires": {
- "is-core-module": "^2.13.0",
- "path-parse": "^1.0.7",
- "supports-preserve-symlinks-flag": "^1.0.0"
- }
- },
- "resolve-from": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
- "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
- "dev": true
- },
- "resolve-pathname": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
- "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==",
- "dev": true
- },
- "reusify": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
- "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
- "dev": true
- },
- "rimraf": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
- "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
- "dev": true,
- "requires": {
- "glob": "^7.1.3"
- }
- },
- "rollup": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.3.0.tgz",
- "integrity": "sha512-scIi1NrKLDIYSPK66jjECtII7vIgdAMFmFo8h6qm++I6nN9qDSV35Ku6erzGVqYjx+lj+j5wkusRMr++8SyDZg==",
- "dev": true,
- "requires": {
- "@rollup/rollup-android-arm-eabi": "4.3.0",
- "@rollup/rollup-android-arm64": "4.3.0",
- "@rollup/rollup-darwin-arm64": "4.3.0",
- "@rollup/rollup-darwin-x64": "4.3.0",
- "@rollup/rollup-linux-arm-gnueabihf": "4.3.0",
- "@rollup/rollup-linux-arm64-gnu": "4.3.0",
- "@rollup/rollup-linux-arm64-musl": "4.3.0",
- "@rollup/rollup-linux-x64-gnu": "4.3.0",
- "@rollup/rollup-linux-x64-musl": "4.3.0",
- "@rollup/rollup-win32-arm64-msvc": "4.3.0",
- "@rollup/rollup-win32-ia32-msvc": "4.3.0",
- "@rollup/rollup-win32-x64-msvc": "4.3.0",
- "fsevents": "~2.3.2"
- }
- },
- "run-parallel": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
- "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
- "dev": true,
- "requires": {
- "queue-microtask": "^1.2.2"
- }
- },
- "safe-array-concat": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz",
- "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "get-intrinsic": "^1.2.1",
- "has-symbols": "^1.0.3",
- "isarray": "^2.0.5"
- },
- "dependencies": {
- "isarray": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
- "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
- "dev": true
- }
- }
- },
- "safe-regex-test": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
- "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "get-intrinsic": "^1.1.3",
- "is-regex": "^1.1.4"
- }
- },
- "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": "7.5.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
- "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
- "dev": true,
- "requires": {
- "lru-cache": "^6.0.0"
- }
- },
- "set-function-length": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
- "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
- "dev": true,
- "requires": {
- "define-data-property": "^1.1.1",
- "get-intrinsic": "^1.2.1",
- "gopd": "^1.0.1",
- "has-property-descriptors": "^1.0.0"
- }
- },
- "set-function-name": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz",
- "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==",
- "dev": true,
- "requires": {
- "define-data-property": "^1.0.1",
- "functions-have-names": "^1.2.3",
- "has-property-descriptors": "^1.0.0"
- }
- },
- "shebang-command": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
- "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "dev": true,
- "requires": {
- "shebang-regex": "^3.0.0"
- }
- },
- "shebang-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
- "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "dev": true
- },
- "side-channel": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
- "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.0",
- "get-intrinsic": "^1.0.2",
- "object-inspect": "^1.9.0"
- }
- },
- "slash": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
- "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
- "dev": true
- },
- "slice-ansi": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
- "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
- "dev": true,
- "requires": {
- "ansi-styles": "^4.0.0",
- "astral-regex": "^2.0.0",
- "is-fullwidth-code-point": "^3.0.0"
- }
- },
- "sprintf-js": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
- "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
- "dev": true
- },
- "string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
- "requires": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- }
- },
- "string.prototype.trim": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz",
- "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1"
- }
- },
- "string.prototype.trimend": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz",
- "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1"
- }
- },
- "string.prototype.trimstart": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz",
- "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1"
- }
- },
- "strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "requires": {
- "ansi-regex": "^5.0.1"
- }
- },
- "strip-bom": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
- "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
- "dev": true
- },
- "strip-json-comments": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
- "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
- "dev": true
- },
- "supports-color": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "dev": true,
- "requires": {
- "has-flag": "^4.0.0"
- }
- },
- "supports-preserve-symlinks-flag": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
- "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
- "dev": true
- },
- "table": {
- "version": "6.8.1",
- "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
- "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==",
- "dev": true,
- "requires": {
- "ajv": "^8.0.1",
- "lodash.truncate": "^4.4.2",
- "slice-ansi": "^4.0.0",
- "string-width": "^4.2.3",
- "strip-ansi": "^6.0.1"
- },
- "dependencies": {
- "ajv": {
- "version": "8.12.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
- "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
- "dev": true,
- "requires": {
- "fast-deep-equal": "^3.1.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2",
- "uri-js": "^4.2.2"
- }
- },
- "json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "dev": true
- }
- }
- },
- "text-table": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
- "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
- "dev": true
- },
- "tiny-invariant": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
- "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==",
- "dev": true
- },
- "tiny-warning": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
- "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
- "dev": true
- },
- "to-regex-range": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
- "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
- "requires": {
- "is-number": "^7.0.0"
- }
- },
- "tsconfig-paths": {
- "version": "3.14.2",
- "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
- "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==",
- "dev": true,
- "requires": {
- "@types/json5": "^0.0.29",
- "json5": "^1.0.2",
- "minimist": "^1.2.6",
- "strip-bom": "^3.0.0"
- }
- },
- "tslib": {
- "version": "2.6.2",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
- "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
- },
- "tsutils": {
- "version": "3.21.0",
- "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
- "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
- "dev": true,
- "requires": {
- "tslib": "^1.8.1"
- },
- "dependencies": {
- "tslib": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
- "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
- "dev": true
- }
- }
- },
- "type-check": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
- "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
- "dev": true,
- "requires": {
- "prelude-ls": "^1.2.1"
- }
- },
- "type-fest": {
- "version": "0.20.2",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
- "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
- "dev": true
- },
- "typed-array-buffer": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz",
- "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "get-intrinsic": "^1.2.1",
- "is-typed-array": "^1.1.10"
- }
- },
- "typed-array-byte-length": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz",
- "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "for-each": "^0.3.3",
- "has-proto": "^1.0.1",
- "is-typed-array": "^1.1.10"
- }
- },
- "typed-array-byte-offset": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz",
- "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==",
- "dev": true,
- "requires": {
- "available-typed-arrays": "^1.0.5",
- "call-bind": "^1.0.2",
- "for-each": "^0.3.3",
- "has-proto": "^1.0.1",
- "is-typed-array": "^1.1.10"
- }
- },
- "typed-array-length": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz",
- "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "for-each": "^0.3.3",
- "is-typed-array": "^1.1.9"
- }
- },
- "typescript": {
- "version": "4.9.5",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
- "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
- "dev": true
- },
- "unbox-primitive": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
- "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "has-bigints": "^1.0.2",
- "has-symbols": "^1.0.3",
- "which-boxed-primitive": "^1.0.2"
- }
- },
- "uri-js": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
- "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
- "dev": true,
- "requires": {
- "punycode": "^2.1.0"
- }
- },
- "v8-compile-cache": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz",
- "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==",
- "dev": true
- },
- "value-equal": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
- "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==",
- "dev": true
- },
- "which": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
- "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "dev": true,
- "requires": {
- "isexe": "^2.0.0"
- }
- },
- "which-boxed-primitive": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
- "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
- "dev": true,
- "requires": {
- "is-bigint": "^1.0.1",
- "is-boolean-object": "^1.1.0",
- "is-number-object": "^1.0.4",
- "is-string": "^1.0.5",
- "is-symbol": "^1.0.3"
- }
- },
- "which-typed-array": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz",
- "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==",
- "dev": true,
- "requires": {
- "available-typed-arrays": "^1.0.5",
- "call-bind": "^1.0.4",
- "for-each": "^0.3.3",
- "gopd": "^1.0.1",
- "has-tostringtag": "^1.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==",
- "dev": true
- },
- "yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true
+ "license": "ISC"
}
}
}
diff --git a/packages/react-router/package.json b/packages/react-router/package.json
index ff4346b99ec..2f6dc1b8bf2 100644
--- a/packages/react-router/package.json
+++ b/packages/react-router/package.json
@@ -42,8 +42,8 @@
"peerDependencies": {
"react": ">=16.8.6",
"react-dom": ">=16.8.6",
- "react-router": "^5.0.1",
- "react-router-dom": "^5.0.1"
+ "react-router": ">=6.4.0 <7",
+ "react-router-dom": ">=6.4.0 <7"
},
"devDependencies": {
"@ionic/eslint-config": "^0.3.0",
@@ -52,16 +52,15 @@
"@types/node": "^14.0.14",
"@types/react": "^17.0.79",
"@types/react-dom": "^17.0.25",
- "@types/react-router": "^5.0.3",
- "@types/react-router-dom": "^5.1.5",
+ "history": "^5.3.0",
"@typescript-eslint/eslint-plugin": "^5.48.2",
"@typescript-eslint/parser": "^5.48.2",
"eslint": "^7.32.0",
"prettier": "^2.8.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
- "react-router": "^5.0.1",
- "react-router-dom": "^5.0.1",
+ "react-router": "^6.30.0",
+ "react-router-dom": "^6.30.0",
"rimraf": "^3.0.2",
"rollup": "^4.2.0",
"typescript": "^4.0.5"
diff --git a/packages/react-router/scripts/sync.sh b/packages/react-router/scripts/sync.sh
index b8f03de4cf2..c69a98efc99 100644
--- a/packages/react-router/scripts/sync.sh
+++ b/packages/react-router/scripts/sync.sh
@@ -2,14 +2,14 @@
set -e
-# Copy ionic react dist
-rm -rf node_modules/@ionic/react/dist node_modules/@ionic/react/css
-cp -a ../react/dist node_modules/@ionic/react/dist
-cp -a ../react/css node_modules/@ionic/react/css
-cp -a ../react/package.json node_modules/@ionic/react/package.json
-
-# Copy core dist
-rm -rf node_modules/@ionic/core/dist node_modules/@ionic/core/components
-cp -a ../../core/dist node_modules/@ionic/core/dist
-cp -a ../../core/components node_modules/@ionic/core/components
-cp -a ../../core/package.json node_modules/@ionic/core/package.json
+# Delete old packages
+rm -f *.tgz
+
+# Pack @ionic/react
+npm pack ../react
+
+# Pack @ionic/core
+npm pack ../../core
+
+# Install Dependencies
+npm install *.tgz --no-save
diff --git a/packages/react-router/scripts/test_runner.sh b/packages/react-router/scripts/test_runner.sh
new file mode 100755
index 00000000000..1a2cd5b837f
--- /dev/null
+++ b/packages/react-router/scripts/test_runner.sh
@@ -0,0 +1,76 @@
+#!/bin/bash
+set -x
+
+# Change to script's directory
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$SCRIPT_DIR"
+
+# Inside core
+echo "Building core..."
+cd ../../../core
+npm run build
+
+# Inside packages/react
+echo "Building packages/react..."
+cd ../packages/react
+npm ci
+npm run sync
+npm run build
+
+# Inside packages/react-router
+echo "Building packages/react-router..."
+cd ../react-router
+npm ci
+npm run sync
+npm run build
+
+# Inside packages/react-router/test
+echo "Building test app..."
+cd ./test
+rm -rf build/reactrouter6 || true
+sh ./build.sh reactrouter6
+cd build/reactrouter6
+echo "Installing dependencies..."
+npm install --legacy-peer-deps > npm_install.log 2>&1
+npm run sync
+
+# Install Playwright browsers if not already present
+npx playwright install chromium 2>/dev/null || true
+
+echo "Cleaning up port 3000..."
+lsof -ti:3000 | xargs kill -9 || true
+
+echo "Starting server..."
+# Start server in background and save PID
+npm start > server.log 2>&1 &
+SERVER_PID=$!
+
+# Ensure server is killed on script exit
+trap "kill $SERVER_PID" EXIT
+
+echo "Waiting for server to start..."
+# Poll until the server responds (up to 60s)
+for i in $(seq 1 60); do
+ if curl -s -o /dev/null -w "%{http_code}" http://localhost:3000 2>/dev/null | grep -q "200"; then
+ echo "Server is healthy."
+ break
+ fi
+ if [ "$i" -eq 60 ]; then
+ echo "Server did not start within 60s. Exiting."
+ cat server.log
+ exit 1
+ fi
+ sleep 1
+done
+
+echo "Running Cypress tests..."
+npm run cypress || CYPRESS_FAILED=1
+
+echo "Running Playwright tests..."
+npx playwright test --retries=2 || PLAYWRIGHT_FAILED=1
+
+if [ "${CYPRESS_FAILED:-0}" = "1" ] || [ "${PLAYWRIGHT_FAILED:-0}" = "1" ]; then
+ echo "One or more test suites failed."
+ exit 1
+fi
+
diff --git a/packages/react-router/src/ReactRouter/IonReactHashRouter.tsx b/packages/react-router/src/ReactRouter/IonReactHashRouter.tsx
index 7908a0f25ec..2c34d9f36e2 100644
--- a/packages/react-router/src/ReactRouter/IonReactHashRouter.tsx
+++ b/packages/react-router/src/ReactRouter/IonReactHashRouter.tsx
@@ -1,53 +1,55 @@
-import type { Action as HistoryAction, History, Location as HistoryLocation } from 'history';
-import { createHashHistory as createHistory } from 'history';
-import React from 'react';
-import type { BrowserRouterProps } from 'react-router-dom';
-import { Router } from 'react-router-dom';
+/**
+ * `IonReactHashRouter` provides a way to use hash-based routing in Ionic
+ * React applications.
+ */
+
+import type { Action as HistoryAction, Location as HistoryLocation } from 'history';
+import type { PropsWithChildren } from 'react';
+import React, { useEffect, useRef, useCallback } from 'react';
+import type { HashRouterProps } from 'react-router-dom';
+import { HashRouter, useLocation, useNavigationType } from 'react-router-dom';
import { IonRouter } from './IonRouter';
-interface IonReactHashRouterProps extends BrowserRouterProps {
- history?: History;
-}
+const RouterContent = ({ children }: PropsWithChildren<{}>) => {
+ const location = useLocation();
+ const navigationType = useNavigationType();
-export class IonReactHashRouter extends React.Component {
- history: History;
- historyListenHandler?: (location: HistoryLocation, action: HistoryAction) => void;
+ const historyListenHandler = useRef<(location: HistoryLocation, action: HistoryAction) => void>();
- constructor(props: IonReactHashRouterProps) {
- super(props);
- const { history, ...rest } = props;
- this.history = history || createHistory(rest);
- this.history.listen(this.handleHistoryChange.bind(this));
- this.registerHistoryListener = this.registerHistoryListener.bind(this);
- }
+ const registerHistoryListener = useCallback((cb: (location: HistoryLocation, action: HistoryAction) => void) => {
+ historyListenHandler.current = cb;
+ }, []);
/**
- * history@4.x passes separate location and action
- * params. history@5.x passes location and action
- * together as a single object.
- * TODO: If support for React Router <=5 is dropped
- * this logic is no longer needed. We can just assume
- * a single object with both location and action.
+ * Processes navigation changes within the application.
+ *
+ * Its purpose is to relay the current `location` and the associated
+ * `action` ('PUSH', 'POP', or 'REPLACE') to any registered listeners,
+ * primarily for `IonRouter` to manage Ionic-specific UI updates and
+ * navigation stack behavior.
+ *
+ * @param location The current browser history location object.
+ * @param action The type of navigation action ('PUSH', 'POP', or
+ * 'REPLACE').
*/
- handleHistoryChange(location: HistoryLocation, action: HistoryAction) {
- const locationValue = (location as any).location || location;
- const actionValue = (location as any).action || action;
- if (this.historyListenHandler) {
- this.historyListenHandler(locationValue, actionValue);
+ const handleHistoryChange = useCallback((loc: HistoryLocation, act: HistoryAction) => {
+ if (historyListenHandler.current) {
+ historyListenHandler.current(loc, act);
}
- }
-
- registerHistoryListener(cb: (location: HistoryLocation, action: HistoryAction) => void) {
- this.historyListenHandler = cb;
- }
-
- render() {
- const { children, ...props } = this.props;
- return (
-
- {children}
-
- );
- }
-}
+ }, []);
+
+ useEffect(() => {
+ handleHistoryChange(location, navigationType);
+ }, [location, navigationType, handleHistoryChange]);
+
+ return {children} ;
+};
+
+export const IonReactHashRouter = ({ children, ...routerProps }: PropsWithChildren) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/react-router/src/ReactRouter/IonReactMemoryRouter.tsx b/packages/react-router/src/ReactRouter/IonReactMemoryRouter.tsx
index c3bf8d74bbc..2884a7bb73e 100644
--- a/packages/react-router/src/ReactRouter/IonReactMemoryRouter.tsx
+++ b/packages/react-router/src/ReactRouter/IonReactMemoryRouter.tsx
@@ -1,51 +1,56 @@
-import type { Action as HistoryAction, Location as HistoryLocation, MemoryHistory } from 'history';
-import React from 'react';
+/**
+ * `IonReactMemoryRouter` provides a way to use `react-router` in
+ * environments where a traditional browser history (like `BrowserRouter`)
+ * isn't available or desirable.
+ */
+
+import type { Action as HistoryAction, Location as HistoryLocation } from 'history';
+import type { PropsWithChildren } from 'react';
+import React, { useEffect, useRef, useCallback } from 'react';
import type { MemoryRouterProps } from 'react-router';
-import { Router } from 'react-router';
+import { MemoryRouter, useLocation, useNavigationType } from 'react-router';
import { IonRouter } from './IonRouter';
-interface IonReactMemoryRouterProps extends MemoryRouterProps {
- history: MemoryHistory;
-}
+const RouterContent = ({ children }: PropsWithChildren<{}>) => {
+ const location = useLocation();
+ const navigationType = useNavigationType();
-export class IonReactMemoryRouter extends React.Component {
- history: MemoryHistory;
- historyListenHandler?: (location: HistoryLocation, action: HistoryAction) => void;
+ const historyListenHandler = useRef<(location: HistoryLocation, action: HistoryAction) => void>();
- constructor(props: IonReactMemoryRouterProps) {
- super(props);
- this.history = props.history;
- this.history.listen(this.handleHistoryChange.bind(this));
- this.registerHistoryListener = this.registerHistoryListener.bind(this);
- }
+ const registerHistoryListener = useCallback((cb: (location: HistoryLocation, action: HistoryAction) => void) => {
+ historyListenHandler.current = cb;
+ }, []);
/**
- * history@4.x passes separate location and action
- * params. history@5.x passes location and action
- * together as a single object.
- * TODO: If support for React Router <=5 is dropped
- * this logic is no longer needed. We can just assume
- * a single object with both location and action.
+ * Processes navigation changes within the application.
+ *
+ * Its purpose is to relay the current `location` and the associated
+ * `action` ('PUSH', 'POP', or 'REPLACE') to any registered listeners,
+ * primarily for `IonRouter` to manage Ionic-specific UI updates and
+ * navigation stack behavior.
+ *
+ * @param location The current browser history location object.
+ * @param action The type of navigation action ('PUSH', 'POP', or
+ * 'REPLACE').
*/
- handleHistoryChange(location: HistoryLocation, action: HistoryAction) {
- const locationValue = (location as any).location || location;
- const actionValue = (location as any).action || action;
- if (this.historyListenHandler) {
- this.historyListenHandler(locationValue, actionValue);
+ const handleHistoryChange = useCallback((loc: HistoryLocation, act: HistoryAction) => {
+ if (historyListenHandler.current) {
+ historyListenHandler.current(loc, act);
}
- }
-
- registerHistoryListener(cb: (location: HistoryLocation, action: HistoryAction) => void) {
- this.historyListenHandler = cb;
- }
-
- render() {
- const { children, ...props } = this.props;
- return (
-
- {children}
-
- );
- }
-}
+ }, []);
+
+ useEffect(() => {
+ handleHistoryChange(location, navigationType);
+ }, [location, navigationType, handleHistoryChange]);
+
+ return {children} ;
+};
+
+export const IonReactMemoryRouter = ({ children, ...routerProps }: PropsWithChildren) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/react-router/src/ReactRouter/IonReactRouter.tsx b/packages/react-router/src/ReactRouter/IonReactRouter.tsx
index 0bde5599165..f34a5b5d118 100644
--- a/packages/react-router/src/ReactRouter/IonReactRouter.tsx
+++ b/packages/react-router/src/ReactRouter/IonReactRouter.tsx
@@ -1,53 +1,65 @@
-import type { Action as HistoryAction, History, Location as HistoryLocation } from 'history';
-import { createBrowserHistory as createHistory } from 'history';
-import React from 'react';
+/**
+ * `IonReactRouter` facilitates the integration of Ionic's specific
+ * navigation and UI management with the standard React Router mechanisms,
+ * allowing an inner Ionic-specific router (`IonRouter`) to react to
+ * navigation events.
+ */
+
+import type { Action as HistoryAction, Location as HistoryLocation } from 'history';
+import type { PropsWithChildren } from 'react';
+import React, { useEffect, useRef, useCallback } from 'react';
import type { BrowserRouterProps } from 'react-router-dom';
-import { Router } from 'react-router-dom';
+import { BrowserRouter, useLocation, useNavigationType } from 'react-router-dom';
import { IonRouter } from './IonRouter';
-interface IonReactRouterProps extends BrowserRouterProps {
- history?: History;
-}
+/**
+ * This component acts as a bridge to ensure React Router hooks like
+ * `useLocation` and `useNavigationType` are called within the valid
+ * context of a ``.
+ *
+ * It was split from `IonReactRouter` because these hooks must be
+ * descendants of a `` component, which `BrowserRouter` provides.
+ */
+const RouterContent = ({ children }: PropsWithChildren<{}>) => {
+ const location = useLocation();
+ const navigationType = useNavigationType();
-export class IonReactRouter extends React.Component {
- historyListenHandler?: (location: HistoryLocation, action: HistoryAction) => void;
- history: History;
+ const historyListenHandler = useRef<(location: HistoryLocation, action: HistoryAction) => void>();
- constructor(props: IonReactRouterProps) {
- super(props);
- const { history, ...rest } = props;
- this.history = history || createHistory(rest);
- this.history.listen(this.handleHistoryChange.bind(this));
- this.registerHistoryListener = this.registerHistoryListener.bind(this);
- }
+ const registerHistoryListener = useCallback((cb: (location: HistoryLocation, action: HistoryAction) => void) => {
+ historyListenHandler.current = cb;
+ }, []);
/**
- * history@4.x passes separate location and action
- * params. history@5.x passes location and action
- * together as a single object.
- * TODO: If support for React Router <=5 is dropped
- * this logic is no longer needed. We can just assume
- * a single object with both location and action.
+ * Processes navigation changes within the application.
+ *
+ * Its purpose is to relay the current `location` and the associated
+ * `action` ('PUSH', 'POP', or 'REPLACE') to any registered listeners,
+ * primarily for `IonRouter` to manage Ionic-specific UI updates and
+ * navigation stack behavior.
+ *
+ * @param loc The current browser history location object.
+ * @param act The type of navigation action ('PUSH', 'POP', or
+ * 'REPLACE').
*/
- handleHistoryChange(location: HistoryLocation, action: HistoryAction) {
- const locationValue = (location as any).location || location;
- const actionValue = (location as any).action || action;
- if (this.historyListenHandler) {
- this.historyListenHandler(locationValue, actionValue);
+ const handleHistoryChange = useCallback((loc: HistoryLocation, act: HistoryAction) => {
+ if (historyListenHandler.current) {
+ historyListenHandler.current(loc, act);
}
- }
-
- registerHistoryListener(cb: (location: HistoryLocation, action: HistoryAction) => void) {
- this.historyListenHandler = cb;
- }
-
- render() {
- const { children, ...props } = this.props;
- return (
-
- {children}
-
- );
- }
-}
+ }, []);
+
+ useEffect(() => {
+ handleHistoryChange(location, navigationType);
+ }, [location, navigationType, handleHistoryChange]);
+
+ return {children} ;
+};
+
+export const IonReactRouter = ({ children, ...browserRouterProps }: PropsWithChildren) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/react-router/src/ReactRouter/IonRouteInner.tsx b/packages/react-router/src/ReactRouter/IonRouteInner.tsx
index ddeb0e74d31..8fea15b50d8 100644
--- a/packages/react-router/src/ReactRouter/IonRouteInner.tsx
+++ b/packages/react-router/src/ReactRouter/IonRouteInner.tsx
@@ -1,30 +1,7 @@
import type { IonRouteProps } from '@ionic/react';
import React from 'react';
-import { Route } from 'react-router';
+import { Route } from 'react-router-dom';
-export class IonRouteInner extends React.PureComponent {
- render() {
- return (
-
- );
- }
-}
+export const IonRouteInner = ({ path, index, caseSensitive, element }: IonRouteProps) => {
+ return ;
+};
diff --git a/packages/react-router/src/ReactRouter/IonRouter.tsx b/packages/react-router/src/ReactRouter/IonRouter.tsx
index fcadd6561a6..f185358c55c 100644
--- a/packages/react-router/src/ReactRouter/IonRouter.tsx
+++ b/packages/react-router/src/ReactRouter/IonRouter.tsx
@@ -1,182 +1,403 @@
+/**
+ * `IonRouter` is responsible for managing the application's navigation
+ * state, tracking the history of visited routes, and coordinating
+ * transitions between different views. It intercepts route changes from
+ * React Router and translates them into actions that Ionic can understand
+ * and animate.
+ */
+
import type {
AnimationBuilder,
RouteAction,
RouteInfo,
RouteManagerContextState,
RouterDirection,
- ViewItem,
+ RouterOptions,
} from '@ionic/react';
import { LocationHistory, NavManager, RouteManagerContext, generateId, getConfig } from '@ionic/react';
-import type { Action as HistoryAction, Location as HistoryLocation } from 'history';
-import React from 'react';
-import type { RouteComponentProps } from 'react-router-dom';
-import { withRouter } from 'react-router-dom';
+import type { Action as HistoryAction, Location } from 'history';
+import type { PropsWithChildren } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
+import { useLocation, useNavigate } from 'react-router-dom';
import { IonRouteInner } from './IonRouteInner';
import { ReactRouterViewStack } from './ReactRouterViewStack';
import StackManager from './StackManager';
+// Use Location directly - state is typed as `unknown` in history v5
+type HistoryLocation = Location;
+
export interface LocationState {
direction?: RouterDirection;
- routerOptions?: { as?: string; unmount?: boolean };
+ routerOptions?: RouterOptions;
}
-interface IonRouteProps extends RouteComponentProps<{}, {}, LocationState> {
- registerHistoryListener: (cb: (location: HistoryLocation, action: HistoryAction) => void) => void;
+interface IonRouterProps {
+ registerHistoryListener: (cb: (location: HistoryLocation, action: HistoryAction) => void) => void;
}
-interface IonRouteState {
- routeInfo: RouteInfo;
-}
+type RouteParams = Record;
+type SafeRouteParams = Record;
-class IonRouterInner extends React.PureComponent {
- currentTab?: string;
- exitViewFromOtherOutletHandlers: ((pathname: string) => ViewItem | undefined)[] = [];
- incomingRouteParams?: Partial;
- locationHistory = new LocationHistory();
- viewStack = new ReactRouterViewStack();
- routeMangerContextState: RouteManagerContextState = {
- canGoBack: () => this.locationHistory.canGoBack(),
- clearOutlet: this.viewStack.clear,
- findViewItemByPathname: this.viewStack.findViewItemByPathname,
- getChildrenToRender: this.viewStack.getChildrenToRender,
- goBack: () => this.handleNavigateBack(),
- createViewItem: this.viewStack.createViewItem,
- findViewItemByRouteInfo: this.viewStack.findViewItemByRouteInfo,
- findLeavingViewItemByRouteInfo: this.viewStack.findLeavingViewItemByRouteInfo,
- addViewItem: this.viewStack.add,
- unMountViewItem: this.viewStack.remove,
- };
+const filterUndefinedParams = (params: RouteParams): SafeRouteParams => {
+ const result: SafeRouteParams = {};
+ for (const key of Object.keys(params)) {
+ const value = params[key];
+ if (value !== undefined) {
+ result[key] = value;
+ }
+ }
+ return result;
+};
- constructor(props: IonRouteProps) {
- super(props);
+/**
+ * Checks if a POP event is a multi-step back navigation (navigate(-n) where n > 1).
+ * Walks the pushedByRoute chain from prevInfo to verify the destination is an ancestor
+ * in the same navigation chain. This distinguishes multi-step back from tab-crossing
+ * back navigation where prevInfo.pathname also differs from the browser destination.
+ */
+const checkIsMultiStepBack = (
+ prevInfo: RouteInfo | undefined,
+ destinationPathname: string,
+ history: LocationHistory
+): boolean => {
+ if (!prevInfo || prevInfo.pathname === destinationPathname) return false;
+ const visited = new Set();
+ let walker: RouteInfo | undefined = prevInfo;
+ while (walker?.pushedByRoute) {
+ if (visited.has(walker.id)) break; // cycle guard
+ visited.add(walker.id);
+ if (walker.pushedByRoute === destinationPathname) return true;
+ walker = history.findLastLocation(walker);
+ }
+ return false;
+};
- const routeInfo = {
- id: generateId('routeInfo'),
- pathname: this.props.location.pathname,
- search: this.props.location.search,
- };
+const areParamsEqual = (a?: RouteParams, b?: RouteParams) => {
+ const paramsA = a || {};
+ const paramsB = b || {};
+ const keysA = Object.keys(paramsA);
+ const keysB = Object.keys(paramsB);
- this.locationHistory.add(routeInfo);
- this.handleChangeTab = this.handleChangeTab.bind(this);
- this.handleResetTab = this.handleResetTab.bind(this);
- this.handleNativeBack = this.handleNativeBack.bind(this);
- this.handleNavigate = this.handleNavigate.bind(this);
- this.handleNavigateBack = this.handleNavigateBack.bind(this);
- this.props.registerHistoryListener(this.handleHistoryChange.bind(this));
- this.handleSetCurrentTab = this.handleSetCurrentTab.bind(this);
-
- this.state = {
- routeInfo,
- };
+ if (keysA.length !== keysB.length) {
+ return false;
}
- handleChangeTab(tab: string, path?: string, routeOptions?: any) {
- if (!path) {
+ return keysA.every((key) => {
+ const valueA = paramsA[key];
+ const valueB = paramsB[key];
+ if (Array.isArray(valueA) && Array.isArray(valueB)) {
+ if (valueA.length !== valueB.length) {
+ return false;
+ }
+ return valueA.every((entry, idx) => entry === valueB[idx]);
+ }
+ return valueA === valueB;
+ });
+};
+
+export const IonRouter = ({ children, registerHistoryListener }: PropsWithChildren) => {
+ const location = useLocation();
+ const navigate = useNavigate();
+
+ const didMountRef = useRef(false);
+ const locationHistory = useRef(new LocationHistory());
+ const currentTab = useRef(undefined);
+ const viewStack = useRef(new ReactRouterViewStack());
+ const incomingRouteParams = useRef | null>(null);
+ /**
+ * Tracks location keys that the user navigated away from via browser back.
+ * When a POP event's destination key matches the top of this stack, it's a
+ * browser forward navigation. Uses React Router's unique location.key
+ * instead of URLs to correctly handle duplicate URLs in history (e.g.,
+ * navigating to /details, then /settings, then /details via routerLink,
+ * then pressing back).
+ * Cleared on PUSH (new navigation invalidates forward history).
+ */
+ const forwardStack = useRef([]);
+ /**
+ * Tracks the current location key so we can push it onto the forward stack
+ * when navigating back. Updated after each history change.
+ */
+ const currentLocationKeyRef = useRef(location.key);
+
+ const [routeInfo, setRouteInfo] = useState({
+ id: generateId('routeInfo'),
+ pathname: location.pathname,
+ search: location.search,
+ params: {},
+ });
+
+ useEffect(() => {
+ if (didMountRef.current) {
return;
}
- const routeInfo = this.locationHistory.getCurrentRouteInfoForTab(tab);
- const [pathname, search] = path.split('?');
- if (routeInfo) {
- this.incomingRouteParams = { ...routeInfo, routeAction: 'push', routeDirection: 'none' };
- if (routeInfo.pathname === pathname) {
- this.incomingRouteParams.routeOptions = routeOptions;
- this.props.history.push(routeInfo.pathname + (routeInfo.search || ''));
- } else {
- this.incomingRouteParams.pathname = pathname;
- this.incomingRouteParams.search = search ? '?' + search : undefined;
- this.incomingRouteParams.routeOptions = routeOptions;
- this.props.history.push(pathname + (search ? '?' + search : ''));
+ // Seed the history stack with the initial location and begin listening
+ // for future navigations once React has committed the mount. This avoids
+ // duplicate entries when React StrictMode runs an extra render pre-commit.
+ locationHistory.current.add(routeInfo);
+
+ // If IonTabBar already called handleSetCurrentTab during render (before this
+ // effect), the tab was stored in currentTab.current but the history entry was
+ // not yet seeded. Apply the pending tab to the seed entry now.
+ if (currentTab.current) {
+ const ri = { ...locationHistory.current.current() };
+ if (ri.tab !== currentTab.current) {
+ ri.tab = currentTab.current;
+ locationHistory.current.update(ri);
}
- } else {
- this.handleNavigate(pathname, 'push', 'none', undefined, routeOptions, tab);
}
- }
- handleHistoryChange(location: HistoryLocation, action: HistoryAction) {
+ registerHistoryListener(handleHistoryChange);
+
+ didMountRef.current = true;
+ }, []);
+
+ // Sync route params extracted by React Router's path matching back into routeInfo.
+ // The view stack's match may contain params (e.g., :id) not present in the initial routeInfo.
+ useEffect(() => {
+ const activeView = viewStack.current.findViewItemByRouteInfo(routeInfo, undefined, true);
+ const matchedParams = activeView?.routeData.match?.params as RouteParams | undefined;
+
+ if (matchedParams) {
+ const paramsCopy = filterUndefinedParams({ ...matchedParams });
+ if (areParamsEqual(routeInfo.params as RouteParams | undefined, paramsCopy)) {
+ return;
+ }
+
+ const updatedRouteInfo: RouteInfo = {
+ ...routeInfo,
+ params: paramsCopy,
+ };
+ locationHistory.current.update(updatedRouteInfo);
+ setRouteInfo(updatedRouteInfo);
+ }
+ }, [routeInfo]);
+
+ /**
+ * Triggered whenever the history changes, either through user navigation
+ * or programmatic changes. It transforms the raw browser history changes
+ * into `RouteInfo` objects, which are needed Ionic's animations and
+ * navigation patterns.
+ *
+ * @param location The current location object from the history.
+ * @param action The action that triggered the history change.
+ */
+ const handleHistoryChange = (location: HistoryLocation, action: HistoryAction) => {
let leavingLocationInfo: RouteInfo;
- if (this.incomingRouteParams) {
- if (this.incomingRouteParams.routeAction === 'replace') {
- leavingLocationInfo = this.locationHistory.previous();
+ /**
+ * A programmatic navigation was triggered.
+ * e.g., ` `, `navigate()`, or `handleNavigate()`
+ */
+ if (incomingRouteParams.current) {
+ /**
+ * The current history entry is overwritten, so the previous entry
+ * is the one we are leaving.
+ */
+ if (incomingRouteParams.current?.routeAction === 'replace') {
+ leavingLocationInfo = locationHistory.current.previous();
} else {
- leavingLocationInfo = this.locationHistory.current();
+ // If the action is 'push' or 'pop', we want to use the current route.
+ leavingLocationInfo = locationHistory.current.current();
}
} else {
- leavingLocationInfo = this.locationHistory.current();
+ /**
+ * An external navigation was triggered
+ * e.g., browser back/forward button or direct link
+ *
+ * The leaving location is the current route.
+ */
+ leavingLocationInfo = locationHistory.current.current();
}
const leavingUrl = leavingLocationInfo.pathname + leavingLocationInfo.search;
- if (leavingUrl !== location.pathname) {
- if (!this.incomingRouteParams) {
+ if (leavingUrl !== location.pathname + location.search) {
+ if (!incomingRouteParams.current) {
+ // Use history-based tab detection instead of URL-pattern heuristics,
+ // so tab routes work with any URL structure (not just paths containing "/tabs").
+ // Fall back to currentTab.current only when the destination is within the
+ // current tab's path hierarchy (prevents non-tab routes from inheriting a tab).
+ let tabToUse = locationHistory.current.findTabForPathname(location.pathname);
+ if (!tabToUse && currentTab.current) {
+ const tabFirstRoute = locationHistory.current.getFirstRouteInfoForTab(currentTab.current);
+ const tabRootPath = tabFirstRoute?.pathname;
+ if (tabRootPath && (location.pathname === tabRootPath || location.pathname.startsWith(tabRootPath + '/'))) {
+ tabToUse = currentTab.current;
+ }
+ }
+
+ /**
+ * A `REPLACE` action can be triggered by React Router's
+ * ` ` component.
+ */
if (action === 'REPLACE') {
- this.incomingRouteParams = {
+ incomingRouteParams.current = {
routeAction: 'replace',
routeDirection: 'none',
- tab: this.currentTab,
+ tab: tabToUse,
};
}
+ /**
+ * A `POP` action can be triggered by the browser's back/forward
+ * button. Both fire as POP events, so we use a forward stack to
+ * distinguish them: when going back, we push the leaving pathname
+ * onto the stack. When the next POP's destination matches the top
+ * of the stack, it's a forward navigation.
+ */
if (action === 'POP') {
- const currentRoute = this.locationHistory.current();
- if (currentRoute && currentRoute.pushedByRoute) {
- const prevInfo = this.locationHistory.findLastLocation(currentRoute);
- this.incomingRouteParams = { ...prevInfo, routeAction: 'pop', routeDirection: 'back' };
+ const currentRoute = locationHistory.current.current();
+ const isForwardNavigation =
+ forwardStack.current.length > 0 &&
+ forwardStack.current[forwardStack.current.length - 1] === location.key;
+
+ if (isForwardNavigation) {
+ forwardStack.current.pop();
+ incomingRouteParams.current = {
+ routeAction: 'push',
+ routeDirection: 'forward',
+ tab: tabToUse,
+ };
+ } else if (currentRoute && currentRoute.pushedByRoute) {
+ // Back navigation. Record current location key for potential forward
+ forwardStack.current.push(currentLocationKeyRef.current);
+ const prevInfo = locationHistory.current.findLastLocation(currentRoute);
+
+ const isMultiStepBack = checkIsMultiStepBack(prevInfo, location.pathname, locationHistory.current);
+
+ if (isMultiStepBack) {
+ const destinationInfo = locationHistory.current.findLastLocationByPathname(location.pathname);
+ incomingRouteParams.current = {
+ ...(destinationInfo || {}),
+ routeAction: 'pop',
+ routeDirection: 'back',
+ };
+ } else if (prevInfo && prevInfo.pathname !== location.pathname && currentRoute.tab) {
+ // Browser POP destination differs from within-tab back target.
+ // Sync URL via replace, like handleNavigateBack's non-linear path (#25141).
+ incomingRouteParams.current = { ...prevInfo, routeAction: 'pop', routeDirection: 'back' };
+ forwardStack.current = [];
+ handleNavigate(prevInfo.pathname + (prevInfo.search || ''), 'pop', 'back', undefined, undefined, prevInfo.tab);
+ return;
+ } else {
+ incomingRouteParams.current = { ...prevInfo, routeAction: 'pop', routeDirection: 'back' };
+ }
} else {
- this.incomingRouteParams = {
+ // It's a non-linear history path like a direct link.
+ // Still push the current location key so browser forward is detectable.
+ forwardStack.current.push(currentLocationKeyRef.current);
+ incomingRouteParams.current = {
routeAction: 'pop',
routeDirection: 'none',
- tab: this.currentTab,
+ tab: tabToUse,
};
}
}
- if (!this.incomingRouteParams) {
- this.incomingRouteParams = {
+ if (!incomingRouteParams.current) {
+ const state = location.state as LocationState | null;
+ incomingRouteParams.current = {
routeAction: 'push',
- routeDirection: location.state?.direction || 'forward',
- routeOptions: location.state?.routerOptions,
- tab: this.currentTab,
+ routeDirection: state?.direction || 'forward',
+ routeOptions: state?.routerOptions,
+ tab: tabToUse,
};
}
}
+ // New navigation (PUSH) invalidates browser forward history,
+ // so clear our forward stack to stay in sync.
+ if (action === 'PUSH') {
+ forwardStack.current = [];
+ }
+
let routeInfo: RouteInfo;
- if (this.incomingRouteParams?.id) {
+ // If we're navigating away from tabs to a non-tab route, clear the current tab
+ if (!locationHistory.current.findTabForPathname(location.pathname) && currentTab.current) {
+ currentTab.current = undefined;
+ }
+
+ /**
+ * An existing id indicates that it's re-activating an existing route.
+ * e.g., tab switching or navigating back to a previous route
+ */
+ if (incomingRouteParams.current?.id) {
routeInfo = {
- ...(this.incomingRouteParams as RouteInfo),
+ ...(incomingRouteParams.current as RouteInfo),
lastPathname: leavingLocationInfo.pathname,
};
- this.locationHistory.add(routeInfo);
+ locationHistory.current.add(routeInfo);
+ /**
+ * A new route is being created since it's not re-activating
+ * an existing route.
+ */
} else {
const isPushed =
- this.incomingRouteParams.routeAction === 'push' && this.incomingRouteParams.routeDirection === 'forward';
+ incomingRouteParams.current?.routeAction === 'push' &&
+ incomingRouteParams.current.routeDirection === 'forward';
routeInfo = {
id: generateId('routeInfo'),
- ...this.incomingRouteParams,
- lastPathname: leavingLocationInfo.pathname,
- pathname: location.pathname,
+ ...incomingRouteParams.current,
+ lastPathname: leavingLocationInfo.pathname, // The URL we just came from
+ pathname: location.pathname, // The current (destination) URL
search: location.search,
- params: this.props.match.params,
+ params: incomingRouteParams.current?.params
+ ? filterUndefinedParams(incomingRouteParams.current.params as RouteParams)
+ : {},
prevRouteLastPathname: leavingLocationInfo.lastPathname,
};
if (isPushed) {
- routeInfo.tab = leavingLocationInfo.tab;
+ // Only inherit tab from leaving route if we don't already have one.
+ // This preserves tab context for same-tab navigation while allowing cross-tab navigation.
+ routeInfo.tab = routeInfo.tab || leavingLocationInfo.tab;
+ routeInfo.pushedByRoute = leavingLocationInfo.pathname;
+ } else if (
+ routeInfo.routeAction === 'push' &&
+ routeInfo.routeDirection === 'none' &&
+ routeInfo.tab === leavingLocationInfo.tab
+ ) {
+ // Push with routerDirection="none" within the same tab (or non-tab) context.
+ // Still needs pushedByRoute so the back button can navigate back correctly.
+ // Cross-tab navigations with direction "none" are handled by the tab-switching
+ // block below which has different pushedByRoute semantics.
+ routeInfo.tab = routeInfo.tab || leavingLocationInfo.tab;
routeInfo.pushedByRoute = leavingLocationInfo.pathname;
} else if (routeInfo.routeAction === 'pop') {
- const r = this.locationHistory.findLastLocation(routeInfo);
+ // Triggered by a browser back button or handleNavigateBack.
+ // Find the route that pushed this one.
+ const r = locationHistory.current.findLastLocation(routeInfo);
routeInfo.pushedByRoute = r?.pushedByRoute;
+ // Navigating to a new tab.
} else if (routeInfo.routeAction === 'push' && routeInfo.tab !== leavingLocationInfo.tab) {
- // If we are switching tabs grab the last route info for the tab and use its pushedByRoute
- const lastRoute = this.locationHistory.getCurrentRouteInfoForTab(routeInfo.tab);
- routeInfo.pushedByRoute = lastRoute?.pushedByRoute;
+ /**
+ * If we are switching tabs grab the last route info for the
+ * tab and use its `pushedByRoute`.
+ */
+ const lastRoute = locationHistory.current.getCurrentRouteInfoForTab(routeInfo.tab);
+ /**
+ * Tab bar switches (direction 'none') should not create cross-tab back
+ * navigation. Only inherit pushedByRoute from the tab's own history.
+ */
+ if (routeInfo.routeDirection === 'none') {
+ routeInfo.pushedByRoute = lastRoute?.pushedByRoute;
+ } else {
+ routeInfo.pushedByRoute = lastRoute?.pushedByRoute ?? leavingLocationInfo.pathname;
+ }
+ // Triggered by `navigate()` with replace or a ` ` component, etc.
} else if (routeInfo.routeAction === 'replace') {
- // Make sure to set the lastPathname, etc.. to the current route so the page transitions out
- const currentRouteInfo = this.locationHistory.current();
+ /**
+ * Make sure to set the `lastPathname`, etc.. to the current route
+ * so the page transitions out.
+ */
+ const currentRouteInfo = locationHistory.current.current();
/**
- * If going from /home to /child, then replacing from
- * /child to /home, we don't want the route info to
- * say that /home was pushed by /home which is not correct.
+ * Special handling for `replace` to ensure correct `pushedByRoute`
+ * and `lastPathname`.
+ *
+ * If going from `/home` to `/child`, then replacing from
+ * `/child` to `/home`, we don't want the route info to
+ * say that `/home` was pushed by `/home` which is not correct.
*/
const currentPushedBy = currentRouteInfo?.pushedByRoute;
const pushedByRoute =
@@ -198,58 +419,140 @@ class IonRouterInner extends React.PureComponent {
routeInfo.routeAnimation = routeInfo.routeAnimation || currentRouteInfo?.routeAnimation;
}
- this.locationHistory.add(routeInfo);
+ locationHistory.current.add(routeInfo);
}
-
- this.setState({
- routeInfo,
- });
+ setRouteInfo(routeInfo);
}
- this.incomingRouteParams = undefined;
- }
+ // Update the current location key after processing the history change.
+ // This ensures the forward stack records the correct key when navigating back.
+ currentLocationKeyRef.current = location.key;
+ incomingRouteParams.current = null;
+ };
/**
- * history@4.x uses goBack(), history@5.x uses back()
- * TODO: If support for React Router <=5 is dropped
- * this logic is no longer needed. We can just
- * assume back() is available.
+ * Resets the specified tab to its initial, root route.
+ *
+ * @param tab The tab to reset.
+ * @param originalHref The original href for the tab.
+ * @param originalRouteOptions The original route options for the tab.
*/
- handleNativeBack() {
- const history = this.props.history as any;
- const goBack = history.goBack || history.back;
- goBack();
- }
+ const handleResetTab = (tab: string, originalHref: string, originalRouteOptions: any) => {
+ const routeInfo = locationHistory.current.getFirstRouteInfoForTab(tab);
+ if (routeInfo) {
+ const [pathname, search] = originalHref.split('?');
+ const newRouteInfo = { ...routeInfo };
+ newRouteInfo.pathname = pathname;
+ newRouteInfo.search = search ? '?' + search : '';
+ newRouteInfo.routeOptions = originalRouteOptions;
+ incomingRouteParams.current = { ...newRouteInfo, routeAction: 'pop', routeDirection: 'back' };
+ navigate(newRouteInfo.pathname + (newRouteInfo.search || ''));
+ }
+ };
- handleNavigate(
- path: string,
- routeAction: RouteAction,
- routeDirection?: RouterDirection,
- routeAnimation?: AnimationBuilder,
- routeOptions?: any,
- tab?: string
- ) {
- this.incomingRouteParams = Object.assign(this.incomingRouteParams || {}, {
- routeAction,
- routeDirection,
- routeOptions,
- routeAnimation,
- tab,
- });
+ /**
+ * Handles tab changes.
+ *
+ * @param tab The tab to switch to.
+ * @param path The new path for the tab.
+ * @param routeOptions Additional route options.
+ */
+ const handleChangeTab = (tab: string, path?: string, routeOptions?: any) => {
+ if (!path) {
+ return;
+ }
+
+ const routeInfo = locationHistory.current.getCurrentRouteInfoForTab(tab);
+ const [pathname, search] = path.split('?');
+ // User has navigated to the current tab before.
+ if (routeInfo) {
+ const routeParams = {
+ ...routeInfo,
+ routeAction: 'push' as RouteAction,
+ routeDirection: 'none' as RouterDirection,
+ };
+ /**
+ * User is navigating to the same tab.
+ * e.g., `/tabs/home` β `/tabs/home`
+ */
+ if (routeInfo.pathname === pathname) {
+ const newSearch = search ? '?' + search : routeInfo.search;
+ incomingRouteParams.current = {
+ ...routeParams,
+ search: newSearch || '',
+ routeOptions,
+ };
+
+ navigate(routeInfo.pathname + (newSearch || ''));
+ /**
+ * User is navigating to a different tab.
+ * e.g., `/tabs/home` β `/tabs/settings`
+ */
+ } else {
+ incomingRouteParams.current = {
+ ...routeParams,
+ pathname,
+ search: search ? '?' + search : '',
+ routeOptions,
+ };
- if (routeAction === 'push') {
- this.props.history.push(path);
+ navigate(pathname + (search ? '?' + search : ''));
+ }
+ // User has not navigated to this tab before.
} else {
- this.props.history.replace(path);
+ const fullPath = pathname + (search ? '?' + search : '');
+ handleNavigate(fullPath, 'push', 'none', undefined, routeOptions, tab);
}
- }
+ };
- handleNavigateBack(defaultHref: string | RouteInfo = '/', routeAnimation?: AnimationBuilder) {
+ /**
+ * Set the current active tab in `locationHistory`.
+ * This is crucial for maintaining tab history since each tab has
+ * its own navigation stack.
+ *
+ * @param tab The tab to set as active.
+ */
+ const handleSetCurrentTab = (tab: string, _routeInfo?: RouteInfo) => {
+ currentTab.current = tab;
+ const current = locationHistory.current.current();
+ if (!current) {
+ // locationHistory not yet seeded (e.g., called during initial render
+ // before mount effect). The mount effect will seed the correct entry.
+ return;
+ }
+ const ri = { ...current };
+ if (ri.tab !== tab) {
+ ri.tab = tab;
+ locationHistory.current.update(ri);
+ }
+ };
+
+ /**
+ * Handles the native back button press.
+ * It's usually called when a user presses the platform-native back action.
+ */
+ const handleNativeBack = () => {
+ navigate(-1);
+ };
+
+ /**
+ * Used to manage the back navigation within the Ionic React's routing
+ * system. It's deeply integrated with Ionic's view lifecycle, animations,
+ * and its custom history tracking (`locationHistory`) to provide a
+ * native-like transition and maintain correct application state.
+ *
+ * @param defaultHref The fallback URL to navigate to if there's no
+ * previous entry in the `locationHistory` stack.
+ * @param routeAnimation A custom animation builder to override the
+ * default "back" animation.
+ */
+ const handleNavigateBack = (defaultHref: string | RouteInfo = '/', routeAnimation?: AnimationBuilder) => {
const config = getConfig();
defaultHref = defaultHref ? defaultHref : config && config.get('backButtonDefaultHref' as any);
- const routeInfo = this.locationHistory.current();
+ const routeInfo = locationHistory.current.current();
+ // It's a linear navigation.
if (routeInfo && routeInfo.pushedByRoute) {
- const prevInfo = this.locationHistory.findLastLocation(routeInfo);
+ const prevInfo = locationHistory.current.findLastLocation(routeInfo);
if (prevInfo) {
/**
* This needs to be passed to handleNavigate
@@ -257,84 +560,149 @@ class IonRouterInner extends React.PureComponent {
* will be overridden.
*/
const incomingAnimation = routeAnimation || routeInfo.routeAnimation;
- this.incomingRouteParams = {
+ incomingRouteParams.current = {
...prevInfo,
routeAction: 'pop',
routeDirection: 'back',
routeAnimation: incomingAnimation,
};
- if (
- routeInfo.lastPathname === routeInfo.pushedByRoute ||
- /**
- * We need to exclude tab switches/tab
- * context changes here because tabbed
- * navigation is not linear, but router.back()
- * will go back in a linear fashion.
- */
- (prevInfo.pathname === routeInfo.pushedByRoute && routeInfo.tab === '' && prevInfo.tab === '')
- ) {
+ /**
+ * Check if it's a simple linear back navigation (not tabbed).
+ * e.g., `/home` β `/settings` β back to `/home`
+ */
+ const condition1 = routeInfo.lastPathname === routeInfo.pushedByRoute;
+ const condition2 = prevInfo.pathname === routeInfo.pushedByRoute && !routeInfo.tab && !prevInfo.tab;
+ if (condition1 || condition2) {
+ // Record the current location key so browser forward is detectable
+ forwardStack.current.push(currentLocationKeyRef.current);
+ navigate(-1);
+ } else {
/**
- * history@4.x uses goBack(), history@5.x uses back()
- * TODO: If support for React Router <=5 is dropped
- * this logic is no longer needed. We can just
- * assume back() is available.
+ * It's a non-linear back navigation.
+ * e.g., direct link or tab switch or nested navigation with redirects
+ * Clear forward stack since the REPLACE-based navigate resets history
+ * position, making any prior forward entries unreachable.
*/
- const history = this.props.history as any;
- const goBack = history.goBack || history.back;
- goBack();
- } else {
- this.handleNavigate(prevInfo.pathname + (prevInfo.search || ''), 'pop', 'back', incomingAnimation);
+ forwardStack.current = [];
+ handleNavigate(prevInfo.pathname + (prevInfo.search || ''), 'pop', 'back', incomingAnimation);
}
+ /**
+ * `pushedByRoute` exists, but no corresponding previous entry in
+ * the history stack.
+ */
} else {
- this.handleNavigate(defaultHref as string, 'pop', 'back', routeAnimation);
+ handleNavigate(defaultHref as string, 'pop', 'back', routeAnimation);
}
+ /**
+ * No `pushedByRoute` (e.g., initial page load or tab root).
+ * Tabs with no back history should not navigate.
+ */
} else {
- this.handleNavigate(defaultHref as string, 'pop', 'back', routeAnimation);
+ if (routeInfo && routeInfo.tab) {
+ return;
+ }
+ handleNavigate(defaultHref as string, 'pop', 'back', routeAnimation);
}
- }
+ };
- handleResetTab(tab: string, originalHref: string, originalRouteOptions: any) {
- const routeInfo = this.locationHistory.getFirstRouteInfoForTab(tab);
- if (routeInfo) {
- const newRouteInfo = { ...routeInfo };
- newRouteInfo.pathname = originalHref;
- newRouteInfo.routeOptions = originalRouteOptions;
- this.incomingRouteParams = { ...newRouteInfo, routeAction: 'pop', routeDirection: 'back' };
- this.props.history.push(newRouteInfo.pathname + (newRouteInfo.search || ''));
- }
- }
+ /**
+ * Used to programmatically navigate through the app.
+ *
+ * @param path The path to navigate to.
+ * @param routeAction The action to take (push, replace, etc.).
+ * @param routeDirection The direction of the navigation (forward,
+ * back, etc.).
+ * @param routeAnimation The animation to use for the transition.
+ * @param routeOptions Additional options for the route.
+ * @param tab The tab to navigate to, if applicable.
+ */
+ const handleNavigate = (
+ path: string,
+ routeAction: RouteAction,
+ routeDirection?: RouterDirection,
+ routeAnimation?: AnimationBuilder,
+ routeOptions?: any,
+ tab?: string
+ ) => {
+ const normalizedRouteDirection =
+ routeAction === 'push' && routeDirection === undefined ? 'forward' : routeDirection;
- handleSetCurrentTab(tab: string) {
- this.currentTab = tab;
- const ri = { ...this.locationHistory.current() };
- if (ri.tab !== tab) {
- ri.tab = tab;
- this.locationHistory.update(ri);
+ // When navigating from tabs context, we need to determine if the destination
+ // is also within tabs. If not, we should clear the tab context.
+ let navigationTab = tab;
+
+ // If no explicit tab is provided and we're in a tab context,
+ // check if the destination path is outside of the current tab context.
+ // Uses history-based tab detection instead of URL pattern matching,
+ // so it works with any tab URL structure.
+ if (!tab && currentTab.current && path) {
+ // Check if destination was previously visited in a tab context
+ const destinationTab = locationHistory.current.findTabForPathname(path);
+ if (destinationTab) {
+ // Previously visited as a tab route - use the known tab
+ navigationTab = destinationTab;
+ } else {
+ // New destination - check if it's a child of the current tab's root path
+ const tabFirstRoute = locationHistory.current.getFirstRouteInfoForTab(currentTab.current);
+ if (tabFirstRoute) {
+ const tabRootPath = tabFirstRoute.pathname;
+ if (path === tabRootPath || path.startsWith(tabRootPath + '/')) {
+ // Still within the current tab's path hierarchy
+ navigationTab = currentTab.current;
+ } else {
+ // Destination is outside the current tab context
+ currentTab.current = undefined;
+ navigationTab = undefined;
+ }
+ }
+ }
}
- }
- render() {
- return (
-
-
- {this.props.children}
-
-
- );
- }
-}
+ const baseParams = incomingRouteParams.current ?? {};
+ incomingRouteParams.current = {
+ ...baseParams,
+ routeAction,
+ routeDirection: normalizedRouteDirection,
+ routeOptions,
+ routeAnimation,
+ tab: navigationTab,
+ };
+
+ navigate(path, { replace: routeAction !== 'push' });
+ };
+
+ const routeMangerContextValue: RouteManagerContextState = {
+ canGoBack: () => locationHistory.current.canGoBack(),
+ clearOutlet: viewStack.current.clear,
+ findViewItemByPathname: viewStack.current.findViewItemByPathname,
+ getChildrenToRender: viewStack.current.getChildrenToRender,
+ getViewItemsForOutlet: viewStack.current.getViewItemsForOutlet.bind(viewStack.current),
+ goBack: () => handleNavigateBack(),
+ createViewItem: viewStack.current.createViewItem,
+ findViewItemByRouteInfo: viewStack.current.findViewItemByRouteInfo,
+ findLeavingViewItemByRouteInfo: viewStack.current.findLeavingViewItemByRouteInfo,
+ addViewItem: viewStack.current.add,
+ unMountViewItem: viewStack.current.remove,
+ };
+
+ return (
+
+
+ {children}
+
+
+ );
+};
-export const IonRouter = withRouter(IonRouterInner);
IonRouter.displayName = 'IonRouter';
diff --git a/packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx b/packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx
index 92036790203..b823c458599 100644
--- a/packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx
+++ b/packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx
@@ -1,186 +1,1024 @@
+/**
+ * `ReactRouterViewStack` is a custom navigation manager used in Ionic React
+ * apps to map React Router route elements (such as ``) to "view
+ * items" that Ionic can manage in a view stack. This is critical to maintain
+ * Ionicβs animation, lifecycle, and history behavior across views.
+ */
+
import type { RouteInfo, ViewItem } from '@ionic/react';
-import { IonRoute, ViewLifeCycleManager, ViewStacks, generateId } from '@ionic/react';
+import { generateId, IonRoute, ViewLifeCycleManager, ViewStacks } from '@ionic/react';
import React from 'react';
+import type { PathMatch } from 'react-router';
+import { Navigate, UNSAFE_RouteContext as RouteContext } from 'react-router-dom';
+
+import { analyzeRouteChildren, computeParentPath } from './utils/computeParentPath';
+import { derivePathnameToMatch, matchPath } from './utils/pathMatching';
+import { normalizePathnameForComparison } from './utils/pathNormalization';
+import { extractRouteChildren, isNavigateElement } from './utils/routeElements';
+import { sortViewsBySpecificity } from './utils/viewItemUtils';
+
+/**
+ * Delay in milliseconds before removing a Navigate view item after a redirect.
+ * This ensures the redirect navigation completes before the view is removed.
+ */
+const NAVIGATE_REDIRECT_DELAY_MS = 100;
+
+/**
+ * Delay in milliseconds before cleaning up a view without an IonPage element.
+ * This double-checks that the view is truly not needed before removal.
+ */
+const VIEW_CLEANUP_DELAY_MS = 200;
+
+type RouteParams = Record;
+
+type RouteContextMatch = {
+ params: RouteParams;
+ pathname: string;
+ pathnameBase: string;
+ route: {
+ id: string;
+ path?: string;
+ element: React.ReactNode;
+ index: boolean;
+ caseSensitive?: boolean;
+ hasErrorBoundary: boolean;
+ };
+};
+
+/**
+ * Computes the absolute pathnameBase for a route element based on its type.
+ * Handles relative paths, index routes, and splat routes differently.
+ */
+const computeAbsolutePathnameBase = (
+ routeElement: React.ReactElement,
+ routeMatch: PathMatch | undefined,
+ parentPathnameBase: string,
+ routeInfoPathname: string
+): string => {
+ const routePath = routeElement.props.path;
+ const isRelativePath = routePath && !routePath.startsWith('/');
+ const isIndexRoute = !!routeElement.props.index;
+ const isSplatOnlyRoute = routePath === '*' || routePath === '/*';
+
+ if (isSplatOnlyRoute) {
+ // Splat routes should NOT contribute their matched portion to pathnameBase
+ // This aligns with React Router v7's v7_relativeSplatPath behavior
+ return parentPathnameBase;
+ }
+
+ if (isRelativePath && routeMatch?.pathnameBase) {
+ const relativeBase = routeMatch.pathnameBase.startsWith('/')
+ ? routeMatch.pathnameBase.slice(1)
+ : routeMatch.pathnameBase;
+ return parentPathnameBase === '/' ? `/${relativeBase}` : `${parentPathnameBase}/${relativeBase}`;
+ }
+
+ if (isIndexRoute) {
+ return parentPathnameBase;
+ }
+
+ return routeMatch?.pathnameBase || routeInfoPathname;
+};
+
+/**
+ * Gets fallback params from view items in other outlets when parent context is empty.
+ * This handles cases where React context propagation doesn't work as expected.
+ */
+const getFallbackParamsFromViewItems = (
+ allViewItems: ViewItem[],
+ currentOutletId: string,
+ currentPathname: string
+): RouteParams => {
+ const matchingViews: { params: RouteParams; pathLength: number }[] = [];
+
+ for (const otherViewItem of allViewItems) {
+ if (otherViewItem.outletId === currentOutletId) continue;
+
+ const otherMatch = otherViewItem.routeData?.match;
+ if (otherMatch?.params && Object.keys(otherMatch.params).length > 0) {
+ const matchedPathname = otherMatch.pathnameBase || otherMatch.pathname;
+ if (matchedPathname && currentPathname.startsWith(matchedPathname)) {
+ matchingViews.push({
+ params: otherMatch.params,
+ pathLength: matchedPathname.length,
+ });
+ }
+ }
+ }
+
+ // Sort ascending by path length so more-specific (longer) paths are applied
+ // last and their params take priority over less-specific ones.
+ matchingViews.sort((a, b) => a.pathLength - b.pathLength);
+
+ const params: RouteParams = {};
+ for (const view of matchingViews) {
+ Object.assign(params, view.params);
+ }
+
+ return params;
+};
+
+/**
+ * Builds the matches array for RouteContext.
+ */
+const buildContextMatches = (
+ parentMatches: RouteContextMatch[],
+ combinedParams: RouteParams,
+ routeMatch: PathMatch | undefined,
+ routeInfoPathname: string,
+ absolutePathnameBase: string,
+ viewItem: ViewItem,
+ routeElement: React.ReactElement,
+ componentElement: React.ReactNode
+): RouteContextMatch[] => {
+ return [
+ ...parentMatches,
+ {
+ params: combinedParams,
+ pathname: routeMatch?.pathname || routeInfoPathname,
+ pathnameBase: absolutePathnameBase,
+ route: {
+ id: viewItem.id,
+ path: routeElement.props.path,
+ element: componentElement,
+ index: !!routeElement.props.index,
+ caseSensitive: routeElement.props.caseSensitive,
+ hasErrorBoundary: false,
+ },
+ },
+ ];
+};
+
+const createDefaultMatch = (
+ fullPathname: string,
+ routeProps: { path?: string; caseSensitive?: boolean; end?: boolean; index?: boolean }
+): PathMatch => {
+ const isIndexRoute = !!routeProps.index;
+ const patternPath = routeProps.path ?? '';
+ const pathnameBase = fullPathname === '' ? '/' : fullPathname;
+ const computedEnd =
+ routeProps.end !== undefined ? routeProps.end : patternPath !== '' ? !patternPath.endsWith('*') : true;
+
+ return {
+ params: {},
+ pathname: isIndexRoute ? '' : fullPathname,
+ pathnameBase,
+ pattern: {
+ path: patternPath,
+ caseSensitive: routeProps.caseSensitive ?? false,
+ end: isIndexRoute ? true : computedEnd,
+ },
+ };
+};
+
+const computeRelativeToParent = (pathname: string, parentPath?: string): string | null => {
+ if (!parentPath) return null;
+ const normalizedParent = normalizePathnameForComparison(parentPath);
+ const normalizedPathname = normalizePathnameForComparison(pathname);
-import { matchPath } from './utils/matchPath';
+ if (normalizedPathname === normalizedParent) {
+ return '';
+ }
+
+ const withSlash = normalizedParent === '/' ? '/' : normalizedParent + '/';
+ if (normalizedPathname.startsWith(withSlash)) {
+ return normalizedPathname.slice(withSlash.length);
+ }
+ return null;
+};
+
+const resolveIndexRouteMatch = (
+ viewItem: ViewItem,
+ pathname: string,
+ parentPath?: string
+): PathMatch | null => {
+ if (!viewItem.routeData?.childProps?.index) {
+ return null;
+ }
+
+ // Prefer computing against the parent path when available to align with RRv6 semantics
+ const relative = computeRelativeToParent(pathname, parentPath);
+ if (relative !== null) {
+ // Index routes match only when there is no remaining path
+ if (relative === '' || relative === '/') {
+ return createDefaultMatch(parentPath || pathname, viewItem.routeData.childProps);
+ }
+ return null;
+ }
+
+ // Fallback: use previously computed match base for equality check
+ const previousMatch = viewItem.routeData?.match;
+ if (!previousMatch) {
+ return null;
+ }
+
+ const normalizedPathname = normalizePathnameForComparison(pathname);
+ const normalizedBase = normalizePathnameForComparison(previousMatch.pathnameBase || previousMatch.pathname || '');
+
+ return normalizedPathname === normalizedBase ? previousMatch : null;
+};
export class ReactRouterViewStack extends ViewStacks {
+ /**
+ * Stores the computed parent path for each outlet.
+ * Used by findViewItemByPath to correctly evaluate index route matches
+ * without requiring the outlet's React element or route children.
+ */
+ private outletParentPaths = new Map();
+
+ /**
+ * Stores the computed mount path for each outlet.
+ * Fed back into computeParentPath on subsequent calls to stabilize
+ * the parent path computation across navigations (mirrors StackManager.outletMountPath).
+ */
+ private outletMountPaths = new Map();
+
constructor() {
super();
- this.createViewItem = this.createViewItem.bind(this);
- this.findViewItemByRouteInfo = this.findViewItemByRouteInfo.bind(this);
- this.findLeavingViewItemByRouteInfo = this.findLeavingViewItemByRouteInfo.bind(this);
- this.getChildrenToRender = this.getChildrenToRender.bind(this);
- this.findViewItemByPathname = this.findViewItemByPathname.bind(this);
}
- createViewItem(outletId: string, reactElement: React.ReactElement, routeInfo: RouteInfo, page?: HTMLElement) {
+ /**
+ * Creates a new view item for the given outlet and react route element.
+ * Associates route props with the matched route path for further lookups.
+ */
+ createViewItem = (outletId: string, reactElement: React.ReactElement, routeInfo: RouteInfo, page?: HTMLElement) => {
+ const routePath = reactElement.props.path || '';
+
+ // Check if we already have a view item for this exact route that we can reuse
+ // Include wildcard routes like tabs/* since they should be reused
+ // Also check unmounted items that might have been preserved for browser navigation
+ const existingViewItem = this.getViewItemsForOutlet(outletId).find((v) => {
+ const existingRouteProps = v.reactElement?.props ?? {};
+ const existingPath = existingRouteProps.path || '';
+ const existingElement = existingRouteProps.element;
+ const newElement = reactElement.props.element;
+ const existingIsIndexRoute = !!existingRouteProps.index;
+ const newIsIndexRoute = !!reactElement.props.index;
+
+ // For Navigate components, match by destination
+ const existingIsNavigate = React.isValidElement(existingElement) && existingElement.type === Navigate;
+ const newIsNavigate = React.isValidElement(newElement) && newElement.type === Navigate;
+ if (existingIsNavigate && newIsNavigate) {
+ const existingTo = (existingElement.props as { to?: string })?.to;
+ const newTo = (newElement.props as { to?: string })?.to;
+ if (existingTo === newTo) {
+ return true;
+ }
+ }
+
+ if (existingIsIndexRoute && newIsIndexRoute) {
+ return true;
+ }
+
+ // Reuse view items with the same path
+ // Special case: reuse tabs/* and other specific wildcard routes
+ // Don't reuse index routes (empty path) or generic catch-all wildcards (*)
+ if (existingPath === routePath && existingPath !== '' && existingPath !== '*') {
+ // Parameterized routes need pathname matching to ensure /details/1 and /details/2
+ // get separate view items. For wildcard routes (e.g., user/:userId/*), compare
+ // pathnameBase to allow child path changes while preserving the parent view.
+ const hasParams = routePath.includes(':');
+ const isWildcard = routePath.includes('*');
+ if (hasParams) {
+ if (isWildcard) {
+ const existingPathnameBase = v.routeData?.match?.pathnameBase;
+ const newMatch = matchComponent(reactElement, routeInfo.pathname, false, this.outletParentPaths.get(outletId));
+ const newPathnameBase = newMatch?.pathnameBase;
+ if (existingPathnameBase !== newPathnameBase) {
+ return false;
+ }
+ } else {
+ const existingPathname = v.routeData?.match?.pathname;
+ if (existingPathname !== routeInfo.pathname) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+ // Also reuse specific wildcard routes like tabs/*
+ if (existingPath === routePath && existingPath.endsWith('/*') && existingPath !== '/*') {
+ return true;
+ }
+ return false;
+ });
+
+ if (existingViewItem) {
+ // Update and ensure the existing view item is properly configured
+ existingViewItem.reactElement = reactElement;
+ existingViewItem.mount = true;
+ existingViewItem.ionPageElement = page || existingViewItem.ionPageElement;
+ const updatedMatch =
+ matchComponent(reactElement, routeInfo.pathname, false, this.outletParentPaths.get(outletId)) ||
+ existingViewItem.routeData?.match ||
+ createDefaultMatch(routeInfo.pathname, reactElement.props);
+
+ existingViewItem.routeData = {
+ match: updatedMatch,
+ childProps: reactElement.props,
+ lastPathname: existingViewItem.routeData?.lastPathname, // Preserve navigation history
+ };
+ return existingViewItem;
+ }
+
+ const id = `${outletId}-${generateId(outletId)}`;
+
const viewItem: ViewItem = {
- id: generateId('viewItem'),
+ id,
outletId,
ionPageElement: page,
reactElement,
mount: true,
- ionRoute: false,
+ ionRoute: true,
};
if (reactElement.type === IonRoute) {
- viewItem.ionRoute = true;
viewItem.disableIonPageManagement = reactElement.props.disableIonPageManagement;
}
+ const initialMatch =
+ matchComponent(reactElement, routeInfo.pathname, true, this.outletParentPaths.get(outletId)) ||
+ createDefaultMatch(routeInfo.pathname, reactElement.props);
+
viewItem.routeData = {
- match: matchPath({
- pathname: routeInfo.pathname,
- componentProps: reactElement.props,
- }),
+ match: initialMatch,
childProps: reactElement.props,
};
+ this.add(viewItem);
+
return viewItem;
- }
+ };
+
+ /**
+ * Renders a ViewLifeCycleManager for the given view item.
+ * Handles cleanup if the view no longer matches.
+ *
+ * - Deactivates view if it no longer matches the current route
+ * - Wraps the route element in to support nested routing and ensure remounting
+ * - Adds a unique key to so React Router remounts routes when switching
+ */
+ private renderViewItem = (viewItem: ViewItem, routeInfo: RouteInfo, parentPath?: string, reRender?: () => void) => {
+ const routePath = viewItem.reactElement.props.path || '';
+ let match = matchComponent(viewItem.reactElement, routeInfo.pathname, false, parentPath);
+
+ if (!match) {
+ const indexMatch = resolveIndexRouteMatch(viewItem, routeInfo.pathname, parentPath);
+ if (indexMatch) {
+ match = indexMatch;
+ }
+ }
+
+ // For parameterized routes, check if this is a navigation to a different path instance
+ // In that case, we should NOT reuse this view - a new view should be created
+ const isParameterRoute = routePath.includes(':');
+ const previousMatch = viewItem.routeData?.match;
+ const isSamePath = match?.pathname === previousMatch?.pathname;
+
+ // Flag to indicate this view should not be reused for this different parameterized path
+ const shouldSkipForDifferentParam = isParameterRoute && match && previousMatch && !isSamePath;
+
+ // Don't deactivate views automatically - let the StackManager handle view lifecycle
+ // This preserves views in the stack for navigation history like native apps
+ // Views will be hidden/shown by the StackManager's transition logic instead of being unmounted
+
+ // Special handling for Navigate components - they should unmount after redirecting
+ const elementComponent = viewItem.reactElement?.props?.element;
+ const isNavigateComponent = isNavigateElement(elementComponent);
+
+ if (isNavigateComponent) {
+ // Navigate components should only be mounted when they match
+ // Once they redirect (no longer match), they should be removed completely
+ // IMPORTANT: For index routes, we need to check indexMatch too since matchComponent
+ // may not properly match index routes without explicit parent path context
+ const indexMatch = viewItem.routeData?.childProps?.index
+ ? resolveIndexRouteMatch(viewItem, routeInfo.pathname, parentPath)
+ : null;
+ const hasValidMatch = match || indexMatch;
+
+ if (!hasValidMatch && viewItem.mount) {
+ viewItem.mount = false;
+ // Schedule removal of the Navigate view item after a short delay
+ // This ensures the redirect completes before removal
+ setTimeout(() => {
+ this.remove(viewItem);
+ reRender?.();
+ }, NAVIGATE_REDIRECT_DELAY_MS);
+ }
+ }
+
+ // Components that don't have IonPage elements and no longer match should be cleaned up
+ // BUT we need to be careful not to remove them if they're part of browser navigation history
+ // This handles components that perform immediate actions like programmatic navigation
+ // EXCEPTION: Navigate components should ALWAYS remain mounted until they redirect
+ // since they need to be rendered to trigger the navigation
+ if (!match && viewItem.mount && !viewItem.ionPageElement && !isNavigateComponent) {
+ // Check if this view item should be preserved for browser navigation
+ // We'll keep it if it was recently active (within the last navigation)
+ const shouldPreserve =
+ viewItem.routeData.lastPathname === routeInfo.pathname ||
+ viewItem.routeData.match?.pathname === routeInfo.lastPathname;
+
+ if (!shouldPreserve) {
+ // This view item doesn't match and doesn't have an IonPage
+ // It's likely a utility component that performs an action and navigates away
+ viewItem.mount = false;
+ // Schedule removal to allow it to be recreated on next navigation
+ setTimeout(() => {
+ // Double-check before removing - the view might be needed again
+ const stillNotNeeded = !viewItem.mount && !viewItem.ionPageElement;
+ if (stillNotNeeded) {
+ this.remove(viewItem);
+ reRender?.();
+ }
+ }, VIEW_CLEANUP_DELAY_MS);
+ } else {
+ // Preserve it but unmount it for now
+ viewItem.mount = false;
+ }
+ }
+
+ // Reactivate view if it matches but was previously deactivated
+ // Don't reactivate if this is a parameterized route navigating to a different path instance
+ // Don't reactivate catch-all wildcard routes β they are created fresh by createViewItem
+ const isCatchAllWildcard = routePath === '*' || routePath === '/*';
+ if (match && !viewItem.mount && !shouldSkipForDifferentParam && !isCatchAllWildcard) {
+ viewItem.mount = true;
+ viewItem.routeData.match = match;
+ }
+
+ // Deactivate wildcard (catch-all) and empty-path (default) routes when a more-specific route matches.
+ // This prevents "Not found" or fallback pages from showing alongside valid routes.
+ if (routePath === '*' || routePath === '') {
+ // Check if any other view in this outlet has a match for the current route
+ const outletViews = this.getViewItemsForOutlet(viewItem.outletId);
+
+ // When parent path context is available, compute the relative pathname once
+ // outside the loop since both routeInfo.pathname and parentPath are invariant.
+ const relativePathname = parentPath
+ ? computeRelativeToParent(routeInfo.pathname, parentPath)
+ : null;
+
+ let hasSpecificMatch = outletViews.some((v) => {
+ if (v.id === viewItem.id) return false; // Skip self
+ const vRoutePath = v.reactElement?.props?.path || '';
+ if (vRoutePath === '*' || vRoutePath === '') return false; // Skip other wildcard/empty routes
+
+ // When parent path context is available and the route is relative, use
+ // parent-path-aware matching. This avoids false positives from
+ // derivePathnameToMatch's tail-slice heuristic, which can incorrectly
+ // match route literals that appear at the wrong position in the pathname.
+ // Example: pathname /parent/extra/details/99 with route details/:id β
+ // the tail-slice extracts ["details","99"] producing a false match.
+ if (parentPath && vRoutePath && !vRoutePath.startsWith('/')) {
+ if (relativePathname === null) {
+ return false; // Pathname is outside this outlet's parent scope
+ }
+ return !!matchPath({
+ pathname: relativePathname,
+ componentProps: v.reactElement.props,
+ });
+ }
+
+ // Fallback to matchComponent when no parent path context is available
+ const vMatch = v.reactElement ? matchComponent(v.reactElement, routeInfo.pathname) : null;
+ return !!vMatch;
+ });
+
+ // For catch-all * routes, also deactivate when the pathname matches the outlet's
+ // parent path exactly. This means there are no remaining segments for the wildcard
+ // to catch, so the empty-path or index route should handle it instead.
+ if (!hasSpecificMatch && routePath === '*') {
+ const outletParentPath = this.outletParentPaths.get(viewItem.outletId);
+ if (outletParentPath) {
+ const normalizedParent = normalizePathnameForComparison(outletParentPath);
+ const normalizedPathname = normalizePathnameForComparison(routeInfo.pathname);
+ if (normalizedPathname === normalizedParent) {
+ // Check if there's an empty-path or index view item that should handle this
+ const hasDefaultRoute = outletViews.some((v) => {
+ if (v.id === viewItem.id) return false;
+ const vRoutePath = v.reactElement?.props?.path;
+ return vRoutePath === '' || vRoutePath === undefined || !!v.routeData?.childProps?.index;
+ });
+ if (hasDefaultRoute) {
+ hasSpecificMatch = true;
+ }
+ }
+ }
+ }
+
+ if (hasSpecificMatch) {
+ viewItem.mount = false;
+ if (viewItem.ionPageElement) {
+ viewItem.ionPageElement.classList.add('ion-page-hidden');
+ viewItem.ionPageElement.setAttribute('aria-hidden', 'true');
+ }
+ }
+ }
+
+ const routeElement = React.cloneElement(viewItem.reactElement);
+ const componentElement = routeElement.props.element;
+ // Don't update match for parameterized routes navigating to different path instances
+ // This preserves the original match so that findViewItemByPath can correctly skip this view
+ if (match && viewItem.routeData.match !== match && !shouldSkipForDifferentParam) {
+ viewItem.routeData.match = match;
+ }
+ const routeMatch = shouldSkipForDifferentParam ? viewItem.routeData?.match : match || viewItem.routeData?.match;
- getChildrenToRender(outletId: string, ionRouterOutlet: React.ReactElement, routeInfo: RouteInfo) {
+ return (
+
+ {(parentContext) => {
+ const parentMatches = (parentContext?.matches ?? []) as RouteContextMatch[];
+
+ // Accumulate params from parent matches, with fallback to other outlets
+ let accumulatedParentParams = parentMatches.reduce((acc, m) => ({ ...acc, ...m.params }), {});
+ if (parentMatches.length === 0 && Object.keys(accumulatedParentParams).length === 0) {
+ accumulatedParentParams = getFallbackParamsFromViewItems(
+ this.getAllViewItems(),
+ viewItem.outletId,
+ routeInfo.pathname
+ );
+ }
+
+ const combinedParams = { ...accumulatedParentParams, ...(routeMatch?.params ?? {}) };
+ const parentPathnameBase =
+ parentMatches.length > 0 ? parentMatches[parentMatches.length - 1].pathnameBase : '/';
+ const absolutePathnameBase = computeAbsolutePathnameBase(
+ routeElement,
+ routeMatch,
+ parentPathnameBase,
+ routeInfo.pathname
+ );
+
+ const contextMatches = buildContextMatches(
+ parentMatches,
+ combinedParams,
+ routeMatch,
+ routeInfo.pathname,
+ absolutePathnameBase,
+ viewItem,
+ routeElement,
+ componentElement
+ );
+
+ const routeContextValue = parentContext
+ ? { ...parentContext, matches: contextMatches }
+ : { outlet: null, matches: contextMatches, isDataRoute: false };
+
+ return (
+ this.remove(viewItem)}
+ >
+ {componentElement}
+
+ );
+ }}
+
+ );
+ };
+
+ /**
+ * Re-renders all active view items for the specified outlet.
+ * Ensures React elements are updated with the latest match.
+ *
+ * 1. Iterates through children of IonRouterOutlet
+ * 2. Updates each matching viewItem with the current child React element
+ * (important for updating props or changes to elements)
+ * 3. Returns a list of React components that will be rendered inside the outlet
+ * Each view is wrapped in to manage lifecycle and rendering
+ */
+ getChildrenToRender = (
+ outletId: string,
+ ionRouterOutlet: React.ReactElement,
+ routeInfo: RouteInfo,
+ reRender: () => void,
+ parentPathnameBase?: string
+ ) => {
const viewItems = this.getViewItemsForOutlet(outletId);
- // Sync latest routes with viewItems
+ // Seed the mount path from the parent route context if available.
+ // This provides the outlet's mount path immediately on first render,
+ // eliminating the need for heuristic-based discovery in computeParentPath.
+ if (parentPathnameBase && !this.outletMountPaths.has(outletId)) {
+ this.outletMountPaths.set(outletId, parentPathnameBase);
+ }
+
+ // Determine parentPath for outlets with relative or index routes.
+ // This populates outletParentPaths for findViewItemByPath's matchView
+ // and the catch-all deactivation logic in renderViewItem.
+ let parentPath: string | undefined = undefined;
+ try {
+ const routeChildren = extractRouteChildren(ionRouterOutlet.props.children);
+ const { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = analyzeRouteChildren(routeChildren);
+
+ if (hasRelativeRoutes || hasIndexRoute) {
+ const result = computeParentPath({
+ currentPathname: routeInfo.pathname,
+ outletMountPath: this.outletMountPaths.get(outletId),
+ routeChildren,
+ hasRelativeRoutes,
+ hasIndexRoute,
+ hasWildcardRoute,
+ });
+ parentPath = result.parentPath;
+
+ // Persist the mount path for subsequent calls, mirroring StackManager.outletMountPath.
+ // Unlike outletParentPaths (cleared when parentPath is undefined), the mount path is
+ // intentionally sticky β it anchors the outlet's scope and is only removed in clear().
+ if (result.outletMountPath && !this.outletMountPaths.has(outletId)) {
+ this.outletMountPaths.set(outletId, result.outletMountPath);
+ }
+ }
+ } catch (e) {
+ // Non-fatal: if we fail to compute parentPath, fall back to previous behavior
+ }
+
+ // Store the computed parentPath for use in findViewItemByPath.
+ // Clear stale entries when parentPath is undefined (e.g., navigated out of scope).
+ if (parentPath !== undefined) {
+ this.outletParentPaths.set(outletId, parentPath);
+ } else if (this.outletParentPaths.has(outletId)) {
+ this.outletParentPaths.delete(outletId);
+ }
+
+ // Sync child elements with stored viewItems (e.g. to reflect new props)
React.Children.forEach(ionRouterOutlet.props.children, (child: React.ReactElement) => {
- const viewItem = viewItems.find((v) => {
- return matchComponent(child, v.routeData.childProps.path || v.routeData.childProps.from);
- });
- if (viewItem) {
- viewItem.reactElement = child;
+ // Ensure the child is a valid React element since we
+ // might have whitespace strings or other non-element children
+ if (React.isValidElement(child)) {
+ // Find view item by exact path match to avoid wildcard routes overwriting specific routes
+ const childPath = (child.props as any).path;
+ const viewItem = viewItems.find((v) => {
+ const viewItemPath = v.reactElement?.props?.path;
+ // Only update if paths match exactly (prevents wildcard routes from overwriting specific routes)
+ return viewItemPath === childPath;
+ });
+ if (viewItem) {
+ viewItem.reactElement = child;
+ }
}
});
- const children = viewItems.map((viewItem) => {
- let clonedChild;
- if (viewItem.ionRoute && !viewItem.disableIonPageManagement) {
- clonedChild = (
- this.remove(viewItem)}
- >
- {React.cloneElement(viewItem.reactElement, {
- computedMatch: viewItem.routeData.match,
- })}
-
- );
- } else {
- const match = matchComponent(viewItem.reactElement, routeInfo.pathname);
- clonedChild = (
- this.remove(viewItem)}
- >
- {React.cloneElement(viewItem.reactElement, {
- computedMatch: viewItem.routeData.match,
- })}
-
- );
-
- if (!match && viewItem.routeData.match) {
- viewItem.routeData.match = undefined;
- viewItem.mount = false;
- }
- }
-
- return clonedChild;
+ // Filter out duplicate view items by ID (but keep all mounted items)
+ const uniqueViewItems = viewItems.filter((viewItem, index, array) => {
+ // Remove duplicates by ID (keep first occurrence)
+ const isFirstOccurrence = array.findIndex((v) => v.id === viewItem.id) === index;
+ return isFirstOccurrence;
});
- return children;
- }
- findViewItemByRouteInfo(routeInfo: RouteInfo, outletId?: string, updateMatch?: boolean) {
+ // Filter out unmounted Navigate components to prevent them from being rendered
+ // and triggering unwanted redirects
+ const renderableViewItems = uniqueViewItems.filter((viewItem) => {
+ const elementComponent = viewItem.reactElement?.props?.element;
+ const isNavigateComponent = isNavigateElement(elementComponent);
+
+ // Exclude unmounted Navigate components from rendering
+ if (isNavigateComponent && !viewItem.mount) {
+ return false;
+ }
+
+ // Filter out views that are unmounted, have no ionPageElement, and don't match the current route.
+ // These are "stale" views from previous routes that should not be rendered.
+ // Views WITH ionPageElement are handled by the normal lifecycle events.
+ // Views that MATCH the current route should be kept (they might be transitioning).
+ if (!viewItem.mount && !viewItem.ionPageElement) {
+ // Check if this view's route path matches the current pathname
+ const viewRoutePath = viewItem.reactElement?.props?.path as string | undefined;
+ if (viewRoutePath) {
+ // First try exact match using matchComponent
+ const routeMatch = matchComponent(viewItem.reactElement, routeInfo.pathname, false, parentPath);
+ if (routeMatch) {
+ // View matches current route, keep it
+ return true;
+ }
+
+ // For parent routes (like /multiple-tabs or /routing), check if current pathname
+ // starts with this route's path. This handles views with IonSplitPane/IonTabs
+ // that don't have IonPage but should remain mounted while navigating within their children.
+ const normalizedViewPath = normalizePathnameForComparison(viewRoutePath.replace(/\/?\*$/, '')); // Remove trailing wildcard
+ const normalizedCurrentPath = normalizePathnameForComparison(routeInfo.pathname);
+
+ // Check if current pathname is within this view's route hierarchy
+ const isWithinRouteHierarchy =
+ normalizedCurrentPath === normalizedViewPath || normalizedCurrentPath.startsWith(normalizedViewPath + '/');
+
+ if (!isWithinRouteHierarchy) {
+ // View is outside current route hierarchy, remove it
+ setTimeout(() => {
+ this.remove(viewItem);
+ reRender();
+ }, 0);
+ return false;
+ }
+ }
+ }
+
+ return true;
+ });
+
+ const renderedItems = renderableViewItems.map((viewItem) =>
+ this.renderViewItem(viewItem, routeInfo, parentPath, reRender)
+ );
+ return renderedItems;
+ };
+
+ /**
+ * Finds a view item matching the current route, optionally updating its match state.
+ */
+ findViewItemByRouteInfo = (routeInfo: RouteInfo, outletId?: string, updateMatch?: boolean) => {
const { viewItem, match } = this.findViewItemByPath(routeInfo.pathname, outletId);
const shouldUpdateMatch = updateMatch === undefined || updateMatch === true;
if (shouldUpdateMatch && viewItem && match) {
viewItem.routeData.match = match;
}
return viewItem;
- }
+ };
+
+ /**
+ * Finds the view item that was previously active before a route change.
+ */
+ findLeavingViewItemByRouteInfo = (routeInfo: RouteInfo, outletId?: string, mustBeIonRoute = true) => {
+ // If the lastPathname is not set, we cannot find a leaving view item
+ if (!routeInfo.lastPathname) {
+ return undefined;
+ }
- findLeavingViewItemByRouteInfo(routeInfo: RouteInfo, outletId?: string, mustBeIonRoute = true) {
- const { viewItem } = this.findViewItemByPath(routeInfo.lastPathname!, outletId, mustBeIonRoute);
+ const { viewItem } = this.findViewItemByPath(routeInfo.lastPathname, outletId, mustBeIonRoute);
return viewItem;
- }
+ };
- findViewItemByPathname(pathname: string, outletId?: string) {
+ /**
+ * Finds a view item by pathname only, used in simpler queries.
+ */
+ findViewItemByPathname = (pathname: string, outletId?: string) => {
const { viewItem } = this.findViewItemByPath(pathname, outletId);
return viewItem;
- }
+ };
/**
- * Returns the matching view item and the match result for a given pathname.
+ * Core function that matches a given pathname against all view items.
+ * Returns both the matched view item and match metadata.
*/
- private findViewItemByPath(pathname: string, outletId?: string, mustBeIonRoute?: boolean) {
+ private findViewItemByPath(pathname: string, outletId?: string, mustBeIonRoute?: boolean, allowDefaultMatch = true) {
let viewItem: ViewItem | undefined;
- let match: ReturnType | undefined;
+ let match: PathMatch | null = null;
let viewStack: ViewItem[];
+ // Capture stored parent paths for use in nested matchView/matchDefaultRoute functions
+ const storedParentPaths = this.outletParentPaths;
if (outletId) {
- viewStack = this.getViewItemsForOutlet(outletId);
+ viewStack = sortViewsBySpecificity(this.getViewItemsForOutlet(outletId));
viewStack.some(matchView);
- if (!viewItem) {
- viewStack.some(matchDefaultRoute);
- }
+ if (!viewItem && allowDefaultMatch) viewStack.some(matchDefaultRoute);
} else {
- const viewItems = this.getAllViewItems();
+ const viewItems = sortViewsBySpecificity(this.getAllViewItems());
viewItems.some(matchView);
- if (!viewItem) {
- viewItems.some(matchDefaultRoute);
- }
+ if (!viewItem && allowDefaultMatch) viewItems.some(matchDefaultRoute);
}
+ // If we still have not found a view item for this outlet, try to find a matching
+ // view item across all outlets and adopt it into the current outlet. This helps
+ // recover when an outlet remounts and receives a new id, leaving views associated
+ // with the previous outlet id.
+ // Do not adopt across outlets; if we didn't find a view for this outlet,
+ // defer to route matching to create a new one.
+
return { viewItem, match };
+ /**
+ * Matches a route path with dynamic parameters (e.g. /tabs/:id)
+ */
function matchView(v: ViewItem) {
- if (mustBeIonRoute && !v.ionRoute) {
- return false;
+ if (mustBeIonRoute && !v.ionRoute) return false;
+
+ const viewItemPath = v.routeData.childProps.path || '';
+
+ // Skip unmounted catch-all wildcard views. After back navigation unmounts
+ // a wildcard view, it should not be reused for subsequent navigations.
+ // A fresh wildcard view will be created by createViewItem when needed.
+ if ((viewItemPath === '*' || viewItemPath === '/*') && !v.mount) return false;
+
+ const isIndexRoute = !!v.routeData.childProps.index;
+ const previousMatch = v.routeData?.match;
+ const outletParentPath = storedParentPaths.get(v.outletId);
+ const result = v.reactElement ? matchComponent(v.reactElement, pathname, false, outletParentPath) : null;
+
+ if (!result) {
+ const indexMatch = resolveIndexRouteMatch(v, pathname, outletParentPath);
+ if (indexMatch) {
+ match = indexMatch;
+ viewItem = v;
+ return true;
+ }
+
+ // Empty path routes (path="") should match when the pathname matches the
+ // outlet's parent path exactly (no remaining segments). matchComponent doesn't
+ // handle this because it lacks parent path context. Without this check, a
+ // catch-all * view item (which matches any pathname) would be incorrectly
+ // returned instead of the empty path route on back navigation.
+ if (viewItemPath === '' && !isIndexRoute && outletParentPath) {
+ const normalizedParent = normalizePathnameForComparison(outletParentPath);
+ const normalizedPathname = normalizePathnameForComparison(pathname);
+ if (normalizedPathname === normalizedParent) {
+ match = createDefaultMatch(pathname, v.routeData.childProps);
+ viewItem = v;
+ return true;
+ }
+ }
}
- match = matchPath({
- pathname,
- componentProps: v.routeData.childProps,
- });
+ if (result) {
+ const hasParams = result.params && Object.keys(result.params).length > 0;
+ const isSamePath = result.pathname === previousMatch?.pathname;
+ const isWildcardRoute = viewItemPath.includes('*');
+ const isParameterRoute = viewItemPath.includes(':');
+
+ // Don't allow view items with undefined paths to match specific routes
+ // This prevents broken index route view items from interfering with navigation
+ if (!viewItemPath && !isIndexRoute && pathname !== '/' && pathname !== '') {
+ return false;
+ }
+
+ // For parameterized routes, check if we should reuse the view item.
+ // Wildcard routes (e.g., user/:userId/*) compare pathnameBase to allow
+ // child path changes while preserving the parent view.
+ if (isParameterRoute && !isSamePath) {
+ if (isWildcardRoute) {
+ const isSameBase = result.pathnameBase === previousMatch?.pathnameBase;
+ if (isSameBase) {
+ match = result;
+ viewItem = v;
+ return true;
+ }
+ }
+ return false;
+ }
- if (match) {
- /**
- * Even though we have a match from react-router, we do not know if the match
- * is for this specific view item.
- *
- * To validate this, we need to check if the path and url match the view item's route data.
- */
- const hasParameter = match.path.includes(':');
- if (!hasParameter || (hasParameter && match.url === v.routeData?.match?.url)) {
+ // For routes without params, or when navigating to the exact same path,
+ // or when there's no previous match, reuse the view item
+ if (!hasParams || isSamePath || !previousMatch) {
+ match = result;
viewItem = v;
return true;
}
+
+ // For pure wildcard routes (without : params), compare pathnameBase to allow
+ // child path changes while preserving the parent view. This handles container
+ // routes like /tabs/* where switching between /tabs/tab1 and /tabs/tab2
+ // should reuse the same ViewItem.
+ if (isWildcardRoute && !isParameterRoute) {
+ const isSameBase = result.pathnameBase === previousMatch?.pathnameBase;
+ if (isSameBase) {
+ match = result;
+ viewItem = v;
+ return true;
+ }
+ }
}
+
return false;
}
- function matchDefaultRoute(v: ViewItem) {
- // try to find a route that doesn't have a path or from prop, that will be our default route
- if (!v.routeData.childProps.path && !v.routeData.childProps.from) {
+ /**
+ * Matches a view with no path prop (default fallback route) or index route.
+ */
+ function matchDefaultRoute(v: ViewItem): boolean {
+ const childProps = v.routeData.childProps;
+
+ const isDefaultRoute = childProps.path === undefined || childProps.path === '';
+ const isIndexRoute = !!childProps.index;
+
+ if (isIndexRoute) {
+ const outletParentPath = storedParentPaths.get(v.outletId);
+ const indexMatch = resolveIndexRouteMatch(v, pathname, outletParentPath);
+ if (indexMatch) {
+ match = indexMatch;
+ viewItem = v;
+ return true;
+ }
+ return false;
+ }
+
+ // For empty path routes, only match if we're at the same level as when the view was created.
+ // This prevents an empty path view item from being reused for different routes.
+ if (isDefaultRoute) {
+ const previousPathnameBase = v.routeData?.match?.pathnameBase || '';
+ const normalizedBase = normalizePathnameForComparison(previousPathnameBase);
+ const normalizedPathname = normalizePathnameForComparison(pathname);
+
+ if (normalizedPathname !== normalizedBase) {
+ return false;
+ }
+
match = {
- path: pathname,
- url: pathname,
- isExact: true,
params: {},
+ pathname,
+ pathnameBase: pathname === '' ? '/' : pathname,
+ pattern: {
+ path: '',
+ caseSensitive: childProps.caseSensitive ?? false,
+ end: true,
+ },
};
viewItem = v;
return true;
}
+
return false;
}
}
+
+ /**
+ * Clean up old, unmounted view items to prevent memory leaks
+ */
+ private cleanupStaleViewItems = (outletId: string) => {
+ const viewItems = this.getViewItemsForOutlet(outletId);
+
+ // Keep only the most recent mounted views and a few unmounted ones for history
+ const maxUnmountedItems = 3;
+ const unmountedItems = viewItems.filter((v) => !v.mount);
+
+ if (unmountedItems.length > maxUnmountedItems) {
+ // Remove oldest unmounted items
+ const itemsToRemove = unmountedItems.slice(0, unmountedItems.length - maxUnmountedItems);
+ itemsToRemove.forEach((item) => {
+ this.remove(item);
+ });
+ }
+ };
+
+ /**
+ * Override add to prevent duplicate view items with the same ID in the same outlet
+ * But allow multiple view items for the same route path (for navigation history)
+ */
+ add = (viewItem: ViewItem) => {
+ const existingViewItem = this.getViewItemsForOutlet(viewItem.outletId).find((v) => v.id === viewItem.id);
+
+ if (existingViewItem) {
+ return;
+ }
+
+ super.add(viewItem);
+
+ this.cleanupStaleViewItems(viewItem.outletId);
+ };
+
+ /**
+ * Override clear to also clean up the stored parent path for the outlet.
+ */
+ clear = (outletId: string) => {
+ this.outletParentPaths.delete(outletId);
+ this.outletMountPaths.delete(outletId);
+ return super.clear(outletId);
+ };
+
+ /**
+ * Override remove
+ */
+ remove = (viewItem: ViewItem) => {
+ super.remove(viewItem);
+ };
}
-function matchComponent(node: React.ReactElement, pathname: string) {
- return matchPath({
- pathname,
- componentProps: node.props,
+/**
+ * Utility to apply matchPath to a React element and return its match state.
+ */
+function matchComponent(node: React.ReactElement, pathname: string, allowFallback = false, parentPath?: string) {
+ const routeProps = node?.props ?? {};
+ const routePath: string | undefined = routeProps.path;
+
+ let pathnameToMatch: string;
+ if (parentPath && routePath && !routePath.startsWith('/')) {
+ // When parent path is known, compute exact relative pathname
+ // instead of using the tail-slice heuristic
+ const relative = pathname.startsWith(parentPath)
+ ? pathname.slice(parentPath.length).replace(/^\//, '')
+ : pathname;
+ pathnameToMatch = relative;
+ } else {
+ pathnameToMatch = derivePathnameToMatch(pathname, routePath);
+ }
+
+ const match = matchPath({
+ pathname: pathnameToMatch,
+ componentProps: routeProps,
});
+
+ if (match || !allowFallback) {
+ return match;
+ }
+
+ const isIndexRoute = !!routeProps.index;
+
+ if (isIndexRoute) {
+ return createDefaultMatch(pathname, routeProps);
+ }
+
+ if (!routePath || routePath === '') {
+ return createDefaultMatch(pathname, routeProps);
+ }
+
+ return null;
}
diff --git a/packages/react-router/src/ReactRouter/StackManager.tsx b/packages/react-router/src/ReactRouter/StackManager.tsx
index 708de651391..792a2e53451 100644
--- a/packages/react-router/src/ReactRouter/StackManager.tsx
+++ b/packages/react-router/src/ReactRouter/StackManager.tsx
@@ -1,24 +1,65 @@
+/**
+ * `StackManager` is responsible for managing page transitions, keeping track
+ * of views (pages), and ensuring that navigation behaves like native apps β
+ * particularly with animations and swipe gestures.
+ */
+
import type { RouteInfo, StackContextState, ViewItem } from '@ionic/react';
-import { RouteManagerContext, StackContext, generateId, getConfig } from '@ionic/react';
+import { IonRoute, RouteManagerContext, StackContext, generateId, getConfig } from '@ionic/react';
import React from 'react';
+import type { RouteObject } from 'react-router-dom';
+import { Route, UNSAFE_RouteContext as RouteContext, matchRoutes } from 'react-router-dom';
import { clonePageElement } from './clonePageElement';
-import { matchPath } from './utils/matchPath';
+import {
+ analyzeRouteChildren,
+ computeCommonPrefix,
+ computeParentPath,
+ isPathnameInScope,
+} from './utils/computeParentPath';
+import { derivePathnameToMatch, matchPath } from './utils/pathMatching';
+import { stripTrailingSlash } from './utils/pathNormalization';
+import { extractRouteChildren, getRoutesChildren, isNavigateElement } from './utils/routeElements';
+
+/**
+ * Delay in milliseconds before unmounting a view after a transition completes.
+ * This ensures the page transition animation finishes before the view is removed.
+ */
+const VIEW_UNMOUNT_DELAY_MS = 250;
-// TODO(FW-2959): types
+/**
+ * Delay (ms) to wait for an IonPage to mount before proceeding with a
+ * page transition. Only container routes (nested outlets with no direct
+ * IonPage) actually hit this timeout; normal routes clear it early via
+ * registerIonPage, so a larger value here doesn't affect the happy path.
+ */
+const ION_PAGE_WAIT_TIMEOUT_MS = 300;
interface StackManagerProps {
routeInfo: RouteInfo;
+ id?: string;
}
-// eslint-disable-next-line @typescript-eslint/no-empty-interface
-interface StackManagerState {}
-
const isViewVisible = (el: HTMLElement) =>
- !el.classList.contains('ion-page-invisible') && !el.classList.contains('ion-page-hidden');
+ !el.classList.contains('ion-page-invisible') && !el.classList.contains('ion-page-hidden') && el.style.visibility !== 'hidden';
+
+const hideIonPageElement = (element: HTMLElement | undefined): void => {
+ if (element) {
+ element.classList.add('ion-page-hidden');
+ element.setAttribute('aria-hidden', 'true');
+ }
+};
+
+const showIonPageElement = (element: HTMLElement | undefined): void => {
+ if (element) {
+ element.style.removeProperty('visibility');
+ element.classList.remove('ion-page-hidden');
+ element.removeAttribute('aria-hidden');
+ }
+};
-export class StackManager extends React.PureComponent {
- id: string;
+export class StackManager extends React.PureComponent {
+ id: string; // Unique id for the router outlet aka outletId
context!: React.ContextType;
ionRouterOutlet?: React.ReactElement;
routerOutletElement: HTMLIonRouterOutletElement | undefined;
@@ -30,32 +71,729 @@ export class StackManager extends React.PureComponent true,
};
- private clearOutletTimeout: any;
private pendingPageTransition = false;
+ private waitingForIonPage = false;
+ private ionPageWaitTimeout?: ReturnType;
+ private outOfScopeUnmountTimeout?: ReturnType;
+ /**
+ * Track the last transition's entering and leaving view IDs to prevent
+ * duplicate transitions during rapid navigation (e.g., Navigate redirects)
+ */
+ private lastTransition?: { enteringId: string; leavingId?: string };
+ /** Tracks whether the component is mounted to guard async transition paths. */
+ private _isMounted = false;
+ /** In-flight requestAnimationFrame IDs from transitionPage, cancelled on unmount. */
+ private transitionRafIds: number[] = [];
+ /** In-flight MutationObserver from waitForComponentsReady, disconnected on unmount. */
+ private transitionObserver?: MutationObserver;
constructor(props: StackManagerProps) {
super(props);
this.registerIonPage = this.registerIonPage.bind(this);
this.transitionPage = this.transitionPage.bind(this);
this.handlePageTransition = this.handlePageTransition.bind(this);
- this.id = generateId('routerOutlet');
+ this.id = props.id || `routerOutlet-${generateId('routerOutlet')}`;
this.prevProps = undefined;
this.skipTransition = false;
}
- componentDidMount() {
- if (this.clearOutletTimeout) {
- /**
- * The clearOutlet integration with React Router is a bit hacky.
- * It uses a timeout to clear the outlet after a transition.
- * In React v18, components are mounted and unmounted in development mode
- * to check for side effects.
- *
- * This clearTimeout prevents the outlet from being cleared when the component is re-mounted,
- * which should only happen in development mode and as a result of a hot reload.
- */
- clearTimeout(this.clearOutletTimeout);
+ private outletMountPath: string | undefined = undefined;
+ /**
+ * Whether this outlet is at the root level (no parent route matches).
+ * Derived from UNSAFE_RouteContext in render() β empty matches means root.
+ */
+ private isRootOutlet = true;
+
+ /**
+ * Determines the parent path for nested routing in React Router 6.
+ *
+ * When the mount path is known (seeded from UNSAFE_RouteContext), returns
+ * it directly β no iterative discovery needed. The computeParentPath
+ * fallback only runs for root outlets where RouteContext doesn't provide
+ * a parent match.
+ */
+ private getParentPath(): string | undefined {
+ const currentPathname = this.props.routeInfo.pathname;
+
+ // Prevent out-of-scope outlets from adopting unrelated routes.
+ // Uses segment-aware comparison: /tabs-secondary must NOT match /tabs scope.
+ if (this.outletMountPath && !isPathnameInScope(currentPathname, this.outletMountPath)) {
+ return undefined;
+ }
+
+ // Fast path: mount path is known from RouteContext. The parent path IS the
+ // mount path β no need to run the iterative computeParentPath algorithm.
+ if (this.outletMountPath && !this.isRootOutlet) {
+ return this.outletMountPath;
+ }
+
+ // Fallback: root outlet or mount path not yet seeded. Run the full
+ // computeParentPath algorithm to discover the parent depth.
+ if (this.ionRouterOutlet) {
+ const routeChildren = extractRouteChildren(this.ionRouterOutlet.props.children);
+ const { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = analyzeRouteChildren(routeChildren);
+
+ if (!this.isRootOutlet || hasRelativeRoutes || hasIndexRoute) {
+ const result = computeParentPath({
+ currentPathname,
+ outletMountPath: this.outletMountPath,
+ routeChildren,
+ hasRelativeRoutes,
+ hasIndexRoute,
+ hasWildcardRoute,
+ });
+
+ if (result.outletMountPath && !this.outletMountPath) {
+ this.outletMountPath = result.outletMountPath;
+ }
+
+ return result.parentPath;
+ }
+ }
+
+ return this.outletMountPath;
+ }
+
+ /**
+ * Finds the entering and leaving view items, handling redirect cases.
+ */
+ private findViewItems(routeInfo: RouteInfo): {
+ enteringViewItem: ViewItem | undefined;
+ leavingViewItem: ViewItem | undefined;
+ } {
+ const enteringViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id);
+ let leavingViewItem = this.context.findLeavingViewItemByRouteInfo(routeInfo, this.id);
+
+ // Try to find leaving view by previous pathname
+ if (!leavingViewItem && routeInfo.prevRouteLastPathname) {
+ leavingViewItem = this.context.findViewItemByPathname(routeInfo.prevRouteLastPathname, this.id);
+ }
+
+ // For redirects where entering === leaving, find the actual previous view
+ if (
+ enteringViewItem &&
+ leavingViewItem &&
+ enteringViewItem === leavingViewItem &&
+ routeInfo.routeAction === 'replace' &&
+ routeInfo.prevRouteLastPathname
+ ) {
+ const actualLeavingView = this.context.findViewItemByPathname(routeInfo.prevRouteLastPathname, this.id);
+ if (actualLeavingView && actualLeavingView !== enteringViewItem) {
+ leavingViewItem = actualLeavingView;
+ }
+ }
+
+ // Handle redirect scenario with no leaving view
+ if (
+ enteringViewItem &&
+ !leavingViewItem &&
+ routeInfo.routeAction === 'replace' &&
+ routeInfo.prevRouteLastPathname
+ ) {
+ const actualLeavingView = this.context.findViewItemByPathname(routeInfo.prevRouteLastPathname, this.id);
+ if (actualLeavingView && actualLeavingView !== enteringViewItem) {
+ leavingViewItem = actualLeavingView;
+ }
+ }
+
+ return { enteringViewItem, leavingViewItem };
+ }
+
+ private shouldUnmountLeavingView(
+ routeInfo: RouteInfo,
+ enteringViewItem: ViewItem | undefined,
+ leavingViewItem: ViewItem | undefined
+ ): boolean {
+ if (!leavingViewItem) {
+ return false;
+ }
+
+ if (routeInfo.routeAction === 'replace') {
+ const leavingRoutePath = leavingViewItem?.reactElement?.props?.path as string | undefined;
+
+ // Never unmount root path or views without a path - needed for back navigation
+ if (!leavingRoutePath || leavingRoutePath === '/' || leavingRoutePath === '') {
+ return false;
+ }
+
+ // Replace actions unmount the leaving view since it's being replaced in history.
+ return true;
+ }
+
+ // For non-replace actions, only unmount for back navigation
+ const isForwardPush = routeInfo.routeAction === 'push' && (routeInfo as any).routeDirection === 'forward';
+ if (!isForwardPush && routeInfo.routeDirection !== 'none' && enteringViewItem !== leavingViewItem) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Handles out-of-scope outlet. Returns true if transition should be aborted.
+ */
+ private handleOutOfScopeOutlet(routeInfo: RouteInfo): boolean {
+ if (!this.outletMountPath || isPathnameInScope(routeInfo.pathname, this.outletMountPath)) {
+ return false;
+ }
+
+ if (this.outOfScopeUnmountTimeout) {
+ clearTimeout(this.outOfScopeUnmountTimeout);
+ this.outOfScopeUnmountTimeout = undefined;
+ }
+
+ // Fire lifecycle events on any visible view before unmounting.
+ // When navigating away from a tabbed section, the parent outlet fires
+ // ionViewDidLeave on the tabs container, but the active tab child page
+ // never receives its own lifecycle events because the core transition
+ // dispatches events with bubbles:false. This ensures tab child pages
+ // get ionViewWillLeave/ionViewDidLeave so useIonViewDidLeave fires.
+ const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
+ allViewsInOutlet.forEach((viewItem) => {
+ if (viewItem.ionPageElement && isViewVisible(viewItem.ionPageElement)) {
+ viewItem.ionPageElement.dispatchEvent(
+ new CustomEvent('ionViewWillLeave', { bubbles: false, cancelable: false })
+ );
+ viewItem.ionPageElement.dispatchEvent(
+ new CustomEvent('ionViewDidLeave', { bubbles: false, cancelable: false })
+ );
+ }
+ });
+
+ // Remove view items from the stack but do NOT apply ion-page-hidden.
+ // ion-page-hidden sets display:none which immediately removes content
+ // from the layout, causing the parent outlet's leaving page to flash
+ // blank during its transition animation (issue #25477).
+ //
+ // Removing from the stack triggers React reconciliation via forceUpdate,
+ // which removes the DOM elements. React batches this re-render after all
+ // componentDidUpdate calls in the current cycle, so the parent outlet's
+ // commit() captures the current DOM state (with content visible) before
+ // React processes the removal. The compositor's cached layer is unaffected
+ // by subsequent DOM changes during the animation.
+ allViewsInOutlet.forEach((viewItem) => {
+ this.context.unMountViewItem(viewItem);
+ });
+
+ this.forceUpdate();
+ return true;
+ }
+
+ /**
+ * Handles nested outlet with relative routes but no parent path. Returns true to abort.
+ */
+ private handleOutOfContextNestedOutlet(
+ parentPath: string | undefined,
+ leavingViewItem: ViewItem | undefined
+ ): boolean {
+ if (this.isRootOutlet || parentPath !== undefined || !this.ionRouterOutlet) {
+ return false;
+ }
+
+ const routesChildren =
+ getRoutesChildren(this.ionRouterOutlet.props.children) ?? this.ionRouterOutlet.props.children;
+ const routeChildren = React.Children.toArray(routesChildren).filter(
+ (child): child is React.ReactElement =>
+ React.isValidElement(child) && (child.type === Route || child.type === IonRoute)
+ );
+
+ const hasRelativeRoutes = routeChildren.some((route) => {
+ const path = route.props.path;
+ return path && !path.startsWith('/') && path !== '*';
+ });
+
+ if (hasRelativeRoutes) {
+ hideIonPageElement(leavingViewItem?.ionPageElement);
+ if (leavingViewItem) {
+ leavingViewItem.mount = false;
+ }
+ this.forceUpdate();
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Handles nested outlet with no matching route. Returns true to abort.
+ */
+ private handleNoMatchingRoute(
+ enteringRoute: React.ReactElement | undefined,
+ enteringViewItem: ViewItem | undefined,
+ leavingViewItem: ViewItem | undefined
+ ): boolean {
+ if (this.isRootOutlet || enteringRoute || enteringViewItem) {
+ return false;
+ }
+
+ hideIonPageElement(leavingViewItem?.ionPageElement);
+ if (leavingViewItem) {
+ leavingViewItem.mount = false;
+ }
+ this.forceUpdate();
+ return true;
+ }
+
+ /**
+ * Handles transition when entering view has ion-page element ready.
+ */
+ private handleReadyEnteringView(
+ routeInfo: RouteInfo,
+ enteringViewItem: ViewItem,
+ leavingViewItem: ViewItem | undefined,
+ shouldUnmountLeavingViewItem: boolean
+ ): void {
+ const routePath = enteringViewItem.reactElement?.props?.path as string | undefined;
+ const isParameterizedRoute = routePath ? routePath.includes(':') : false;
+ const isWildcardContainerRoute = routePath ? routePath.endsWith('/*') : false;
+
+ // Handle same-view transitions (parameterized routes like /user/:id or container routes like /tabs/*)
+ // When entering === leaving, the view is already visible - skip transition to prevent flash
+ if (enteringViewItem === leavingViewItem) {
+ if (isParameterizedRoute || isWildcardContainerRoute) {
+ const updatedMatch = matchComponent(enteringViewItem.reactElement, routeInfo.pathname, true, this.outletMountPath);
+ if (updatedMatch) {
+ enteringViewItem.routeData.match = updatedMatch;
+ }
+
+ const enteringEl = enteringViewItem.ionPageElement;
+ if (enteringEl) {
+ showIonPageElement(enteringEl);
+ enteringEl.classList.remove('ion-page-invisible');
+ }
+
+ this.forceUpdate();
+ return;
+ }
+ }
+
+ // For wildcard container routes, check if we're navigating within the same container.
+ // If both the current pathname and the previous pathname match the same container route,
+ // skip the transition - the nested outlet will handle the actual page change.
+ // This handles cases where leavingViewItem lookup fails (e.g., no IonPage wrapper).
+ if (isWildcardContainerRoute && routeInfo.lastPathname) {
+ // routePath is guaranteed to exist since isWildcardContainerRoute checks routePath?.endsWith('/*')
+ const containerBase = routePath!.replace(/\/\*$/, '');
+ const currentInContainer =
+ routeInfo.pathname.startsWith(containerBase + '/') || routeInfo.pathname === containerBase;
+ const previousInContainer =
+ routeInfo.lastPathname.startsWith(containerBase + '/') || routeInfo.lastPathname === containerBase;
+
+ if (currentInContainer && previousInContainer) {
+ const updatedMatch = matchComponent(enteringViewItem.reactElement, routeInfo.pathname, true, this.outletMountPath);
+ if (updatedMatch) {
+ enteringViewItem.routeData.match = updatedMatch;
+ }
+ this.forceUpdate();
+ return;
+ }
+ }
+
+ if (!leavingViewItem && this.props.routeInfo.prevRouteLastPathname) {
+ leavingViewItem = this.context.findViewItemByPathname(this.props.routeInfo.prevRouteLastPathname, this.id);
+ }
+
+ // Re-mount views that were previously unmounted (e.g., navigating back to home)
+ if (!enteringViewItem.mount) {
+ enteringViewItem.mount = true;
}
+
+ // Check visibility state BEFORE showing entering view
+ const enteringWasVisible = enteringViewItem.ionPageElement && isViewVisible(enteringViewItem.ionPageElement);
+ const leavingIsHidden =
+ leavingViewItem !== undefined && leavingViewItem.ionPageElement && !isViewVisible(leavingViewItem.ionPageElement);
+
+ const currentTransition = {
+ enteringId: enteringViewItem.id,
+ leavingId: leavingViewItem?.id,
+ };
+
+ const isDuplicateTransition =
+ leavingViewItem &&
+ this.lastTransition &&
+ this.lastTransition.leavingId &&
+ this.lastTransition.enteringId === currentTransition.enteringId &&
+ this.lastTransition.leavingId === currentTransition.leavingId;
+
+ // Skip if transition already performed (e.g., via swipe gesture)
+ if (enteringWasVisible && leavingIsHidden && isDuplicateTransition) {
+ if (
+ this.skipTransition &&
+ shouldUnmountLeavingViewItem &&
+ leavingViewItem &&
+ enteringViewItem !== leavingViewItem
+ ) {
+ leavingViewItem.mount = false;
+ // Trigger ionViewDidLeave lifecycle for ViewLifeCycleManager cleanup
+ this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back');
+ }
+ this.skipTransition = false;
+ this.forceUpdate();
+ return;
+ }
+
+ showIonPageElement(enteringViewItem.ionPageElement);
+
+ // Handle duplicate transition or swipe gesture completion
+ if (isDuplicateTransition || this.skipTransition) {
+ if (
+ this.skipTransition &&
+ shouldUnmountLeavingViewItem &&
+ leavingViewItem &&
+ enteringViewItem !== leavingViewItem
+ ) {
+ leavingViewItem.mount = false;
+ // Re-fire ionViewDidLeave since gesture completed before mount=false was set
+ this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back');
+ }
+ this.skipTransition = false;
+ this.forceUpdate();
+ return;
+ }
+
+ this.lastTransition = currentTransition;
+
+ const shouldSkipAnimation = this.applySkipAnimationIfNeeded(enteringViewItem, leavingViewItem);
+
+ this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, undefined, false, shouldSkipAnimation);
+
+ if (shouldUnmountLeavingViewItem && leavingViewItem && enteringViewItem !== leavingViewItem) {
+ // For non-replace actions (back nav), set mount=false here to hide the view.
+ // For replace actions, handleLeavingViewUnmount sets mount=false only after
+ // its container-to-container guard passes, avoiding zombie state.
+ if (routeInfo.routeAction !== 'replace') {
+ leavingViewItem.mount = false;
+ }
+ this.handleLeavingViewUnmount(routeInfo, enteringViewItem, leavingViewItem);
+ }
+
+ // Clean up orphaned sibling views after replace actions (redirects)
+ this.cleanupOrphanedSiblingViews(routeInfo, enteringViewItem, leavingViewItem);
+ }
+
+ /**
+ * Handles leaving view unmount for replace actions.
+ */
+ private handleLeavingViewUnmount(routeInfo: RouteInfo, enteringViewItem: ViewItem, leavingViewItem: ViewItem): void {
+ // Only replace actions unmount views; push/pop cache for navigation history
+ if (routeInfo.routeAction !== 'replace') {
+ return;
+ }
+
+ if (!leavingViewItem.ionPageElement) {
+ leavingViewItem.mount = false;
+ const viewToUnmount = leavingViewItem;
+ setTimeout(() => {
+ this.context.unMountViewItem(viewToUnmount);
+ this.forceUpdate();
+ }, VIEW_UNMOUNT_DELAY_MS);
+ return;
+ }
+
+ const enteringRoutePath = enteringViewItem.reactElement?.props?.path as string | undefined;
+ const leavingRoutePath = leavingViewItem.reactElement?.props?.path as string | undefined;
+ const isEnteringContainerRoute = enteringRoutePath && enteringRoutePath.endsWith('/*');
+ const isLeavingSpecificRoute =
+ leavingRoutePath &&
+ leavingRoutePath !== '' &&
+ leavingRoutePath !== '*' &&
+ !leavingRoutePath.endsWith('/*') &&
+ !leavingViewItem.reactElement?.props?.index;
+
+ // Skip removal for container-to-container transitions (e.g., /tabs/* β /settings/*).
+ // These routes manage their own nested outlets; unmounting would disrupt child views.
+ if (isEnteringContainerRoute && !isLeavingSpecificRoute) {
+ return;
+ }
+
+ leavingViewItem.mount = false;
+
+ const viewToUnmount = leavingViewItem;
+ setTimeout(() => {
+ this.context.unMountViewItem(viewToUnmount);
+ this.forceUpdate();
+ }, VIEW_UNMOUNT_DELAY_MS);
+ }
+
+ /**
+ * Cleans up orphaned sibling views after replace actions or push-to-container navigations.
+ */
+ private cleanupOrphanedSiblingViews(
+ routeInfo: RouteInfo,
+ enteringViewItem: ViewItem,
+ leavingViewItem: ViewItem | undefined
+ ): void {
+ const enteringRoutePath = enteringViewItem.reactElement?.props?.path as string | undefined;
+ if (!enteringRoutePath) {
+ return;
+ }
+
+ const leavingRoutePath = leavingViewItem?.reactElement?.props?.path as string | undefined;
+ const isContainerRoute = (path: string | undefined) => path?.endsWith('/*');
+
+ const isReplaceAction = routeInfo.routeAction === 'replace';
+ const isPushToContainer =
+ routeInfo.routeAction === 'push' && routeInfo.routeDirection === 'none' && isContainerRoute(enteringRoutePath);
+
+ if (!isReplaceAction && !isPushToContainer) {
+ return;
+ }
+
+ // Skip cleanup for tab switches
+ const isSameView = enteringViewItem === leavingViewItem;
+ const isSameContainerRoute = isContainerRoute(enteringRoutePath) && leavingRoutePath === enteringRoutePath;
+ const isNavigatingWithinContainer =
+ isPushToContainer &&
+ !leavingViewItem &&
+ routeInfo.prevRouteLastPathname?.startsWith(enteringRoutePath.replace(/\/\*$/, ''));
+
+ if (isSameView || isSameContainerRoute || isNavigatingWithinContainer) {
+ return;
+ }
+
+ const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
+ const areSiblingRoutes = (path1: string, path2: string): boolean => {
+ const path1IsRelative = !path1.startsWith('/');
+ const path2IsRelative = !path2.startsWith('/');
+
+ if (path1IsRelative && path2IsRelative) {
+ const path1Depth = path1.replace(/\/\*$/, '').split('/').filter(Boolean).length;
+ const path2Depth = path2.replace(/\/\*$/, '').split('/').filter(Boolean).length;
+ return path1Depth === path2Depth && path1Depth <= 1;
+ }
+
+ const getParent = (path: string) => {
+ const normalized = path.replace(/\/\*$/, '');
+ const segments = normalized.split('/').filter(Boolean);
+ // Strip trailing parameter segments (e.g., :id) so that
+ // sibling routes like /items/list/:id and /items/detail/:id
+ // resolve to the same parent (/items).
+ while (segments.length > 0 && segments[segments.length - 1].startsWith(':')) {
+ segments.pop();
+ }
+ segments.pop();
+ return segments.length > 0 ? '/' + segments.join('/') : '/';
+ };
+
+ const parent = getParent(path1);
+ // Exclude root-level routes from sibling detection to avoid unintended
+ // cleanup of unrelated top-level routes. Also covers single-depth param
+ // routes (e.g., /items/:id) which resolve to root after param stripping.
+ if (parent === '/') {
+ return false;
+ }
+ return parent === getParent(path2);
+ };
+
+ for (const viewItem of allViewsInOutlet) {
+ const viewRoutePath = viewItem.reactElement?.props?.path as string | undefined;
+
+ const shouldSkip =
+ viewItem.id === enteringViewItem.id ||
+ (leavingViewItem && viewItem.id === leavingViewItem.id) ||
+ !viewItem.mount ||
+ !viewRoutePath ||
+ // Don't clean up container routes when entering a container route
+ // (e.g., /tabs/* and /settings/* coexist for tab switching)
+ (viewRoutePath.endsWith('/*') && enteringRoutePath.endsWith('/*'));
+
+ if (shouldSkip) {
+ continue;
+ }
+
+ const isOrphanedSpecificRoute = !viewRoutePath.endsWith('/*');
+
+ // Clean up sibling non-container routes that are no longer reachable.
+ let shouldCleanup = false;
+ if ((isReplaceAction || isPushToContainer) && isOrphanedSpecificRoute) {
+ shouldCleanup = areSiblingRoutes(enteringRoutePath, viewRoutePath);
+ }
+
+ if (shouldCleanup) {
+ hideIonPageElement(viewItem.ionPageElement);
+ viewItem.mount = false;
+
+ const viewToRemove = viewItem;
+ setTimeout(() => {
+ this.context.unMountViewItem(viewToRemove);
+ this.forceUpdate();
+ }, VIEW_UNMOUNT_DELAY_MS);
+ }
+ }
+ }
+
+ /**
+ * Determines whether to skip the transition animation and, if so, immediately
+ * hides the leaving view with inline `visibility:hidden`.
+ *
+ * Skips transitions only for outlets nested inside a parent IonPage's content
+ * area (i.e., an ion-content sits between the outlet and the .ion-page). These
+ * outlets render child pages inside a parent page's scrollable area, and the MD
+ * animation shows both entering and leaving pages simultaneously β causing text
+ * overlap and nested scrollbars. Standard page-level outlets (tabs, routing,
+ * swipe-to-go-back) animate normally even though they sit inside a framework-
+ * managed .ion-page wrapper from the parent outlet's view stack.
+ *
+ * Uses inline visibility:hidden rather than ion-page-hidden class because
+ * core's beforeTransition() removes ion-page-hidden via setPageHidden().
+ * Inline visibility:hidden survives that removal, keeping the page hidden
+ * until React unmounts it after ionViewDidLeave fires. Unlike display:none,
+ * visibility:hidden preserves element geometry so commit() animations
+ * can resolve normally.
+ */
+ private applySkipAnimationIfNeeded(
+ enteringViewItem: ViewItem,
+ leavingViewItem: ViewItem | undefined
+ ): boolean {
+ // Only skip for outlets genuinely nested inside a page's content area.
+ // Walk from the outlet up to the nearest .ion-page; if an ion-content
+ // sits in between, the outlet is inside scrollable page content and
+ // animating would cause overlapping pages with duplicate scrollbars.
+ let isInsidePageContent = false;
+ let el: HTMLElement | null = this.routerOutletElement?.parentElement ?? null;
+ while (el) {
+ if (el.classList.contains('ion-page')) break;
+ if (el.tagName === 'ION-CONTENT') {
+ isInsidePageContent = true;
+ break;
+ }
+ el = el.parentElement;
+ }
+
+ const shouldSkip = isInsidePageContent && !!leavingViewItem && enteringViewItem !== leavingViewItem;
+
+ if (shouldSkip && leavingViewItem?.ionPageElement) {
+ leavingViewItem.ionPageElement.style.setProperty('visibility', 'hidden');
+ leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
+ }
+
+ return shouldSkip;
+ }
+
+ /**
+ * Handles entering view with no ion-page element yet (waiting for render).
+ */
+ private handleWaitingForIonPage(
+ routeInfo: RouteInfo,
+ enteringViewItem: ViewItem,
+ leavingViewItem: ViewItem | undefined,
+ shouldUnmountLeavingViewItem: boolean
+ ): void {
+ const enteringRouteElement = enteringViewItem.reactElement?.props?.element;
+
+ // Handle Navigate components (they never render an IonPage)
+ if (isNavigateElement(enteringRouteElement)) {
+ this.waitingForIonPage = false;
+ if (this.ionPageWaitTimeout) {
+ clearTimeout(this.ionPageWaitTimeout);
+ this.ionPageWaitTimeout = undefined;
+ }
+ this.pendingPageTransition = false;
+
+ // Hide ALL other visible views in this outlet for Navigate redirects.
+ // Same rationale as the timeout path: intermediate redirects can shift
+ // the leaving view reference, leaving the original page visible.
+ const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
+ allViewsInOutlet.forEach((viewItem) => {
+ if (viewItem.id !== enteringViewItem.id && viewItem.ionPageElement) {
+ hideIonPageElement(viewItem.ionPageElement);
+ }
+ });
+
+ // Don't unmount if entering and leaving are the same view item
+ if (shouldUnmountLeavingViewItem && leavingViewItem && enteringViewItem !== leavingViewItem) {
+ if (routeInfo.routeAction !== 'replace') {
+ leavingViewItem.mount = false;
+ }
+ this.handleLeavingViewUnmount(routeInfo, enteringViewItem, leavingViewItem);
+ }
+
+ this.forceUpdate();
+ return;
+ }
+
+ // Do not hide the leaving view here - wait until the entering view is ready.
+ // Hiding the leaving view while the entering view is still mounting causes a flash
+ // where both views are hidden/invisible simultaneously.
+ // The leaving view will be hidden in transitionPage() after the entering view is visible.
+
+ this.waitingForIonPage = true;
+
+ if (this.ionPageWaitTimeout) {
+ clearTimeout(this.ionPageWaitTimeout);
+ }
+
+ this.ionPageWaitTimeout = setTimeout(() => {
+ this.ionPageWaitTimeout = undefined;
+
+ if (!this.waitingForIonPage) {
+ return;
+ }
+ this.waitingForIonPage = false;
+
+ const latestEnteringView = this.context.findViewItemByRouteInfo(routeInfo, this.id) ?? enteringViewItem;
+ const latestLeavingView = this.context.findLeavingViewItemByRouteInfo(routeInfo, this.id) ?? leavingViewItem;
+
+ if (latestEnteringView?.ionPageElement) {
+ const shouldSkipAnimation = this.applySkipAnimationIfNeeded(latestEnteringView, latestLeavingView ?? undefined);
+ this.transitionPage(routeInfo, latestEnteringView, latestLeavingView ?? undefined, undefined, false, shouldSkipAnimation);
+
+ if (shouldUnmountLeavingViewItem && latestLeavingView && latestEnteringView !== latestLeavingView) {
+ if (routeInfo.routeAction !== 'replace') {
+ latestLeavingView.mount = false;
+ }
+ this.handleLeavingViewUnmount(routeInfo, latestEnteringView, latestLeavingView);
+ }
+
+ this.forceUpdate();
+ } else {
+ /**
+ * Timeout fired and entering view still has no ionPageElement.
+ * This happens for container routes that render nested outlets without a direct IonPage.
+ * Hide ALL other visible views in this outlet, not just the computed leaving view.
+ * This handles cases where intermediate redirects (e.g., Navigate in nested routes)
+ * change the leaving view reference, leaving the original page still visible.
+ */
+ const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
+ allViewsInOutlet.forEach((viewItem) => {
+ if (viewItem.id !== latestEnteringView.id && viewItem.ionPageElement) {
+ hideIonPageElement(viewItem.ionPageElement);
+ }
+ });
+ this.forceUpdate();
+
+ // Safety net: after forceUpdate triggers a React render cycle, check if
+ // any pages in this outlet are stuck with ion-page-invisible. This can
+ // happen when view lookup fails (e.g., wildcard-to-index transitions
+ // where the view item gets corrupted). The forceUpdate above causes
+ // React to render the correct component, but ion-page-invisible may
+ // persist if no transition runs for that page.
+ setTimeout(() => {
+ if (!this._isMounted || !this.routerOutletElement) return;
+ const stuckPages = this.routerOutletElement.querySelectorAll(':scope > .ion-page-invisible');
+ stuckPages.forEach((page) => {
+ page.classList.remove('ion-page-invisible');
+ });
+ }, ION_PAGE_WAIT_TIMEOUT_MS);
+ }
+ }, ION_PAGE_WAIT_TIMEOUT_MS);
+
+ this.forceUpdate();
+ }
+
+ /**
+ * Gets the route info to use for finding views during swipe-to-go-back gestures.
+ * This pattern is used in multiple places in setupRouterOutlet.
+ */
+ private getSwipeBackRouteInfo(): RouteInfo {
+ const { routeInfo } = this.props;
+ return this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute
+ ? this.prevProps.routeInfo
+ : ({ pathname: routeInfo.pushedByRoute || '' } as RouteInfo);
+ }
+
+ componentDidMount() {
+ this._isMounted = true;
if (this.routerOutletElement) {
this.setupRouterOutlet(this.routerOutletElement);
this.handlePageTransition(this.props.routeInfo);
@@ -76,124 +814,211 @@ export class StackManager extends React.PureComponent {
+ hideIonPageElement(viewItem.ionPageElement);
+ });
+
+ this.context.clearOutlet(this.id);
}
+ /**
+ * Sets the transition between pages within this router outlet.
+ * This function determines the entering and leaving views based on the
+ * provided route information and triggers the appropriate animation.
+ * It also handles scenarios like initial loads, back navigation, and
+ * navigation to the same view with different parameters.
+ *
+ * @param routeInfo It contains info about the current route,
+ * the previous route, and the action taken (e.g., push, replace).
+ *
+ * @returns A promise that resolves when the transition is complete.
+ * If no transition is needed or if the router outlet isn't ready,
+ * the Promise may resolve immediately.
+ */
async handlePageTransition(routeInfo: RouteInfo) {
+ // Wait for router outlet to mount
if (!this.routerOutletElement || !this.routerOutletElement.commit) {
- /**
- * The route outlet has not mounted yet. We need to wait for it to render
- * before we can transition the page.
- *
- * Set a flag to indicate that we should transition the page after
- * the component has updated.
- */
this.pendingPageTransition = true;
- } else {
- let enteringViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id);
- let leavingViewItem = this.context.findLeavingViewItemByRouteInfo(routeInfo, this.id);
+ return;
+ }
- if (!leavingViewItem && routeInfo.prevRouteLastPathname) {
- leavingViewItem = this.context.findViewItemByPathname(routeInfo.prevRouteLastPathname, this.id);
- }
+ // Find entering and leaving view items
+ const viewItems = this.findViewItems(routeInfo);
+ let enteringViewItem = viewItems.enteringViewItem;
+ const leavingViewItem = viewItems.leavingViewItem;
+ const shouldUnmountLeavingViewItem = this.shouldUnmountLeavingView(routeInfo, enteringViewItem, leavingViewItem);
- // Check if leavingViewItem should be unmounted
- if (leavingViewItem) {
- if (routeInfo.routeAction === 'replace') {
- leavingViewItem.mount = false;
- } else if (!(routeInfo.routeAction === 'push' && routeInfo.routeDirection === 'forward')) {
- if (routeInfo.routeDirection !== 'none' && enteringViewItem !== leavingViewItem) {
- leavingViewItem.mount = false;
- }
- } else if (routeInfo.routeOptions?.unmount) {
- leavingViewItem.mount = false;
- }
- }
+ // Get parent path for nested outlets
+ const parentPath = this.getParentPath();
- const enteringRoute = matchRoute(this.ionRouterOutlet?.props.children, routeInfo) as React.ReactElement;
+ // Handle out-of-scope outlet (route outside mount path)
+ if (this.handleOutOfScopeOutlet(routeInfo)) {
+ return;
+ }
- if (enteringViewItem) {
- enteringViewItem.reactElement = enteringRoute;
- } else if (enteringRoute) {
- enteringViewItem = this.context.createViewItem(this.id, enteringRoute, routeInfo);
- this.context.addViewItem(enteringViewItem);
- }
+ // Clear any pending out-of-scope unmount timeout
+ if (this.outOfScopeUnmountTimeout) {
+ clearTimeout(this.outOfScopeUnmountTimeout);
+ this.outOfScopeUnmountTimeout = undefined;
+ }
- if (enteringViewItem && enteringViewItem.ionPageElement) {
- /**
- * If the entering view item is the same as the leaving view item,
- * then we don't need to transition.
- */
- if (enteringViewItem === leavingViewItem) {
- /**
- * If the entering view item is the same as the leaving view item,
- * we are either transitioning using parameterized routes to the same view
- * or a parent router outlet is re-rendering as a result of React props changing.
- *
- * If the route data does not match the current path, the parent router outlet
- * is attempting to transition and we cancel the operation.
- */
- if (enteringViewItem.routeData.match.url !== routeInfo.pathname) {
- return;
- }
- }
+ // Handle nested outlet with relative routes but no valid parent path
+ if (this.handleOutOfContextNestedOutlet(parentPath, leavingViewItem)) {
+ return;
+ }
- /**
- * If there isn't a leaving view item, but the route info indicates
- * that the user has routed from a previous path, then we need
- * to find the leaving view item to transition between.
- */
- if (!leavingViewItem && this.props.routeInfo.prevRouteLastPathname) {
- leavingViewItem = this.context.findViewItemByPathname(this.props.routeInfo.prevRouteLastPathname, this.id);
- }
+ // Find the matching route element
+ const enteringRoute = findRouteByRouteInfo(
+ this.ionRouterOutlet?.props.children,
+ routeInfo,
+ parentPath
+ ) as React.ReactElement;
- /**
- * If the entering view is already visible and the leaving view is not, the transition does not need to occur.
- */
- if (
- isViewVisible(enteringViewItem.ionPageElement) &&
- leavingViewItem !== undefined &&
- !isViewVisible(leavingViewItem.ionPageElement!)
- ) {
- return;
- }
+ // Handle nested outlet with no matching route
+ if (this.handleNoMatchingRoute(enteringRoute, enteringViewItem, leavingViewItem)) {
+ return;
+ }
- /**
- * The view should only be transitioned in the following cases:
- * 1. Performing a replace or pop action, such as a swipe to go back gesture
- * to animation the leaving view off the screen.
- *
- * 2. Navigating between top-level router outlets, such as /page-1 to /page-2;
- * or navigating within a nested outlet, such as /tabs/tab-1 to /tabs/tab-2.
- *
- * 3. The entering view is an ion-router-outlet containing a page
- * matching the current route and that hasn't already transitioned in.
- *
- * This should only happen when navigating directly to a nested router outlet
- * route or on an initial page load (i.e. refreshing). In cases when loading
- * /tabs/tab-1, we need to transition the /tabs page element into the view.
- */
- this.transitionPage(routeInfo, enteringViewItem, leavingViewItem);
- } else if (leavingViewItem && !enteringRoute && !enteringViewItem) {
- // If we have a leavingView but no entering view/route, we are probably leaving to
- // another outlet, so hide this leavingView. We do it in a timeout to give time for a
- // transition to finish.
- // setTimeout(() => {
- if (leavingViewItem.ionPageElement) {
- leavingViewItem.ionPageElement.classList.add('ion-page-hidden');
- leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
- }
- // }, 250);
+ // Create or update the entering view item
+ if (enteringViewItem && enteringRoute) {
+ enteringViewItem.reactElement = enteringRoute;
+ } else if (enteringRoute) {
+ enteringViewItem = this.context.createViewItem(this.id, enteringRoute, routeInfo);
+ this.context.addViewItem(enteringViewItem);
+ }
+
+ // Handle transition based on ion-page element availability
+ // Check if the ionPageElement is still in the document.
+ // If the view was previously unmounted (mount=false), the ViewLifeCycleManager
+ // removes the React component from the tree, which removes the IonPage from the DOM.
+ // The ionPageElement reference becomes stale and we need to wait for a new one.
+ const ionPageIsInDocument =
+ enteringViewItem?.ionPageElement && document.body.contains(enteringViewItem.ionPageElement);
+
+ if (enteringViewItem && ionPageIsInDocument) {
+ // Clear waiting state
+ if (this.waitingForIonPage) {
+ this.waitingForIonPage = false;
+ }
+ if (this.ionPageWaitTimeout) {
+ clearTimeout(this.ionPageWaitTimeout);
+ this.ionPageWaitTimeout = undefined;
}
- this.forceUpdate();
+ this.handleReadyEnteringView(routeInfo, enteringViewItem, leavingViewItem, shouldUnmountLeavingViewItem);
+ } else if (enteringViewItem && !ionPageIsInDocument) {
+ // Wait for ion-page to mount
+ // This handles both: no ionPageElement, or stale ionPageElement (not in document)
+ // Clear stale reference if the element is no longer in the document
+ if (enteringViewItem.ionPageElement && !document.body.contains(enteringViewItem.ionPageElement)) {
+ enteringViewItem.ionPageElement = undefined;
+ }
+ // Ensure the view is marked as mounted so ViewLifeCycleManager renders the IonPage
+ if (!enteringViewItem.mount) {
+ enteringViewItem.mount = true;
+ }
+ this.handleWaitingForIonPage(routeInfo, enteringViewItem, leavingViewItem, shouldUnmountLeavingViewItem);
+ return;
+ } else if (!enteringViewItem && !enteringRoute) {
+ // No view or route found - likely leaving to another outlet
+ if (leavingViewItem) {
+ hideIonPageElement(leavingViewItem.ionPageElement);
+ if (shouldUnmountLeavingViewItem) {
+ leavingViewItem.mount = false;
+ }
+ }
}
+
+ this.forceUpdate();
}
+ /**
+ * Registers an `` DOM element with the `StackManager`.
+ * This is called when `` has been mounted.
+ *
+ * @param page The element of the rendered ``.
+ * @param routeInfo The route information that associates with ``.
+ */
registerIonPage(page: HTMLElement, routeInfo: RouteInfo) {
+ /**
+ * DO NOT remove ion-page-invisible here.
+ *
+ * PageManager's ref callback adds ion-page-invisible synchronously to prevent flash.
+ * At this point, the div exists but its CHILDREN (header, toolbar, menu-button)
+ * have NOT rendered yet. If we remove ion-page-invisible now, the page becomes visible
+ * with empty/incomplete content, causing a flicker (especially for ion-menu-button which
+ * starts with menu-button-hidden class).
+ *
+ * Instead, let transitionPage handle visibility AFTER waiting for components to be ready.
+ * This ensures the page only becomes visible when its content is fully rendered.
+ */
+ this.waitingForIonPage = false;
+ if (this.ionPageWaitTimeout) {
+ clearTimeout(this.ionPageWaitTimeout);
+ this.ionPageWaitTimeout = undefined;
+ }
+ this.pendingPageTransition = false;
+
const foundView = this.context.findViewItemByRouteInfo(routeInfo, this.id);
+
if (foundView) {
const oldPageElement = foundView.ionPageElement;
+
+ /**
+ * FIX for issue #28878: Reject orphaned IonPage registrations.
+ *
+ * When a component conditionally renders different IonPages (e.g., list vs empty state)
+ * using React keys, and state changes simultaneously with navigation, the new IonPage
+ * tries to register for a route we're navigating away from. This creates a stale view.
+ *
+ * Only reject if both pageIds exist and differ, to allow nested outlet registrations.
+ */
+ if (this.shouldRejectOrphanedPage(page, oldPageElement, routeInfo)) {
+ this.hideAndRemoveOrphanedPage(page);
+ return;
+ }
+
+ /**
+ * Don't let a nested element (e.g., ion-router-outlet with ionPage prop)
+ * override an existing IonPage registration when the existing element is
+ * an ancestor of the new one. This ensures ionPageElement always points
+ * to the outermost IonPage, which is needed to properly hide the entire
+ * page during back navigation (not just the inner outlet).
+ */
+ if (oldPageElement && oldPageElement !== page && oldPageElement.isConnected && oldPageElement.contains(page)) {
+ return;
+ }
+
foundView.ionPageElement = page;
foundView.ionRoute = true;
@@ -209,6 +1034,42 @@ export class StackManager extends React.PureComponent {
+ if (page.parentElement) {
+ page.remove();
+ }
+ }, VIEW_UNMOUNT_DELAY_MS);
+ }
+
+ /**
+ * Configures swipe-to-go-back gesture for the router outlet.
+ */
async setupRouterOutlet(routerOutlet: HTMLIonRouterOutletElement) {
const canStart = () => {
const config = getConfig();
@@ -218,88 +1079,75 @@ export class StackManager extends React.PureComponent {
const { routeInfo } = this.props;
-
- const propsToUse =
- this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute
- ? this.prevProps.routeInfo
- : ({ pathname: routeInfo.pushedByRoute || '' } as any);
- const enteringViewItem = this.context.findViewItemByRouteInfo(propsToUse, this.id, false);
+ const swipeBackRouteInfo = this.getSwipeBackRouteInfo();
+ // First try to find the view in the current outlet, then search all outlets
+ let enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, this.id, false);
+ if (!enteringViewItem) {
+ enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, undefined, false);
+ }
const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
- /**
- * When the gesture starts, kick off
- * a transition that is controlled
- * via a swipe gesture.
- */
+ // Ensure the entering view is mounted so React keeps rendering it during the gesture.
+ // This is important when the view was previously marked for unmount but its
+ // ionPageElement is still in the DOM.
+ if (enteringViewItem && !enteringViewItem.mount) {
+ enteringViewItem.mount = true;
+ }
+
+ // When the gesture starts, kick off a transition controlled via swipe gesture
if (enteringViewItem && leavingViewItem) {
await this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back', true);
}
return Promise.resolve();
};
+
const onEnd = (shouldContinue: boolean) => {
if (shouldContinue) {
+ // User finished the swipe gesture, so complete the back navigation
this.skipTransition = true;
-
this.context.goBack();
} else {
- /**
- * In the event that the swipe
- * gesture was aborted, we should
- * re-hide the page that was going to enter.
- */
+ // Swipe gesture was aborted - re-hide the page that was going to enter
const { routeInfo } = this.props;
-
- const propsToUse =
- this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute
- ? this.prevProps.routeInfo
- : ({ pathname: routeInfo.pushedByRoute || '' } as any);
- const enteringViewItem = this.context.findViewItemByRouteInfo(propsToUse, this.id, false);
+ const swipeBackRouteInfo = this.getSwipeBackRouteInfo();
+ // First try to find the view in the current outlet, then search all outlets
+ let enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, this.id, false);
+ if (!enteringViewItem) {
+ enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, undefined, false);
+ }
const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
- /**
- * Ionic React has a design defect where it
- * a) Unmounts the leaving view item when using parameterized routes
- * b) Considers the current view to be the entering view when using
- * parameterized routes
- *
- * As a result, we should not hide the view item here
- * as it will cause the current view to be hidden.
- */
+ // Don't hide if entering and leaving are the same (parameterized route edge case)
if (enteringViewItem !== leavingViewItem && enteringViewItem?.ionPageElement !== undefined) {
- const { ionPageElement } = enteringViewItem;
- ionPageElement.setAttribute('aria-hidden', 'true');
- ionPageElement.classList.add('ion-page-hidden');
+ hideIonPageElement(enteringViewItem.ionPageElement);
}
}
};
@@ -311,12 +1159,29 @@ export class StackManager extends React.PureComponent {
const skipTransition = this.skipTransition;
@@ -345,16 +1210,40 @@ export class StackManager extends React.PureComponent((resolve) => setTimeout(() => resolve('timeout'), timeoutMs));
+ const result = await Promise.race([commitPromise.then(() => 'done' as const), timeoutPromise]);
+
+ if (result === 'timeout') {
+ // Force entering page visible even though commit hung
+ enteringEl.classList.remove('ion-page-invisible');
+ }
+
+ if (!progressAnimation) {
+ enteringEl.classList.remove('ion-page-invisible');
+ }
};
const routerOutlet = this.routerOutletElement!;
@@ -365,10 +1254,8 @@ export class StackManager extends React.PureComponent {
+ return new Promise((resolve) => {
+ const checkReady = () => {
+ const ionicComponents = enteringEl.querySelectorAll(
+ 'ion-header, ion-toolbar, ion-buttons, ion-menu-button, ion-title, ion-content'
+ );
+ const allHydrated = Array.from(ionicComponents).every((el) => el.classList.contains('hydrated'));
+
+ const menuButtons = enteringEl.querySelectorAll('ion-menu-button');
+ const menuButtonsReady = Array.from(menuButtons).every(
+ (el) => !el.classList.contains('menu-button-hidden')
+ );
+
+ return allHydrated && menuButtonsReady;
+ };
+
+ if (checkReady()) {
+ resolve();
+ return;
+ }
+
+ let resolved = false;
+ const observer = new MutationObserver(() => {
+ if (!resolved && checkReady()) {
+ resolved = true;
+ observer.disconnect();
+ if (this.transitionObserver === observer) {
+ this.transitionObserver = undefined;
+ }
+ resolve();
+ }
+ });
+
+ // Disconnect any previous observer before tracking the new one
+ if (this.transitionObserver) {
+ this.transitionObserver.disconnect();
+ }
+ this.transitionObserver = observer;
+
+ observer.observe(enteringEl, {
+ subtree: true,
+ attributes: true,
+ attributeFilter: ['class'],
+ });
+
+ setTimeout(() => {
+ if (!resolved) {
+ resolved = true;
+ observer.disconnect();
+ if (this.transitionObserver === observer) {
+ this.transitionObserver = undefined;
+ }
+ resolve();
+ }
+ }, 100);
+ });
+ };
+
+ await waitForComponentsReady();
+
+ // Bail out if the component unmounted during waitForComponentsReady
+ if (!this._isMounted) return;
+
+ // Swap visibility synchronously - show entering, hide leaving
+ enteringEl.classList.remove('ion-page-invisible');
+ leavingEl.classList.add('ion-page-hidden');
+ leavingEl.setAttribute('aria-hidden', 'true');
+ } else {
+ await runCommit(enteringViewItem.ionPageElement, leavingEl);
+ if (leavingEl && !progressAnimation) {
+ leavingEl.classList.add('ion-page-hidden');
+ leavingEl.setAttribute('aria-hidden', 'true');
+ }
}
}
}
@@ -392,34 +1385,68 @@ export class StackManager extends React.PureComponent {
- this.forceUpdate();
- });
-
return (
-
- {React.cloneElement(
- ionRouterOutlet as any,
- {
- ref: (node: HTMLIonRouterOutletElement) => {
- if (ionRouterOutlet.props.setRef) {
- ionRouterOutlet.props.setRef(node);
- }
- if (ionRouterOutlet.props.forwardedRef) {
- ionRouterOutlet.props.forwardedRef.current = node;
- }
- this.routerOutletElement = node;
- const { ref } = ionRouterOutlet as any;
- if (typeof ref === 'function') {
- ref(node);
- }
+
+ {(parentContext) => {
+ // Derive the outlet's mount path from React Router's matched route context.
+ // This eliminates the need for heuristic-based mount path discovery in
+ // computeParentPath, since React Router already knows the matched base path.
+ const parentMatches = parentContext?.matches as { pathnameBase: string }[] | undefined;
+ const parentPathnameBase =
+ parentMatches && parentMatches.length > 0
+ ? parentMatches[parentMatches.length - 1].pathnameBase
+ : undefined;
+
+ // Derive isRootOutlet from RouteContext: empty matches means root.
+ this.isRootOutlet = !parentMatches || parentMatches.length === 0;
+
+ // Seed StackManager's mount path from the parent route context
+ if (parentPathnameBase && !this.outletMountPath) {
+ this.outletMountPath = parentPathnameBase;
+ }
+
+ const components = this.context.getChildrenToRender(
+ this.id,
+ this.ionRouterOutlet,
+ this.props.routeInfo,
+ () => {
+ // Callback triggers re-render when view items are modified during getChildrenToRender
+ this.forceUpdate();
},
- },
- components
- )}
-
+ parentPathnameBase
+ );
+
+ return (
+
+ {React.cloneElement(
+ ionRouterOutlet as any,
+ {
+ ref: (node: HTMLIonRouterOutletElement) => {
+ if (ionRouterOutlet.props.setRef) {
+ // Needed to handle external refs from devs.
+ ionRouterOutlet.props.setRef(node);
+ }
+ if (ionRouterOutlet.props.forwardedRef) {
+ // Needed to handle external refs from devs.
+ ionRouterOutlet.props.forwardedRef.current = node;
+ }
+ this.routerOutletElement = node;
+ const { ref } = ionRouterOutlet as any;
+ // Check for legacy refs.
+ if (typeof ref === 'function') {
+ ref(node);
+ }
+ },
+ },
+ components
+ )}
+
+ );
+ }}
+
);
}
@@ -430,38 +1457,130 @@ export class StackManager extends React.PureComponent {
- const match = matchPath({
- pathname: routeInfo.pathname,
- componentProps: child.props,
+/**
+ * Converts React Route elements to RouteObject format for use with matchRoutes().
+ * Filters out pathless routes (which are handled by fallback logic separately).
+ *
+ * When a basename is provided, absolute route paths are relativized by stripping
+ * the basename prefix. This is necessary because matchRoutes() strips the basename
+ * from the LOCATION pathname but not from route paths β absolute paths must be
+ * made relative to the basename for matching to work correctly.
+ *
+ * @param routeChildren The flat array of Route/IonRoute elements from the outlet.
+ * @param basename The resolved parent path (without trailing slash or `/*`) used to relativize absolute paths.
+ */
+function routeElementsToRouteObjects(routeChildren: React.ReactElement[], basename?: string): RouteObject[] {
+ return routeChildren
+ .filter((child) => child.props.path != null || child.props.index)
+ .map((child): RouteObject => {
+ const handle = { _element: child };
+ let path = child.props.path as string | undefined;
+
+ // Relativize absolute paths by stripping the basename prefix
+ if (path && path.startsWith('/') && basename) {
+ if (path === basename) {
+ path = '';
+ } else if (path.startsWith(basename + '/')) {
+ path = path.slice(basename.length + 1);
+ }
+ }
+
+ if (child.props.index) {
+ return {
+ index: true,
+ handle,
+ caseSensitive: child.props.caseSensitive || undefined,
+ };
+ }
+ return {
+ path,
+ handle,
+ caseSensitive: child.props.caseSensitive || undefined,
+ };
});
- if (match) {
- matchedNode = child;
- }
- });
+}
+
+/**
+ * Finds the ` ` node matching the current route info.
+ * If no ` ` can be matched, a fallback node is returned.
+ * Routes are prioritized by specificity (most specific first).
+ *
+ * @param node The root node to search for ` ` nodes.
+ * @param routeInfo The route information to match against.
+ * @param parentPath The parent path that was matched by the parent outlet (for nested routing)
+ */
+function findRouteByRouteInfo(node: React.ReactNode, routeInfo: RouteInfo, parentPath?: string) {
+ let matchedNode: React.ReactNode;
+ let fallbackNode: React.ReactNode;
+
+ // ` ` nodes are rendered inside of a node
+ const routesChildren = getRoutesChildren(node) ?? node;
+
+ // Collect all route children
+ const routeChildren = React.Children.toArray(routesChildren).filter(
+ (child): child is React.ReactElement =>
+ React.isValidElement(child) && (child.type === Route || child.type === IonRoute)
+ );
- if (matchedNode) {
- return matchedNode;
+ // Delegate route matching to RR6's matchRoutes(), which handles specificity ranking internally.
+ const basename = parentPath ? stripTrailingSlash(parentPath.replace('/*', '')) : undefined;
+ const routeObjects = routeElementsToRouteObjects(routeChildren, basename);
+ const matches = matchRoutes(routeObjects, { pathname: routeInfo.pathname }, basename);
+
+ if (matches && matches.length > 0) {
+ const bestMatch = matches[matches.length - 1];
+ matchedNode = (bestMatch.route as any).handle?._element ?? undefined;
}
- // If we haven't found a node
- // try to find one that doesn't have a path or from prop, that will be our not found route
- React.Children.forEach(node as React.ReactElement, (child: React.ReactElement) => {
- if (!(child.props.path || child.props.from)) {
- matchedNode = child;
+
+ // Fallback: try pathless routes, but only if pathname is within scope.
+ if (!matchedNode) {
+ let pathnameInScope = true;
+
+ if (parentPath) {
+ pathnameInScope = isPathnameInScope(routeInfo.pathname, parentPath);
+ } else {
+ const absolutePathRoutes = routeChildren.filter((r) => r.props.path && r.props.path.startsWith('/'));
+ if (absolutePathRoutes.length > 0) {
+ const absolutePaths = absolutePathRoutes.map((r) => r.props.path as string);
+ const commonPrefix = computeCommonPrefix(absolutePaths);
+ if (commonPrefix && commonPrefix !== '/') {
+ pathnameInScope = routeInfo.pathname.startsWith(commonPrefix);
+ }
+ }
}
- });
- return matchedNode;
+ if (pathnameInScope) {
+ for (const child of routeChildren) {
+ if (!child.props.path) {
+ fallbackNode = child;
+ break;
+ }
+ }
+ }
+ }
+
+ return matchedNode ?? fallbackNode;
}
-function matchComponent(node: React.ReactElement, pathname: string, forceExact?: boolean) {
+function matchComponent(node: React.ReactElement, pathname: string, forceExact?: boolean, parentPath?: string) {
+ const routePath: string | undefined = node?.props?.path;
+
+ let pathnameToMatch: string;
+ if (parentPath && routePath && !routePath.startsWith('/')) {
+ // When parent path is known, compute exact relative pathname
+ const relative = pathname.startsWith(parentPath)
+ ? pathname.slice(parentPath.length).replace(/^\//, '')
+ : pathname;
+ pathnameToMatch = relative;
+ } else {
+ pathnameToMatch = derivePathnameToMatch(pathname, routePath);
+ }
+
return matchPath({
- pathname,
+ pathname: pathnameToMatch,
componentProps: {
...node.props,
- exact: forceExact,
+ end: forceExact,
},
});
}
diff --git a/packages/react-router/src/ReactRouter/utils/computeParentPath.ts b/packages/react-router/src/ReactRouter/utils/computeParentPath.ts
new file mode 100644
index 00000000000..0045922b9dd
--- /dev/null
+++ b/packages/react-router/src/ReactRouter/utils/computeParentPath.ts
@@ -0,0 +1,333 @@
+import type React from 'react';
+
+import { matchPath } from './pathMatching';
+
+/**
+ * Finds the longest common prefix among an array of paths.
+ * Used to determine the scope of an outlet with absolute routes.
+ *
+ * @param paths An array of absolute path strings.
+ * @returns The common prefix shared by all paths.
+ */
+export const computeCommonPrefix = (paths: string[]): string => {
+ if (paths.length === 0) return '';
+ if (paths.length === 1) {
+ // For a single path, extract the directory-like prefix
+ // e.g., /dynamic-routes/home -> /dynamic-routes
+ const segments = paths[0].split('/').filter(Boolean);
+ if (segments.length > 1) {
+ return '/' + segments.slice(0, -1).join('/');
+ }
+ return '/' + segments[0];
+ }
+
+ // Split all paths into segments
+ const segmentArrays = paths.map((p) => p.split('/').filter(Boolean));
+ const minLength = Math.min(...segmentArrays.map((s) => s.length));
+
+ const commonSegments: string[] = [];
+ for (let i = 0; i < minLength; i++) {
+ const segment = segmentArrays[0][i];
+ // Skip segments with route parameters or wildcards
+ if (segment.includes(':') || segment.includes('*')) {
+ break;
+ }
+ const allMatch = segmentArrays.every((s) => s[i] === segment);
+ if (allMatch) {
+ commonSegments.push(segment);
+ } else {
+ break;
+ }
+ }
+
+ return commonSegments.length > 0 ? '/' + commonSegments.join('/') : '';
+};
+
+/**
+ * Checks if a pathname falls within the scope of a mount path using
+ * segment-aware comparison. Prevents false positives like "/tabs-secondary"
+ * matching mount path "/tabs".
+ */
+export const isPathnameInScope = (pathname: string, mountPath: string): boolean => {
+ if (mountPath === '/') return true;
+ return pathname === mountPath || pathname.startsWith(mountPath + '/');
+};
+
+/**
+ * Checks if a route path is a "splat-only" route (just `*` or `/*`).
+ */
+const isSplatOnlyRoute = (routePath: string | undefined): boolean => {
+ return routePath === '*' || routePath === '/*';
+};
+
+/**
+ * Checks if a route has an embedded wildcard (e.g., "tab1/*" but not "*" or "/*").
+ */
+const hasEmbeddedWildcard = (routePath: string | undefined): boolean => {
+ return !!routePath && routePath.includes('*') && !isSplatOnlyRoute(routePath);
+};
+
+/**
+ * Checks if a route with an embedded wildcard matches a pathname.
+ */
+const matchesEmbeddedWildcardRoute = (route: React.ReactElement, pathname: string): boolean => {
+ const routePath = route.props.path as string | undefined;
+ if (!hasEmbeddedWildcard(routePath)) {
+ return false;
+ }
+ return !!matchPath({ pathname, componentProps: route.props });
+};
+
+/**
+ * Checks if a route is a specific match (not wildcard-only or index).
+ */
+export const isSpecificRouteMatch = (route: React.ReactElement, remainingPath: string): boolean => {
+ const routePath = route.props.path;
+ if (route.props.index || isSplatOnlyRoute(routePath)) {
+ return false;
+ }
+ return !!matchPath({ pathname: remainingPath, componentProps: route.props });
+};
+
+/**
+ * Result of parent path computation.
+ */
+export interface ParentPathResult {
+ parentPath: string | undefined;
+ outletMountPath: string | undefined;
+}
+
+interface RouteAnalysis {
+ hasRelativeRoutes: boolean;
+ hasIndexRoute: boolean;
+ hasWildcardRoute: boolean;
+ routeChildren: React.ReactElement[];
+}
+
+/**
+ * Analyzes route children to determine their characteristics.
+ *
+ * @param routeChildren The route children to analyze.
+ * @returns Analysis of the route characteristics.
+ */
+export const analyzeRouteChildren = (routeChildren: React.ReactElement[]): RouteAnalysis => {
+ const hasRelativeRoutes = routeChildren.some((route) => {
+ const path = route.props.path;
+ return path && !path.startsWith('/') && path !== '*';
+ });
+
+ const hasIndexRoute = routeChildren.some((route) => route.props.index);
+
+ const hasWildcardRoute = routeChildren.some((route) => {
+ const routePath = route.props.path;
+ return routePath === '*' || routePath === '/*';
+ });
+
+ return { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute, routeChildren };
+};
+
+interface ComputeParentPathOptions {
+ currentPathname: string;
+ outletMountPath: string | undefined;
+ routeChildren: React.ReactElement[];
+ hasRelativeRoutes: boolean;
+ hasIndexRoute: boolean;
+ hasWildcardRoute: boolean;
+}
+
+/**
+ * Checks if any route matches as a specific (non-wildcard, non-index) route.
+ */
+const findSpecificMatch = (routeChildren: React.ReactElement[], remainingPath: string): boolean => {
+ return routeChildren.some(
+ (route) => isSpecificRouteMatch(route, remainingPath) || matchesEmbeddedWildcardRoute(route, remainingPath)
+ );
+};
+
+/**
+ * Returns the first route that matches as a specific (non-wildcard, non-index) route.
+ */
+const findFirstSpecificMatchingRoute = (
+ routeChildren: React.ReactElement[],
+ remainingPath: string
+): React.ReactElement | undefined => {
+ return routeChildren.find(
+ (route) => isSpecificRouteMatch(route, remainingPath) || matchesEmbeddedWildcardRoute(route, remainingPath)
+ );
+};
+
+/**
+ * Checks if any specific route could plausibly match the remaining path.
+ * Used to determine if we should fall back to a wildcard match.
+ *
+ * Uses exact first-segment matching: the remaining path's first segment
+ * must exactly equal a route's first segment to block the wildcard.
+ * The outlet's mount path is always known from React Router's RouteContext,
+ * so no heuristic-based discovery is needed.
+ */
+const couldSpecificRouteMatch = (
+ routeChildren: React.ReactElement[],
+ remainingPath: string
+): boolean => {
+ const remainingFirstSegment = remainingPath.split('/')[0];
+
+ return routeChildren.some((route) => {
+ const routePath = route.props.path as string | undefined;
+ if (!routePath || routePath === '*' || routePath === '/*') return false;
+ if (route.props.index) return false;
+
+ const routeFirstSegment = routePath.split('/')[0].replace(/[*:]/g, '');
+ if (!routeFirstSegment) return false;
+
+ return routeFirstSegment === remainingFirstSegment;
+ });
+};
+
+/**
+ * Determines the best parent path from the available matches.
+ * Priority: specific > wildcard > index
+ */
+const selectBestMatch = (
+ specificMatch: string | undefined,
+ wildcardMatch: string | undefined,
+ indexMatch: string | undefined
+): string | undefined => {
+ return specificMatch ?? wildcardMatch ?? indexMatch;
+};
+
+/**
+ * Handles outlets with only absolute routes by computing their common prefix.
+ */
+const computeAbsoluteRoutesParentPath = (
+ routeChildren: React.ReactElement[],
+ currentPathname: string,
+ outletMountPath: string | undefined
+): ParentPathResult | undefined => {
+ const absolutePathRoutes = routeChildren.filter((route) => {
+ const path = route.props.path;
+ return path && path.startsWith('/');
+ });
+
+ if (absolutePathRoutes.length === 0) {
+ return undefined;
+ }
+
+ const absolutePaths = absolutePathRoutes.map((r) => r.props.path as string);
+ const commonPrefix = computeCommonPrefix(absolutePaths);
+
+ if (!commonPrefix || commonPrefix === '/') {
+ return undefined;
+ }
+
+ const newOutletMountPath = outletMountPath || commonPrefix;
+
+ if (!currentPathname.startsWith(commonPrefix)) {
+ return { parentPath: undefined, outletMountPath: newOutletMountPath };
+ }
+
+ return { parentPath: commonPrefix, outletMountPath: newOutletMountPath };
+};
+
+/**
+ * Computes the parent path for a nested outlet based on the current pathname
+ * and the outlet's route configuration.
+ *
+ * When the mount path is known (seeded from React Router's RouteContext), the
+ * parent path is simply the mount path β no iterative discovery needed. The
+ * iterative fallback only runs for outlets where RouteContext doesn't provide
+ * a parent match (typically root-level outlets on first render).
+ *
+ * @param options The options for computing the parent path.
+ * @returns The computed parent path result.
+ */
+export const computeParentPath = (options: ComputeParentPathOptions): ParentPathResult => {
+ const { currentPathname, outletMountPath, routeChildren, hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } =
+ options;
+
+ // If pathname is outside the established mount path scope, skip computation.
+ // Use segment-aware comparison: /tabs-secondary must NOT match /tabs scope.
+ if (outletMountPath && !isPathnameInScope(currentPathname, outletMountPath)) {
+ return { parentPath: undefined, outletMountPath };
+ }
+
+ // Fast path: when the mount path is known (from React Router's RouteContext),
+ // the parent path IS the mount path. The iterative segment-by-segment discovery
+ // below was needed when the mount depth had to be guessed from URL structure,
+ // but with RouteContext we already know exactly where this outlet is mounted.
+ if (outletMountPath && (hasRelativeRoutes || hasIndexRoute)) {
+ return { parentPath: outletMountPath, outletMountPath };
+ }
+
+ // Fallback: mount path not yet known. Iterate through path segments to discover
+ // the correct parent depth. This only runs on first render of outlets where
+ // RouteContext doesn't provide a parent match (typically root-level outlets,
+ // which usually have absolute routes and take the absolute routes path below).
+ if (!outletMountPath && (hasRelativeRoutes || hasIndexRoute) && currentPathname.includes('/')) {
+ const segments = currentPathname.split('/').filter(Boolean);
+
+ if (segments.length >= 1) {
+ let firstSpecificMatch: string | undefined;
+ let firstWildcardMatch: string | undefined;
+ let indexMatchAtMount: string | undefined;
+
+ for (let i = 1; i <= segments.length; i++) {
+ const parentPath = '/' + segments.slice(0, i).join('/');
+ const remainingPath = segments.slice(i).join('/');
+
+ // Check for specific route match (highest priority)
+ if (!firstSpecificMatch && findSpecificMatch(routeChildren, remainingPath)) {
+ // Don't let empty/default path routes (path="" or undefined) drive
+ // the parent deeper than a wildcard match. An empty path route matching
+ // when remainingPath is "" just means all segments were consumed.
+ if (firstWildcardMatch) {
+ const matchingRoute = findFirstSpecificMatchingRoute(routeChildren, remainingPath);
+ if (matchingRoute) {
+ const matchingPath = matchingRoute.props.path as string | undefined;
+ if (!matchingPath || matchingPath === '') {
+ continue;
+ }
+ }
+ }
+
+ firstSpecificMatch = parentPath;
+ break;
+ }
+
+ // Check for wildcard match (only if remaining path is non-empty)
+ const hasNonEmptyRemaining = remainingPath !== '' && remainingPath !== '/';
+ if (!firstWildcardMatch && hasNonEmptyRemaining && hasWildcardRoute) {
+ if (!couldSpecificRouteMatch(routeChildren, remainingPath)) {
+ firstWildcardMatch = parentPath;
+ }
+ }
+
+ // Check for index route match
+ if ((remainingPath === '' || remainingPath === '/') && hasIndexRoute) {
+ indexMatchAtMount = parentPath;
+ }
+ }
+
+ // Fallback: check root level for embedded wildcard routes (e.g., "tab1/*")
+ if (!firstSpecificMatch) {
+ const fullRemainingPath = segments.join('/');
+ if (routeChildren.some((route) => matchesEmbeddedWildcardRoute(route, fullRemainingPath))) {
+ firstSpecificMatch = '/';
+ }
+ }
+
+ const bestPath = selectBestMatch(firstSpecificMatch, firstWildcardMatch, indexMatchAtMount);
+
+ return { parentPath: bestPath, outletMountPath: bestPath };
+ }
+ }
+
+ // Handle outlets with only absolute routes
+ if (!hasRelativeRoutes && !hasIndexRoute) {
+ const result = computeAbsoluteRoutesParentPath(routeChildren, currentPathname, outletMountPath);
+ if (result) {
+ return result;
+ }
+ }
+
+ return { parentPath: outletMountPath, outletMountPath };
+};
diff --git a/packages/react-router/src/ReactRouter/utils/matchPath.ts b/packages/react-router/src/ReactRouter/utils/matchPath.ts
deleted file mode 100644
index 891eda08bb2..00000000000
--- a/packages/react-router/src/ReactRouter/utils/matchPath.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { matchPath as reactRouterMatchPath } from 'react-router';
-
-interface MatchPathOptions {
- /**
- * The pathname to match against.
- */
- pathname: string;
- /**
- * The props to match against, they are identical to the matching props `Route` accepts.
- */
- componentProps: {
- path?: string;
- from?: string;
- component?: any;
- exact?: boolean;
- };
-}
-
-/**
- * @see https://v5.reactrouter.com/web/api/matchPath
- */
-export const matchPath = ({
- pathname,
- componentProps,
-}: MatchPathOptions): false | ReturnType => {
- const { exact, component } = componentProps;
-
- const path = componentProps.path || componentProps.from;
- /***
- * The props to match against, they are identical
- * to the matching props `Route` accepts. It could also be a string
- * or an array of strings as shortcut for `{ path }`.
- */
- const matchProps = {
- exact,
- path,
- component,
- };
-
- const match = reactRouterMatchPath(pathname, matchProps);
-
- if (!match) {
- return false;
- }
-
- return match;
-};
diff --git a/packages/react-router/src/ReactRouter/utils/pathMatching.ts b/packages/react-router/src/ReactRouter/utils/pathMatching.ts
new file mode 100644
index 00000000000..7f764ad18d9
--- /dev/null
+++ b/packages/react-router/src/ReactRouter/utils/pathMatching.ts
@@ -0,0 +1,170 @@
+import type { PathMatch } from 'react-router';
+import { matchPath as reactRouterMatchPath } from 'react-router-dom';
+
+/**
+ * Options for the matchPath function.
+ */
+interface MatchPathOptions {
+ /**
+ * The pathname to match against.
+ */
+ pathname: string;
+ /**
+ * The props to match against, they are identical to the matching props `Route` accepts.
+ */
+ componentProps: {
+ path?: string;
+ caseSensitive?: boolean;
+ end?: boolean;
+ index?: boolean;
+ };
+}
+
+/**
+ * The matchPath function is used only for matching paths, not rendering components or elements.
+ * @see https://reactrouter.com/v6/utils/match-path
+ */
+export const matchPath = ({ pathname, componentProps }: MatchPathOptions): PathMatch | null => {
+ const { path, index, ...restProps } = componentProps;
+
+ // Handle index routes - they match when pathname is empty or just "/"
+ if (index && !path) {
+ if (pathname === '' || pathname === '/') {
+ return {
+ params: {},
+ pathname: pathname,
+ pathnameBase: pathname || '/',
+ pattern: {
+ path: '',
+ caseSensitive: false,
+ end: true,
+ },
+ };
+ }
+ return null;
+ }
+
+ // Handle empty path routes - they match when pathname is also empty or just "/"
+ if (path === '' || path === undefined) {
+ if (pathname === '' || pathname === '/') {
+ return {
+ params: {},
+ pathname: pathname,
+ pathnameBase: pathname || '/',
+ pattern: {
+ path: '',
+ caseSensitive: restProps.caseSensitive ?? false,
+ end: restProps.end ?? true,
+ },
+ };
+ }
+ return null;
+ }
+
+ // For relative paths (don't start with '/'), normalize both path and pathname for matching
+ if (!path.startsWith('/')) {
+ const matchOptions: Parameters[0] = {
+ path: `/${path}`,
+ ...restProps,
+ };
+
+ if (matchOptions?.end === undefined) {
+ matchOptions.end = !path.endsWith('*');
+ }
+
+ const normalizedPathname = pathname.startsWith('/') ? pathname : `/${pathname}`;
+ const match = reactRouterMatchPath(matchOptions, normalizedPathname);
+
+ if (match) {
+ // Adjust the match to remove the leading '/' we added
+ return {
+ ...match,
+ pathname: pathname,
+ pathnameBase: match.pathnameBase === '/' ? '/' : match.pathnameBase.slice(1),
+ pattern: {
+ ...match.pattern,
+ path: path,
+ },
+ };
+ }
+
+ return null;
+ }
+
+ // For absolute paths, use React Router's matcher directly.
+ // React Router v6 routes default to `end: true` unless the pattern
+ // explicitly opts into wildcards with `*`. Mirror that behaviour so
+ // matching parity stays aligned with .
+ const matchOptions: Parameters[0] = {
+ path,
+ ...restProps,
+ };
+
+ if (matchOptions?.end === undefined) {
+ matchOptions.end = !path.endsWith('*');
+ }
+
+ return reactRouterMatchPath(matchOptions, pathname);
+};
+
+/**
+ * Determines the portion of a pathname that a given route pattern should match against.
+ * For absolute route patterns we return the full pathname. For relative patterns we
+ * strip off the already-matched parent segments so React Router receives the remainder.
+ */
+export const derivePathnameToMatch = (fullPathname: string, routePath?: string): string => {
+ // For absolute or empty routes, use the full pathname as-is
+ if (!routePath || routePath === '' || routePath.startsWith('/')) {
+ return fullPathname;
+ }
+
+ const trimmedPath = fullPathname.startsWith('/') ? fullPathname.slice(1) : fullPathname;
+ if (!trimmedPath) {
+ // For root-level relative routes (pathname is "/" and routePath is relative),
+ // return the full pathname so matchPath can normalize both.
+ // This allows routes like at root level to work correctly.
+ return fullPathname;
+ }
+
+ const fullSegments = trimmedPath.split('/').filter(Boolean);
+ if (fullSegments.length === 0) {
+ return '';
+ }
+
+ const routeSegments = routePath.split('/').filter(Boolean);
+ if (routeSegments.length === 0) {
+ return trimmedPath;
+ }
+
+ const wildcardIndex = routeSegments.findIndex((segment) => segment === '*' || segment === '**');
+
+ if (wildcardIndex >= 0) {
+ const baseSegments = routeSegments.slice(0, wildcardIndex);
+ if (baseSegments.length === 0) {
+ return trimmedPath;
+ }
+
+ const startIndex = fullSegments.findIndex((_, idx) =>
+ baseSegments.every((seg, segIdx) => {
+ const target = fullSegments[idx + segIdx];
+ if (!target) {
+ return false;
+ }
+ if (seg.startsWith(':')) {
+ return true;
+ }
+ return target === seg;
+ })
+ );
+
+ if (startIndex >= 0) {
+ return fullSegments.slice(startIndex).join('/');
+ }
+ }
+
+ if (routeSegments.length <= fullSegments.length) {
+ return fullSegments.slice(fullSegments.length - routeSegments.length).join('/');
+ }
+
+ return fullSegments[fullSegments.length - 1] ?? trimmedPath;
+};
diff --git a/packages/react-router/src/ReactRouter/utils/pathNormalization.ts b/packages/react-router/src/ReactRouter/utils/pathNormalization.ts
new file mode 100644
index 00000000000..0ce180e4a18
--- /dev/null
+++ b/packages/react-router/src/ReactRouter/utils/pathNormalization.ts
@@ -0,0 +1,37 @@
+/**
+ * Ensures the given path has a leading slash.
+ *
+ * @param value The path string to normalize.
+ * @returns The path with a leading slash.
+ */
+export const ensureLeadingSlash = (value: string): string => {
+ if (value === '') {
+ return '/';
+ }
+ return value.startsWith('/') ? value : `/${value}`;
+};
+
+/**
+ * Strips the trailing slash from a path, unless it's the root path.
+ *
+ * @param value The path string to normalize.
+ * @returns The path without a trailing slash.
+ */
+export const stripTrailingSlash = (value: string): string => {
+ return value.length > 1 && value.endsWith('/') ? value.slice(0, -1) : value;
+};
+
+/**
+ * Normalizes a pathname for comparison by ensuring a leading slash
+ * and removing trailing slashes.
+ *
+ * @param value The pathname to normalize, can be undefined.
+ * @returns A normalized pathname string.
+ */
+export const normalizePathnameForComparison = (value: string | undefined): string => {
+ if (!value || value === '') {
+ return '/';
+ }
+ const withLeadingSlash = ensureLeadingSlash(value);
+ return stripTrailingSlash(withLeadingSlash);
+};
diff --git a/packages/react-router/src/ReactRouter/utils/routeElements.ts b/packages/react-router/src/ReactRouter/utils/routeElements.ts
new file mode 100644
index 00000000000..9d749fe1358
--- /dev/null
+++ b/packages/react-router/src/ReactRouter/utils/routeElements.ts
@@ -0,0 +1,53 @@
+import { IonRoute } from '@ionic/react';
+import React from 'react';
+import { Navigate, Route, Routes } from 'react-router-dom';
+
+/**
+ * Extracts the children from a Routes wrapper component.
+ * The use of ` ` is encouraged with React Router v6.
+ *
+ * @param node The React node to extract Routes children from.
+ * @returns The children of the Routes component, or undefined if not found.
+ */
+export const getRoutesChildren = (node: React.ReactNode): React.ReactNode | undefined => {
+ let routesNode: React.ReactNode;
+ React.Children.forEach(node as React.ReactElement, (child: React.ReactElement) => {
+ if (child.type === Routes) {
+ routesNode = child;
+ }
+ });
+
+ if (routesNode) {
+ // The children of the ` ` component are most likely
+ // (and should be) the ` ` components.
+ return (routesNode as React.ReactElement).props.children;
+ }
+ return undefined;
+};
+
+/**
+ * Extracts Route children from a node (either directly or from a Routes wrapper).
+ *
+ * @param children The children to extract routes from.
+ * @returns An array of Route elements.
+ */
+export const extractRouteChildren = (children: React.ReactNode): React.ReactElement[] => {
+ const routesChildren = getRoutesChildren(children) ?? children;
+ return React.Children.toArray(routesChildren).filter(
+ (child): child is React.ReactElement =>
+ React.isValidElement(child) && (child.type === Route || child.type === IonRoute)
+ );
+};
+
+/**
+ * Checks if a React element is a Navigate component (redirect).
+ *
+ * @param element The element to check.
+ * @returns True if the element is a Navigate component.
+ */
+export const isNavigateElement = (element: unknown): boolean => {
+ return (
+ React.isValidElement(element) &&
+ (element.type === Navigate || (typeof element.type === 'function' && element.type.name === 'Navigate'))
+ );
+};
diff --git a/packages/react-router/src/ReactRouter/utils/viewItemUtils.ts b/packages/react-router/src/ReactRouter/utils/viewItemUtils.ts
new file mode 100644
index 00000000000..4fc4bd07816
--- /dev/null
+++ b/packages/react-router/src/ReactRouter/utils/viewItemUtils.ts
@@ -0,0 +1,59 @@
+import type { ViewItem } from '@ionic/react';
+
+/**
+ * Compares two routes by specificity for sorting (most specific first).
+ *
+ * Sort order:
+ * 1. Index routes come first
+ * 2. Wildcard-only routes (* or /*) come last
+ * 3. Exact matches (no wildcards/params) before wildcard/param routes
+ * 4. Among routes with same status, longer paths are more specific
+ */
+export const compareRouteSpecificity = (
+ a: { path: string; index: boolean },
+ b: { path: string; index: boolean }
+): number => {
+ // Index routes come first
+ if (a.index && !b.index) return -1;
+ if (!a.index && b.index) return 1;
+
+ // Wildcard-only routes (* or /*) should come last
+ const aIsWildcardOnly = a.path === '*' || a.path === '/*';
+ const bIsWildcardOnly = b.path === '*' || b.path === '/*';
+ if (!aIsWildcardOnly && bIsWildcardOnly) return -1;
+ if (aIsWildcardOnly && !bIsWildcardOnly) return 1;
+
+ // Exact matches (no wildcards/params) come before wildcard/param routes
+ const aHasWildcard = a.path.includes('*') || a.path.includes(':');
+ const bHasWildcard = b.path.includes('*') || b.path.includes(':');
+ if (!aHasWildcard && bHasWildcard) return -1;
+ if (aHasWildcard && !bHasWildcard) return 1;
+
+ // Among routes with same wildcard status, longer paths are more specific
+ if (a.path.length !== b.path.length) {
+ return b.path.length - a.path.length;
+ }
+
+ return 0;
+};
+
+/**
+ * Sorts view items by route specificity (most specific first).
+ *
+ * Sort order aligns with findViewItemByPath in ReactRouterViewStack.tsx:
+ * 1. Index routes come first
+ * 2. Wildcard-only routes (* or /*) come last
+ * 3. Exact matches (no wildcards/params) come before wildcard/param routes
+ * 4. Among routes with same wildcard status, longer paths are more specific
+ *
+ * @param views The view items to sort.
+ * @returns A new sorted array of view items.
+ */
+export const sortViewsBySpecificity = (views: ViewItem[]): ViewItem[] => {
+ return [...views].sort((a, b) =>
+ compareRouteSpecificity(
+ { path: a.routeData?.childProps?.path || '', index: !!a.routeData?.childProps?.index },
+ { path: b.routeData?.childProps?.path || '', index: !!b.routeData?.childProps?.index }
+ )
+ );
+};
diff --git a/packages/react-router/test/apps/reactrouter5/package-lock.json b/packages/react-router/test/apps/reactrouter6/package-lock.json
similarity index 99%
rename from packages/react-router/test/apps/reactrouter5/package-lock.json
rename to packages/react-router/test/apps/reactrouter6/package-lock.json
index 8783c314b8f..418033c77b3 100644
--- a/packages/react-router/test/apps/reactrouter5/package-lock.json
+++ b/packages/react-router/test/apps/reactrouter6/package-lock.json
@@ -8,8 +8,8 @@
"name": "react-router-new",
"version": "0.0.1",
"dependencies": {
- "@ionic/react": "^6.6.1",
- "@ionic/react-router": "^6.6.1",
+ "@ionic/react": "^8.6.1",
+ "@ionic/react-router": "^8.6.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
@@ -17,11 +17,11 @@
"@types/react-dom": "^18.0.11",
"@types/react-router": "^5.1.20",
"@types/react-router-dom": "^5.3.3",
- "ionicons": "^8.0.13",
+ "ionicons": "^6.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "react-router": "^5.3.4",
- "react-router-dom": "^5.3.4",
+ "react-router": "^6.0.0",
+ "react-router-dom": "^6.0.0",
"react-scripts": "^5.0.1",
"sass-loader": "8.0.2",
"typescript": "^4.4.2",
@@ -2295,31 +2295,52 @@
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA=="
},
"node_modules/@ionic/core": {
- "version": "6.6.1",
- "resolved": "https://registry.npmjs.org/@ionic/core/-/core-6.6.1.tgz",
- "integrity": "sha512-+LMBk7kUX55rvYQ35AiAXPNzbNm3zNx9ginvuCzByguMjl+N63lpdPzIEfeRURkmq7NByD1VqpodMj5c6Oq2KQ==",
+ "version": "8.6.2",
+ "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.6.2.tgz",
+ "integrity": "sha512-CGZ9CDp/XHtm9WrK3wt0ZtR2f2B76qEvJIaF/juCqmpza9Al6u2L9R/NTEwInDRCWfbkAIF22nHNH54/VvN78Q==",
"dependencies": {
- "@stencil/core": "^2.18.0",
- "ionicons": "^6.1.3",
+ "@stencil/core": "4.33.1",
+ "ionicons": "^7.2.2",
"tslib": "^2.1.0"
}
},
+ "node_modules/@ionic/core/node_modules/@stencil/core": {
+ "version": "4.33.1",
+ "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.33.1.tgz",
+ "integrity": "sha512-12k9xhAJBkpg598it+NRmaYIdEe6TSnsL/v6/KRXDcUyTK11VYwZQej2eHnMWtqot+znJ+GNTqb5YbiXi+5Low==",
+ "bin": {
+ "stencil": "bin/stencil"
+ },
+ "engines": {
+ "node": ">=16.0.0",
+ "npm": ">=7.10.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-darwin-arm64": "4.34.9",
+ "@rollup/rollup-darwin-x64": "4.34.9",
+ "@rollup/rollup-linux-arm64-gnu": "4.34.9",
+ "@rollup/rollup-linux-arm64-musl": "4.34.9",
+ "@rollup/rollup-linux-x64-gnu": "4.34.9",
+ "@rollup/rollup-linux-x64-musl": "4.34.9",
+ "@rollup/rollup-win32-arm64-msvc": "4.34.9",
+ "@rollup/rollup-win32-x64-msvc": "4.34.9"
+ }
+ },
"node_modules/@ionic/core/node_modules/ionicons": {
- "version": "6.1.3",
- "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.1.3.tgz",
- "integrity": "sha512-ptzz38dd/Yq+PgjhXegh7yhb/SLIk1bvL9vQDtLv1aoSc7alO6mX2DIMgcKYzt9vrNWkRu1f9Jr78zIFFyOXqw==",
- "license": "MIT",
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz",
+ "integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==",
"dependencies": {
- "@stencil/core": "^2.18.0"
+ "@stencil/core": "^4.0.3"
}
},
"node_modules/@ionic/react": {
- "version": "6.6.1",
- "resolved": "https://registry.npmjs.org/@ionic/react/-/react-6.6.1.tgz",
- "integrity": "sha512-gq8FzC0CAPt6MpOFethe9+zIU7jg1JyWPWRANJ/UudlF05f2eFOzLgqe/EH0uIIsuDjeoM50hrqfuvg6x2j3UQ==",
+ "version": "8.6.2",
+ "resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.6.2.tgz",
+ "integrity": "sha512-SXE1RnzGqj0MGKGs6D4UCk4rOghbLYI5qwANdZJuBxlIcrcBJuAySjneuTGt+Y3UHS8W3YZHFujRv2Gvb+zvqQ==",
"dependencies": {
- "@ionic/core": "6.6.1",
- "ionicons": "^6.1.3",
+ "@ionic/core": "8.6.2",
+ "ionicons": "^7.0.0",
"tslib": "*"
},
"peerDependencies": {
@@ -2328,11 +2349,11 @@
}
},
"node_modules/@ionic/react-router": {
- "version": "6.6.1",
- "resolved": "https://registry.npmjs.org/@ionic/react-router/-/react-router-6.6.1.tgz",
- "integrity": "sha512-9bHlz3MdzvkUyZ9QfxzcAGDtbRhZ7R5uMjm3UHvGhYS1Rdx4KIc8E5q31IQf7H6j2ULU9YcB7UeyW5ORxBX18Q==",
+ "version": "8.6.2",
+ "resolved": "https://registry.npmjs.org/@ionic/react-router/-/react-router-8.6.2.tgz",
+ "integrity": "sha512-wNVYZHEHkRkNimiK24bJ8KsWjuQyug7C+J/rNER7BKtZDzU3kWKVjvzD3P7kaiOf/DtVo+OrZNvYQJOuoIEhWg==",
"dependencies": {
- "@ionic/react": "6.6.1",
+ "@ionic/react": "8.6.2",
"tslib": "*"
},
"peerDependencies": {
@@ -2342,13 +2363,34 @@
"react-router-dom": "^5.0.1"
}
},
+ "node_modules/@ionic/react/node_modules/@stencil/core": {
+ "version": "4.35.1",
+ "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.35.1.tgz",
+ "integrity": "sha512-u65m3TbzOtpn679gUV4Yvi8YpInhRJ62js30a7YtXief9Ej/vzrhwDE22U0w4DMWJOYwAsJl133BUaZkWwnmzg==",
+ "bin": {
+ "stencil": "bin/stencil"
+ },
+ "engines": {
+ "node": ">=16.0.0",
+ "npm": ">=7.10.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-darwin-arm64": "4.34.9",
+ "@rollup/rollup-darwin-x64": "4.34.9",
+ "@rollup/rollup-linux-arm64-gnu": "4.34.9",
+ "@rollup/rollup-linux-arm64-musl": "4.34.9",
+ "@rollup/rollup-linux-x64-gnu": "4.34.9",
+ "@rollup/rollup-linux-x64-musl": "4.34.9",
+ "@rollup/rollup-win32-arm64-msvc": "4.34.9",
+ "@rollup/rollup-win32-x64-msvc": "4.34.9"
+ }
+ },
"node_modules/@ionic/react/node_modules/ionicons": {
- "version": "6.1.3",
- "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.1.3.tgz",
- "integrity": "sha512-ptzz38dd/Yq+PgjhXegh7yhb/SLIk1bvL9vQDtLv1aoSc7alO6mX2DIMgcKYzt9vrNWkRu1f9Jr78zIFFyOXqw==",
- "license": "MIT",
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz",
+ "integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==",
"dependencies": {
- "@stencil/core": "^2.18.0"
+ "@stencil/core": "^4.0.3"
}
},
"node_modules/@istanbuljs/load-nyc-config": {
@@ -3703,6 +3745,14 @@
"node": ">= 8"
}
},
+ "node_modules/@remix-run/router": {
+ "version": "1.23.0",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
+ "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@@ -3800,7 +3850,6 @@
"cpu": [
"arm64"
],
- "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -3813,7 +3862,6 @@
"cpu": [
"x64"
],
- "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -3826,7 +3874,6 @@
"cpu": [
"arm64"
],
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -3839,7 +3886,6 @@
"cpu": [
"arm64"
],
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -3852,7 +3898,6 @@
"cpu": [
"x64"
],
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -3865,7 +3910,6 @@
"cpu": [
"x64"
],
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -3878,7 +3922,6 @@
"cpu": [
"arm64"
],
- "license": "MIT",
"optional": true,
"os": [
"win32"
@@ -3891,7 +3934,6 @@
"cpu": [
"x64"
],
- "license": "MIT",
"optional": true,
"os": [
"win32"
@@ -10771,27 +10813,6 @@
"he": "bin/he"
}
},
- "node_modules/history": {
- "version": "4.10.1",
- "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
- "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
- "dependencies": {
- "@babel/runtime": "^7.1.2",
- "loose-envify": "^1.2.0",
- "resolve-pathname": "^3.0.0",
- "tiny-invariant": "^1.0.2",
- "tiny-warning": "^1.0.0",
- "value-equal": "^1.0.1"
- }
- },
- "node_modules/hoist-non-react-statics": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
- "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
- "dependencies": {
- "react-is": "^16.7.0"
- }
- },
"node_modules/hoopy": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
@@ -11216,35 +11237,11 @@
}
},
"node_modules/ionicons": {
- "version": "8.0.13",
- "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-8.0.13.tgz",
- "integrity": "sha512-2QQVyG2P4wszne79jemMjWYLp0DBbDhr4/yFroPCxvPP1wtMxgdIV3l5n+XZ5E9mgoXU79w7yTWpm2XzJsISxQ==",
- "license": "MIT",
+ "version": "6.1.3",
+ "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.1.3.tgz",
+ "integrity": "sha512-ptzz38dd/Yq+PgjhXegh7yhb/SLIk1bvL9vQDtLv1aoSc7alO6mX2DIMgcKYzt9vrNWkRu1f9Jr78zIFFyOXqw==",
"dependencies": {
- "@stencil/core": "^4.35.3"
- }
- },
- "node_modules/ionicons/node_modules/@stencil/core": {
- "version": "4.35.3",
- "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.35.3.tgz",
- "integrity": "sha512-RH5/I+amV31QI8TMXhXkAkjzs2eod6Y07jkUYTl9kMB+X7c5wUpv95Y/2LtcAx0Rqdhh4SHbJiwpr0ApBZmv0g==",
- "license": "MIT",
- "bin": {
- "stencil": "bin/stencil"
- },
- "engines": {
- "node": ">=16.0.0",
- "npm": ">=7.10.0"
- },
- "optionalDependencies": {
- "@rollup/rollup-darwin-arm64": "4.34.9",
- "@rollup/rollup-darwin-x64": "4.34.9",
- "@rollup/rollup-linux-arm64-gnu": "4.34.9",
- "@rollup/rollup-linux-arm64-musl": "4.34.9",
- "@rollup/rollup-linux-x64-gnu": "4.34.9",
- "@rollup/rollup-linux-x64-musl": "4.34.9",
- "@rollup/rollup-win32-arm64-msvc": "4.34.9",
- "@rollup/rollup-win32-x64-msvc": "4.34.9"
+ "@stencil/core": "^2.18.0"
}
},
"node_modules/ipaddr.js": {
@@ -11695,11 +11692,6 @@
"node": ">=8"
}
},
- "node_modules/isarray": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
- "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
- },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -16428,14 +16420,6 @@
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
- "node_modules/path-to-regexp": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
- "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
- "dependencies": {
- "isarray": "0.0.1"
- }
- },
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
@@ -18388,39 +18372,33 @@
}
},
"node_modules/react-router": {
- "version": "5.3.4",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
- "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
+ "version": "6.30.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
+ "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
"dependencies": {
- "@babel/runtime": "^7.12.13",
- "history": "^4.9.0",
- "hoist-non-react-statics": "^3.1.0",
- "loose-envify": "^1.3.1",
- "path-to-regexp": "^1.7.0",
- "prop-types": "^15.6.2",
- "react-is": "^16.6.0",
- "tiny-invariant": "^1.0.2",
- "tiny-warning": "^1.0.0"
+ "@remix-run/router": "1.23.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
},
"peerDependencies": {
- "react": ">=15"
+ "react": ">=16.8"
}
},
"node_modules/react-router-dom": {
- "version": "5.3.4",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz",
- "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==",
+ "version": "6.30.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
+ "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
"dependencies": {
- "@babel/runtime": "^7.12.13",
- "history": "^4.9.0",
- "loose-envify": "^1.3.1",
- "prop-types": "^15.6.2",
- "react-router": "5.3.4",
- "tiny-invariant": "^1.0.2",
- "tiny-warning": "^1.0.0"
+ "@remix-run/router": "1.23.0",
+ "react-router": "6.30.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
},
"peerDependencies": {
- "react": ">=15"
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
}
},
"node_modules/react-scripts": {
@@ -19372,11 +19350,6 @@
"node": ">=8"
}
},
- "node_modules/resolve-pathname": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
- "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
- },
"node_modules/resolve.exports": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz",
@@ -20817,16 +20790,6 @@
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="
},
- "node_modules/tiny-invariant": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz",
- "integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg=="
- },
- "node_modules/tiny-warning": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
- "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
- },
"node_modules/tmp": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
@@ -21264,11 +21227,6 @@
"node": ">= 8"
}
},
- "node_modules/value-equal": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
- "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
- },
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -24028,51 +23986,81 @@
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA=="
},
"@ionic/core": {
- "version": "6.6.1",
- "resolved": "https://registry.npmjs.org/@ionic/core/-/core-6.6.1.tgz",
- "integrity": "sha512-+LMBk7kUX55rvYQ35AiAXPNzbNm3zNx9ginvuCzByguMjl+N63lpdPzIEfeRURkmq7NByD1VqpodMj5c6Oq2KQ==",
+ "version": "8.6.2",
+ "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.6.2.tgz",
+ "integrity": "sha512-CGZ9CDp/XHtm9WrK3wt0ZtR2f2B76qEvJIaF/juCqmpza9Al6u2L9R/NTEwInDRCWfbkAIF22nHNH54/VvN78Q==",
"requires": {
- "@stencil/core": "^2.18.0",
- "ionicons": "^6.1.3",
+ "@stencil/core": "4.33.1",
+ "ionicons": "^7.2.2",
"tslib": "^2.1.0"
},
"dependencies": {
+ "@stencil/core": {
+ "version": "4.33.1",
+ "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.33.1.tgz",
+ "integrity": "sha512-12k9xhAJBkpg598it+NRmaYIdEe6TSnsL/v6/KRXDcUyTK11VYwZQej2eHnMWtqot+znJ+GNTqb5YbiXi+5Low==",
+ "requires": {
+ "@rollup/rollup-darwin-arm64": "4.34.9",
+ "@rollup/rollup-darwin-x64": "4.34.9",
+ "@rollup/rollup-linux-arm64-gnu": "4.34.9",
+ "@rollup/rollup-linux-arm64-musl": "4.34.9",
+ "@rollup/rollup-linux-x64-gnu": "4.34.9",
+ "@rollup/rollup-linux-x64-musl": "4.34.9",
+ "@rollup/rollup-win32-arm64-msvc": "4.34.9",
+ "@rollup/rollup-win32-x64-msvc": "4.34.9"
+ }
+ },
"ionicons": {
- "version": "6.1.3",
- "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.1.3.tgz",
- "integrity": "sha512-ptzz38dd/Yq+PgjhXegh7yhb/SLIk1bvL9vQDtLv1aoSc7alO6mX2DIMgcKYzt9vrNWkRu1f9Jr78zIFFyOXqw==",
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz",
+ "integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==",
"requires": {
- "@stencil/core": "^2.18.0"
+ "@stencil/core": "^4.0.3"
}
}
}
},
"@ionic/react": {
- "version": "6.6.1",
- "resolved": "https://registry.npmjs.org/@ionic/react/-/react-6.6.1.tgz",
- "integrity": "sha512-gq8FzC0CAPt6MpOFethe9+zIU7jg1JyWPWRANJ/UudlF05f2eFOzLgqe/EH0uIIsuDjeoM50hrqfuvg6x2j3UQ==",
+ "version": "8.6.2",
+ "resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.6.2.tgz",
+ "integrity": "sha512-SXE1RnzGqj0MGKGs6D4UCk4rOghbLYI5qwANdZJuBxlIcrcBJuAySjneuTGt+Y3UHS8W3YZHFujRv2Gvb+zvqQ==",
"requires": {
- "@ionic/core": "6.6.1",
- "ionicons": "^6.1.3",
+ "@ionic/core": "8.6.2",
+ "ionicons": "^7.0.0",
"tslib": "*"
},
"dependencies": {
+ "@stencil/core": {
+ "version": "4.35.1",
+ "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.35.1.tgz",
+ "integrity": "sha512-u65m3TbzOtpn679gUV4Yvi8YpInhRJ62js30a7YtXief9Ej/vzrhwDE22U0w4DMWJOYwAsJl133BUaZkWwnmzg==",
+ "requires": {
+ "@rollup/rollup-darwin-arm64": "4.34.9",
+ "@rollup/rollup-darwin-x64": "4.34.9",
+ "@rollup/rollup-linux-arm64-gnu": "4.34.9",
+ "@rollup/rollup-linux-arm64-musl": "4.34.9",
+ "@rollup/rollup-linux-x64-gnu": "4.34.9",
+ "@rollup/rollup-linux-x64-musl": "4.34.9",
+ "@rollup/rollup-win32-arm64-msvc": "4.34.9",
+ "@rollup/rollup-win32-x64-msvc": "4.34.9"
+ }
+ },
"ionicons": {
- "version": "6.1.3",
- "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.1.3.tgz",
- "integrity": "sha512-ptzz38dd/Yq+PgjhXegh7yhb/SLIk1bvL9vQDtLv1aoSc7alO6mX2DIMgcKYzt9vrNWkRu1f9Jr78zIFFyOXqw==",
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz",
+ "integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==",
"requires": {
- "@stencil/core": "^2.18.0"
+ "@stencil/core": "^4.0.3"
}
}
}
},
"@ionic/react-router": {
- "version": "6.6.1",
- "resolved": "https://registry.npmjs.org/@ionic/react-router/-/react-router-6.6.1.tgz",
- "integrity": "sha512-9bHlz3MdzvkUyZ9QfxzcAGDtbRhZ7R5uMjm3UHvGhYS1Rdx4KIc8E5q31IQf7H6j2ULU9YcB7UeyW5ORxBX18Q==",
+ "version": "8.6.2",
+ "resolved": "https://registry.npmjs.org/@ionic/react-router/-/react-router-8.6.2.tgz",
+ "integrity": "sha512-wNVYZHEHkRkNimiK24bJ8KsWjuQyug7C+J/rNER7BKtZDzU3kWKVjvzD3P7kaiOf/DtVo+OrZNvYQJOuoIEhWg==",
"requires": {
- "@ionic/react": "6.6.1",
+ "@ionic/react": "8.6.2",
"tslib": "*"
}
},
@@ -25068,6 +25056,11 @@
}
}
},
+ "@remix-run/router": {
+ "version": "1.23.0",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
+ "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA=="
+ },
"@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@@ -30255,27 +30248,6 @@
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
},
- "history": {
- "version": "4.10.1",
- "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
- "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
- "requires": {
- "@babel/runtime": "^7.1.2",
- "loose-envify": "^1.2.0",
- "resolve-pathname": "^3.0.0",
- "tiny-invariant": "^1.0.2",
- "tiny-warning": "^1.0.0",
- "value-equal": "^1.0.1"
- }
- },
- "hoist-non-react-statics": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
- "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
- "requires": {
- "react-is": "^16.7.0"
- }
- },
"hoopy": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
@@ -30582,28 +30554,11 @@
"dev": true
},
"ionicons": {
- "version": "8.0.13",
- "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-8.0.13.tgz",
- "integrity": "sha512-2QQVyG2P4wszne79jemMjWYLp0DBbDhr4/yFroPCxvPP1wtMxgdIV3l5n+XZ5E9mgoXU79w7yTWpm2XzJsISxQ==",
+ "version": "6.1.3",
+ "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.1.3.tgz",
+ "integrity": "sha512-ptzz38dd/Yq+PgjhXegh7yhb/SLIk1bvL9vQDtLv1aoSc7alO6mX2DIMgcKYzt9vrNWkRu1f9Jr78zIFFyOXqw==",
"requires": {
- "@stencil/core": "^4.35.3"
- },
- "dependencies": {
- "@stencil/core": {
- "version": "4.35.3",
- "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.35.3.tgz",
- "integrity": "sha512-RH5/I+amV31QI8TMXhXkAkjzs2eod6Y07jkUYTl9kMB+X7c5wUpv95Y/2LtcAx0Rqdhh4SHbJiwpr0ApBZmv0g==",
- "requires": {
- "@rollup/rollup-darwin-arm64": "4.34.9",
- "@rollup/rollup-darwin-x64": "4.34.9",
- "@rollup/rollup-linux-arm64-gnu": "4.34.9",
- "@rollup/rollup-linux-arm64-musl": "4.34.9",
- "@rollup/rollup-linux-x64-gnu": "4.34.9",
- "@rollup/rollup-linux-x64-musl": "4.34.9",
- "@rollup/rollup-win32-arm64-msvc": "4.34.9",
- "@rollup/rollup-win32-x64-msvc": "4.34.9"
- }
- }
+ "@stencil/core": "^2.18.0"
}
},
"ipaddr.js": {
@@ -30892,11 +30847,6 @@
"is-docker": "^2.0.0"
}
},
- "isarray": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
- "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
- },
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -34400,14 +34350,6 @@
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
- "path-to-regexp": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
- "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
- "requires": {
- "isarray": "0.0.1"
- }
- },
"pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
@@ -35656,33 +35598,20 @@
"integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A=="
},
"react-router": {
- "version": "5.3.4",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
- "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
+ "version": "6.30.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
+ "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
"requires": {
- "@babel/runtime": "^7.12.13",
- "history": "^4.9.0",
- "hoist-non-react-statics": "^3.1.0",
- "loose-envify": "^1.3.1",
- "path-to-regexp": "^1.7.0",
- "prop-types": "^15.6.2",
- "react-is": "^16.6.0",
- "tiny-invariant": "^1.0.2",
- "tiny-warning": "^1.0.0"
+ "@remix-run/router": "1.23.0"
}
},
"react-router-dom": {
- "version": "5.3.4",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz",
- "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==",
+ "version": "6.30.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
+ "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
"requires": {
- "@babel/runtime": "^7.12.13",
- "history": "^4.9.0",
- "loose-envify": "^1.3.1",
- "prop-types": "^15.6.2",
- "react-router": "5.3.4",
- "tiny-invariant": "^1.0.2",
- "tiny-warning": "^1.0.0"
+ "@remix-run/router": "1.23.0",
+ "react-router": "6.30.1"
}
},
"react-scripts": {
@@ -36303,11 +36232,6 @@
}
}
},
- "resolve-pathname": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
- "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
- },
"resolve.exports": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz",
@@ -37413,16 +37337,6 @@
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="
},
- "tiny-invariant": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz",
- "integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg=="
- },
- "tiny-warning": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
- "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
- },
"tmp": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
@@ -37759,11 +37673,6 @@
}
}
},
- "value-equal": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
- "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
- },
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
diff --git a/packages/react-router/test/apps/reactrouter5/package.json b/packages/react-router/test/apps/reactrouter6/package.json
similarity index 55%
rename from packages/react-router/test/apps/reactrouter5/package.json
rename to packages/react-router/test/apps/reactrouter6/package.json
index 4e59232f653..e6bb4327f92 100644
--- a/packages/react-router/test/apps/reactrouter5/package.json
+++ b/packages/react-router/test/apps/reactrouter6/package.json
@@ -2,33 +2,38 @@
"name": "react-router-new",
"version": "0.0.1",
"private": true,
+ "overrides": {
+ "@ionic/react-router": {
+ "react-router": "$react-router",
+ "react-router-dom": "$react-router-dom"
+ }
+ },
"dependencies": {
- "@ionic/react": "^6.6.1",
- "@ionic/react-router": "^6.6.1",
+ "@ionic/react": "^8.6.1",
+ "@ionic/react-router": "^8.6.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
- "@types/react-router": "^5.1.20",
- "@types/react-router-dom": "^5.3.3",
- "ionicons": "^8.0.13",
+ "ionicons": "^6.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "react-router": "^5.3.4",
- "react-router-dom": "^5.3.4",
- "react-scripts": "^5.0.1",
- "sass-loader": "8.0.2",
- "typescript": "^4.4.2",
- "wait-on": "^5.3.0"
+ "react-router": "^6.0.0",
+ "react-router-dom": "^6.0.0",
+ "typescript": "^4.4.2"
},
"scripts": {
- "start": "react-scripts start",
- "build": "react-scripts build",
+ "dev": "vite",
+ "start": "vite",
+ "build": "vite build",
"test": "cypress open",
"cypress": "node_modules/.bin/cypress run --headless --browser chrome",
"cypress.open": "cypress open",
- "e2e": "concurrently \"serve -s build -l 3000\" \"wait-on http-get://localhost:3000 && npm run cypress\" --kill-others --success first",
+ "e2e": "concurrently \"serve -s dist -l 3000\" \"wait-on http-get://localhost:3000 && npm run cypress\" --kill-others --success first",
+ "playwright": "npx playwright test",
+ "playwright.open": "npx playwright test --ui",
+ "playwright.headed": "npx playwright test --headed",
"sync": "sh ./scripts/sync.sh"
},
"eslintConfig": {
@@ -50,11 +55,14 @@
]
},
"devDependencies": {
+ "@playwright/test": "^1.40.0",
+ "@vitejs/plugin-react": "^4.0.0",
"concurrently": "^6.3.0",
"cypress": "^13.2.0",
+ "cypress-terminal-report": "^5.3.0",
"serve": "^14.0.1",
- "wait-on": "^6.0.0",
- "webpack-cli": "^4.9.1"
+ "vite": "^5.0.0",
+ "wait-on": "^6.0.0"
},
"description": "An Ionic project",
"engines": {
diff --git a/packages/react-router/test/apps/reactrouter6/vite.config.ts b/packages/react-router/test/apps/reactrouter6/vite.config.ts
new file mode 100644
index 00000000000..968658ad4c4
--- /dev/null
+++ b/packages/react-router/test/apps/reactrouter6/vite.config.ts
@@ -0,0 +1,9 @@
+import react from '@vitejs/plugin-react';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 3000,
+ },
+});
diff --git a/packages/react-router/test/base/cypress.config.ts b/packages/react-router/test/base/cypress.config.ts
index b2ec230fb07..69674476860 100644
--- a/packages/react-router/test/base/cypress.config.ts
+++ b/packages/react-router/test/base/cypress.config.ts
@@ -5,6 +5,10 @@ export default defineConfig({
pageLoadTimeout: 6000000000,
screenshotOnRunFailure: false,
defaultCommandTimeout: 10000,
+ retries: {
+ runMode: 2,
+ openMode: 0,
+ },
fixturesFolder: 'tests/e2e/fixtures',
screenshotsFolder: 'tests/e2e/screenshots',
videosFolder: 'tests/e2e/videos',
diff --git a/packages/react-router/test/base/public/index.html b/packages/react-router/test/base/index.html
similarity index 80%
rename from packages/react-router/test/base/public/index.html
rename to packages/react-router/test/base/index.html
index 80d0b3ae6ab..cc1db995178 100644
--- a/packages/react-router/test/base/public/index.html
+++ b/packages/react-router/test/base/index.html
@@ -14,9 +14,9 @@
-
+
-
+
@@ -26,5 +26,6 @@
+
diff --git a/packages/react-router/test/base/playwright.config.ts b/packages/react-router/test/base/playwright.config.ts
new file mode 100644
index 00000000000..5c57c4e90bd
--- /dev/null
+++ b/packages/react-router/test/base/playwright.config.ts
@@ -0,0 +1,32 @@
+import { defineConfig, devices } from '@playwright/test';
+
+export default defineConfig({
+ testDir: './tests/e2e/playwright',
+ fullyParallel: false,
+ forbidOnly: !!process.env['CI'],
+ retries: process.env['CI'] ? 2 : 0,
+ workers: 1,
+ reporter: 'html',
+ use: {
+ baseURL: 'http://localhost:3000',
+ trace: 'on-first-retry',
+ actionTimeout: 5000,
+ },
+ projects: [
+ {
+ name: 'Mobile Chrome',
+ use: {
+ browserName: 'chromium',
+ ...devices['Pixel 5'],
+ },
+ },
+ ],
+ webServer: {
+ command: 'npm run dev',
+ url: 'http://localhost:3000',
+ // Always reuse an existing server. In the test_runner.sh pipeline, the
+ // server is started before Cypress and stays running for Playwright.
+ // Locally, developers can start the server manually with `npm run dev`.
+ reuseExistingServer: true,
+ },
+});
diff --git a/packages/react-router/test/base/scripts/sync.sh b/packages/react-router/test/base/scripts/sync.sh
index 1be037080bd..f44c34a44af 100644
--- a/packages/react-router/test/base/scripts/sync.sh
+++ b/packages/react-router/test/base/scripts/sync.sh
@@ -15,4 +15,7 @@ npm pack ../../../../react
npm pack ../../../
# Install Dependencies
-npm install *.tgz --no-save
+# TODO: Remove --legacy-peer-deps once @ionic/react peer deps align with test app versions.
+# Currently needed because packed tarballs may have peer dep ranges that conflict with
+# the specific React/React-Router versions in test apps.
+npm install *.tgz --no-save --legacy-peer-deps
diff --git a/packages/react-router/test/base/src/App.test.tsx b/packages/react-router/test/base/src/App.test.tsx
index 8c927a8d7ac..b770c1c4d5c 100644
--- a/packages/react-router/test/base/src/App.test.tsx
+++ b/packages/react-router/test/base/src/App.test.tsx
@@ -1,5 +1,6 @@
-import React from 'react';
import { render } from '@testing-library/react';
+import React from 'react';
+
import App from './App';
test('renders without crashing', () => {
diff --git a/packages/react-router/test/base/src/App.tsx b/packages/react-router/test/base/src/App.tsx
index cdfa67d1360..ad5538f0563 100644
--- a/packages/react-router/test/base/src/App.tsx
+++ b/packages/react-router/test/base/src/App.tsx
@@ -1,6 +1,6 @@
import { IonApp, setupIonicReact, IonRouterOutlet } from '@ionic/react';
import React from 'react';
-import { Route } from 'react-router-dom';
+import { Route, Navigate } from 'react-router-dom';
/* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css';
@@ -21,23 +21,49 @@ import '@ionic/react/css/text-transformation.css';
/* Theme variables */
import './theme/variables.css';
import Main from './pages/Main';
+
import { IonReactRouter } from '@ionic/react-router';
+
+import DynamicIonpageClassnames from './pages/dynamic-ionpage-classnames/DynamicIonpageClassnames';
import DynamicRoutes from './pages/dynamic-routes/DynamicRoutes';
-import Routing from './pages/routing/Routing';
-import MultipleTabs from './pages/muiltiple-tabs/MultipleTabs';
import DynamicTabs from './pages/dynamic-tabs/DynamicTabs';
+import MultipleTabs from './pages/muiltiple-tabs/MultipleTabs';
import NestedOutlet from './pages/nested-outlet/NestedOutlet';
import NestedOutlet2 from './pages/nested-outlet/NestedOutlet2';
-import ReplaceAction from './pages/replace-action/Replace';
-import TabsContext from './pages/tab-context/TabContext';
+import NestedParams from './pages/nested-params/NestedParams';
+import RelativePaths from './pages/relative-paths/RelativePaths';
import { OutletRef } from './pages/outlet-ref/OutletRef';
-import { SwipeToGoBack } from './pages/swipe-to-go-back/SwipToGoBack';
+import Params from './pages/params/Params';
import Refs from './pages/refs/Refs';
-import DynamicIonpageClassnames from './pages/dynamic-ionpage-classnames/DynamicIonpageClassnames';
+import { Page1, Page2, Page3 } from './pages/replace-action/Replace';
+import Routing from './pages/routing/Routing';
+import { SwipeToGoBack } from './pages/swipe-to-go-back/SwipeToGoBack';
+import TabsContext from './pages/tab-context/TabContext';
import Tabs from './pages/tabs/Tabs';
import TabsSecondary from './pages/tabs/TabsSecondary';
-import Params from './pages/params/Params';
+import TabHistoryIsolation from './pages/tab-history-isolation/TabHistoryIsolation';
import Overlays from './pages/overlays/Overlays';
+import NestedTabsRelativeLinks from './pages/nested-tabs-relative-links/NestedTabsRelativeLinks';
+import RootSplatTabs from './pages/root-splat-tabs/RootSplatTabs';
+import ContentChangeNavigation from './pages/content-change-navigation/ContentChangeNavigation';
+import SearchParams from './pages/search-params/SearchParams';
+import IonRoutePropsTest from './pages/ion-route-props/IonRouteProps';
+import PrefixMatchWildcard from './pages/prefix-match-wildcard/PrefixMatchWildcard';
+import StaleViewCleanup from './pages/stale-view-cleanup/StaleViewCleanup';
+import IndexParamPriority from './pages/index-param-priority/IndexParamPriority';
+import IndexRouteReuse from './pages/index-route-reuse/IndexRouteReuse';
+import TailSliceAmbiguity from './pages/tail-slice-ambiguity/TailSliceAmbiguity';
+import WildcardNoHeuristic from './pages/wildcard-no-heuristic/WildcardNoHeuristic';
+import RouteContextShape from './pages/route-context-shape/RouteContextShape';
+import ModalAriaHidden from './pages/modal-aria-hidden/ModalAriaHidden';
+import RedirectParams from './pages/redirect-params/RedirectParams';
+import MultiStepBack from './pages/multi-step-back/MultiStepBack';
+import DirectionNoneBack from './pages/direction-none-back/DirectionNoneBack';
+import TabSearchParams from './pages/tab-search-params/TabSearchParams';
+import { Step1, Step2, Step3, Step4 } from './pages/replace-params/ReplaceParams';
+import { ParamSwipeBack, ParamSwipeBackB } from './pages/param-swipe-back/ParamSwipeBack';
+import TabLifecycle from './pages/tab-lifecycle/TabLifecycle';
+import TabLifecycleOutside from './pages/tab-lifecycle/TabLifecycleOutside';
setupIonicReact();
@@ -46,23 +72,56 @@ const App: React.FC = () => {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ {/* Test root-level relative path - no leading slash */}
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
diff --git a/packages/react-router/test/base/src/index.tsx b/packages/react-router/test/base/src/index.tsx
index 5ed41355a40..de6c73b77f4 100644
--- a/packages/react-router/test/base/src/index.tsx
+++ b/packages/react-router/test/base/src/index.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
+
import App from './App';
const container = document.getElementById('root');
diff --git a/packages/react-router/test/base/src/pages/Main.tsx b/packages/react-router/test/base/src/pages/Main.tsx
index cd4c5f6980c..65664681f0c 100644
--- a/packages/react-router/test/base/src/pages/Main.tsx
+++ b/packages/react-router/test/base/src/pages/Main.tsx
@@ -1,4 +1,3 @@
-import React from 'react';
import {
IonContent,
IonHeader,
@@ -9,10 +8,9 @@ import {
IonItem,
IonLabel,
} from '@ionic/react';
+import React from 'react';
-interface MainProps {}
-
-const Main: React.FC = () => {
+const Main: React.FC = () => {
return (
@@ -64,9 +62,78 @@ const Main: React.FC = () => {
Tabs
+
+ Tab History Isolation
+
Params
+
+ Nested Params
+
+
+ Relative Paths
+
+
+ Nested Tabs Relative Links
+
+
+ Root Splat Tabs
+
+
+ Content Change Navigation
+
+
+ Search Params
+
+
+ IonRoute Props
+
+
+ Prefix Match Wildcard
+
+
+ Stale View Cleanup
+
+
+ Index Param Priority
+
+
+ Index Route Reuse
+
+
+ Tail Slice Ambiguity
+
+
+ Wildcard No Heuristic
+
+
+ Route Context Shape
+
+
+ Modal Aria Hidden
+
+
+ Redirect Params
+
+
+ Multi-Step Back
+
+
+ Direction None Back
+
+
+ Tab Search Params
+
+
+ Replace Params
+
+
+ Param Swipe Back
+
+
+ Tab Lifecycle
+
diff --git a/packages/react-router/test/base/src/pages/content-change-navigation/ContentChangeNavigation.tsx b/packages/react-router/test/base/src/pages/content-change-navigation/ContentChangeNavigation.tsx
new file mode 100644
index 00000000000..16075d9ab4e
--- /dev/null
+++ b/packages/react-router/test/base/src/pages/content-change-navigation/ContentChangeNavigation.tsx
@@ -0,0 +1,109 @@
+/**
+ * Reproduces the bug where changing view content while navigating causes
+ * an invalid view to be rendered.
+ */
+
+import {
+ IonContent,
+ IonHeader,
+ IonPage,
+ IonTitle,
+ IonToolbar,
+ IonList,
+ IonItem,
+ IonLabel,
+ IonButton,
+ IonRouterOutlet,
+ IonButtons,
+ IonBackButton,
+} from '@ionic/react';
+import React, { useState } from 'react';
+import { Route, Navigate, useNavigate } from 'react-router-dom';
+
+const ListPage: React.FC = () => {
+ const [items, setItems] = useState(['Item 1', 'Item 2', 'Item 3']);
+ const navigate = useNavigate();
+
+ const clearItemsAndNavigate = () => {
+ setItems([]);
+ navigate('/content-change-navigation/home');
+ };
+
+ // Using different keys forces React to unmount/remount IonPage
+ if (items.length === 0) {
+ return (
+
+
+
+
+
+
+ Empty List
+
+
+
+ There are no items
+
+ Go Home
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ Item List
+
+
+
+
+ {items.map((item, index) => (
+
+ {item}
+
+ ))}
+
+
+
+ Remove all items and navigate to home
+
+
+
+ );
+};
+
+const HomePage: React.FC = () => {
+ return (
+
+
+
+ Home
+
+
+
+ Home Page Content
+
+ Go to list page
+
+
+
+ );
+};
+
+const ContentChangeNavigation: React.FC = () => {
+ return (
+
+ } />
+ } />
+ } />
+
+ );
+};
+
+export default ContentChangeNavigation;
diff --git a/packages/react-router/test/base/src/pages/direction-none-back/DirectionNoneBack.tsx b/packages/react-router/test/base/src/pages/direction-none-back/DirectionNoneBack.tsx
new file mode 100644
index 00000000000..dedf321158d
--- /dev/null
+++ b/packages/react-router/test/base/src/pages/direction-none-back/DirectionNoneBack.tsx
@@ -0,0 +1,84 @@
+import {
+ IonContent,
+ IonHeader,
+ IonPage,
+ IonTitle,
+ IonToolbar,
+ IonButton,
+ IonRouterOutlet,
+ IonBackButton,
+ IonButtons,
+} from '@ionic/react';
+import React from 'react';
+import { Route, Navigate } from 'react-router-dom';
+
+/**
+ * Tests that IonBackButton works correctly after navigating with
+ * routerDirection="none". The back button should use history to
+ * determine the previous page, not fall back to defaultHref.
+ */
+const PageA: React.FC = () => {
+ return (
+
+
+
+ Page A
+
+
+
+
+ Go to B (forward)
+
+
+ Go to B (none)
+
+
+
+ );
+};
+
+const PageB: React.FC = () => {
+ return (
+
+
+
+
+
+
+ Page B
+
+
+
+ Page B content
+
+
+ );
+};
+
+const Fallback: React.FC = () => {
+ return (
+
+
+
+ Fallback (defaultHref)
+
+
+
+ This page should NOT be reached via back button if history exists
+
+
+ );
+};
+
+const DirectionNoneBack: React.FC = () => {
+ return (
+
+ } />
+ } />
+ } />
+ } />
+
+ );
+};
+
+export default DirectionNoneBack;
diff --git a/packages/react-router/test/base/src/pages/dynamic-ionpage-classnames/DynamicIonpageClassnames.tsx b/packages/react-router/test/base/src/pages/dynamic-ionpage-classnames/DynamicIonpageClassnames.tsx
index baba236a0c6..b49ef3da444 100644
--- a/packages/react-router/test/base/src/pages/dynamic-ionpage-classnames/DynamicIonpageClassnames.tsx
+++ b/packages/react-router/test/base/src/pages/dynamic-ionpage-classnames/DynamicIonpageClassnames.tsx
@@ -1,59 +1,57 @@
+import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
import React, { useEffect, useRef, useState } from 'react';
-import {
- IonButton,
- IonContent,
- IonHeader,
- IonPage,
- IonRouterOutlet,
- IonTitle,
- IonToolbar,
-} from '@ionic/react';
-import { Route } from 'react-router';
-
-interface DynamicIonpageClassnamesProps {}
-
-const DynamicIonpageClassnames: React.FC = () => {
- return (
-
-
-
- );
+
+/**
+ * Test page for verifying that dynamically changing className on IonPage
+ * preserves framework-added classes (can-go-back, ion-page-invisible, etc.).
+ */
+const DynamicIonpageClassnames: React.FC = () => {
+ return ;
};
export default DynamicIonpageClassnames;
-const Page: React.FC = (props) => {
+const Page: React.FC = () => {
const [styleClass, setStyleClass] = useState('initial-class');
const [divClasses, setDivClasses] = useState();
- const ref = useRef();
+ const ref = useRef(null);
+
useEffect(() => {
- if(ref.current) {
- var observer = new MutationObserver(function (event) {
- setDivClasses(ref.current?.className)
- })
-
- observer.observe(ref.current, {
- attributes: true,
- attributeFilter: ['class'],
- childList: false,
- characterData: false
- })
- }
- return () => observer.disconnect()
- }, [])
+ if (!ref.current) return;
+
+ const observer = new MutationObserver(() => {
+ setDivClasses(ref.current?.className);
+ });
+
+ observer.observe(ref.current, {
+ attributes: true,
+ attributeFilter: ['class'],
+ childList: false,
+ characterData: false,
+ });
+ return () => observer.disconnect();
+ }, []);
return (
Dynamic Ionpage Classnames
+ setStyleClass('other-class')}>Add Class
-
- setStyleClass('other-class')}>Add Class
-
- Div classes: {divClasses}
+
+
+ This page tests that dynamically changing the className prop on IonPage
+ preserves framework classes (ion-page, ion-page-hidden, ion-page-invisible).
+
+
+ Test: Click "Add Class" and verify the classes below include
+ both "other-class" AND "ion-page". If only "other-class" appears,
+ the framework classes were incorrectly removed.
+
+ Current classes: {divClasses}
);
diff --git a/packages/react-router/test/base/src/pages/dynamic-routes/DynamicRoutes.tsx b/packages/react-router/test/base/src/pages/dynamic-routes/DynamicRoutes.tsx
index 89a598c4636..4fb55e8417a 100644
--- a/packages/react-router/test/base/src/pages/dynamic-routes/DynamicRoutes.tsx
+++ b/packages/react-router/test/base/src/pages/dynamic-routes/DynamicRoutes.tsx
@@ -1,4 +1,3 @@
-import React, { useState, ReactElement } from 'react';
import {
IonContent,
IonHeader,
@@ -7,32 +6,32 @@ import {
IonToolbar,
IonRouterOutlet,
} from '@ionic/react';
-import { Route, Redirect } from 'react-router';
+import React, { useState } from 'react';
+import type { ReactElement } from 'react';
+import { Route, Navigate } from 'react-router';
import { Link } from 'react-router-dom';
const DynamicRoutes: React.FC = () => {
+ const addRoute = () => {
+ const newRoute = (
+ } />
+ );
+ setRoutes([...routes, newRoute]);
+ };
+
const [routes, setRoutes] = useState([
}
- exact={true}
+ element={ }
/>,
]);
- const addRoute = () => {
- const newRoute = (
-
- );
- setRoutes([...routes, newRoute]);
- };
-
return (
{routes}
- {/* } /> */}
- } />
- } />
+ } />
+ } />
);
};
diff --git a/packages/react-router/test/base/src/pages/dynamic-tabs/DynamicTabs.tsx b/packages/react-router/test/base/src/pages/dynamic-tabs/DynamicTabs.tsx
index 5f2203dd389..cb1079328a8 100644
--- a/packages/react-router/test/base/src/pages/dynamic-tabs/DynamicTabs.tsx
+++ b/packages/react-router/test/base/src/pages/dynamic-tabs/DynamicTabs.tsx
@@ -1,11 +1,9 @@
-import React, { useState, useCallback } from 'react';
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
- IonApp,
IonTabs,
IonRouterOutlet,
IonTabBar,
@@ -14,56 +12,36 @@ import {
IonLabel,
IonButton,
} from '@ionic/react';
-import { Route, Redirect } from 'react-router';
-import { IonReactRouter } from '@ionic/react-router';
import { triangle, square } from 'ionicons/icons';
+import React, { useState, useCallback } from 'react';
+import { Route, Navigate } from 'react-router';
const DynamicTabs: React.FC = () => {
const [display2ndTab, setDisplayThirdTab] = useState(false);
- const renderFirstTab = useCallback(() => {
- return setDisplayThirdTab(!display2ndTab)} />;
- }, [display2ndTab]);
-
- const render2ndTabRoute = useCallback(() => {
- if (display2ndTab) {
- return ;
- } else {
- // This is weird, if I return null or undefined then I get all sorts of errors, seemingly
- // because the router is mad about a child not being a route.
- return ;
- }
- }, [display2ndTab]);
-
return (
-
-
-
-
-
- {render2ndTabRoute()}
- }
- exact={true}
- />
- } />
-
-
-
-
- Tab 1
-
- {display2ndTab && (
-
-
- Tab 2
-
- )}
-
-
-
-
+
+
+ } />
+ : }
+ />
+ } />
+
+
+
+
+ Tab 1
+
+ {display2ndTab && (
+
+
+ Tab 2
+
+ )}
+
+
);
};
diff --git a/packages/react-router/test/base/src/pages/index-param-priority/IndexParamPriority.tsx b/packages/react-router/test/base/src/pages/index-param-priority/IndexParamPriority.tsx
new file mode 100644
index 00000000000..4707a5ef81a
--- /dev/null
+++ b/packages/react-router/test/base/src/pages/index-param-priority/IndexParamPriority.tsx
@@ -0,0 +1,108 @@
+import {
+ IonButton,
+ IonContent,
+ IonHeader,
+ IonLabel,
+ IonPage,
+ IonRouterOutlet,
+ IonTitle,
+ IonToolbar,
+} from '@ionic/react';
+import React from 'react';
+import { Route } from 'react-router';
+import { useParams } from 'react-router-dom';
+
+/**
+ * Test page for route specificity and priority:
+ *
+ * 1. Sort consistency: index routes should be prioritized over parameterized routes
+ * in both findRouteByRouteInfo (creation) and sortViewsBySpecificity (lookup).
+ *
+ * 2. Wildcard vs param matching: multi-segment paths like "deep/nested/path" should
+ * match the wildcard (*), NOT the single-param route (:slug). The :slug route
+ * should only match single-segment paths.
+ *
+ * Route configuration:
+ * (index route)
+ * (single-param route)
+ * (catch-all wildcard)
+ */
+const IndexParamPriority: React.FC = () => (
+
+
+
+ Index Param Priority
+
+
+
+
+ } />
+ } />
+ } />
+
+
+
+);
+
+const IndexPage: React.FC = () => (
+
+
+
+ Index Page
+
+
+
+ This is the index page
+
+ Go to Slug "hello"
+
+
+ Go to Slug "world"
+
+
+ Go to Deep Path (wildcard)
+
+
+
+);
+
+const SlugPage: React.FC = () => {
+ const { slug } = useParams<{ slug: string }>();
+
+ return (
+
+
+
+ Slug: {slug}
+
+
+
+ Slug page: {slug}
+
+ Back to Index
+
+
+ Go to Slug "world"
+
+
+
+ );
+};
+
+const NotFoundPage: React.FC = () => (
+
+
+
+ Not Found
+
+
+
+ Page not found (wildcard catch-all)
+
+ Back to Index
+
+
+
+);
+
+export default IndexParamPriority;
diff --git a/packages/react-router/test/base/src/pages/index-route-reuse/IndexRouteReuse.tsx b/packages/react-router/test/base/src/pages/index-route-reuse/IndexRouteReuse.tsx
new file mode 100644
index 00000000000..e4791af0888
--- /dev/null
+++ b/packages/react-router/test/base/src/pages/index-route-reuse/IndexRouteReuse.tsx
@@ -0,0 +1,180 @@
+import {
+ IonContent,
+ IonHeader,
+ IonPage,
+ IonTitle,
+ IonToolbar,
+ IonRouterOutlet,
+ IonTabs,
+ IonTabBar,
+ IonTabButton,
+ IonIcon,
+ IonLabel,
+ IonButton,
+ IonBackButton,
+ IonButtons,
+} from '@ionic/react';
+import { triangle, ellipse, square } from 'ionicons/icons';
+import React from 'react';
+import { Route, Navigate } from 'react-router-dom';
+
+/**
+ * Test page for index route reuse across tabs with nested outlets.
+ *
+ * Each tab has its own nested IonRouterOutlet with an index route that
+ * renders distinct content. This tests that switching between tabs shows
+ * the correct index route content and doesn't incorrectly reuse a view
+ * item from another tab's index route.
+ */
+
+// Tab 1 nested outlet with its own index route
+const Tab1Content: React.FC = () => {
+ return (
+
+
+
+ Tab 1 Home
+
+
+
+ Tab 1 Index Route Content
+
+ Go to Tab 1 Detail
+
+
+
+ );
+};
+
+const Tab1Detail: React.FC = () => {
+ return (
+
+
+
+
+
+
+ Tab 1 Detail
+
+
+
+ Tab 1 Detail Content
+
+
+ );
+};
+
+const Tab1Outlet: React.FC = () => {
+ return (
+
+
+ } />
+ } />
+
+
+ );
+};
+
+// Tab 2 nested outlet with its own index route
+const Tab2Content: React.FC = () => {
+ return (
+
+
+
+ Tab 2 Home
+
+
+
+ Tab 2 Index Route Content
+
+ Go to Tab 2 Detail
+
+
+
+ );
+};
+
+const Tab2Detail: React.FC = () => {
+ return (
+
+
+
+
+
+
+ Tab 2 Detail
+
+
+
+ Tab 2 Detail Content
+
+
+ );
+};
+
+const Tab2Outlet: React.FC = () => {
+ return (
+
+
+ } />
+ } />
+
+
+ );
+};
+
+// Tab 3 nested outlet with its own index route
+const Tab3Content: React.FC = () => {
+ return (
+
+
+
+ Tab 3 Home
+
+
+
+ Tab 3 Index Route Content
+
+
+ );
+};
+
+const Tab3Outlet: React.FC = () => {
+ return (
+
+
+ } />
+
+
+ );
+};
+
+// Main tabs component
+const IndexRouteReuse: React.FC = () => {
+ return (
+
+
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+ Tab 1
+
+
+
+ Tab 2
+
+
+
+ Tab 3
+
+
+
+ );
+};
+
+export default IndexRouteReuse;
diff --git a/packages/react-router/test/base/src/pages/ion-route-props/IonRouteProps.tsx b/packages/react-router/test/base/src/pages/ion-route-props/IonRouteProps.tsx
new file mode 100644
index 00000000000..7e13f22d947
--- /dev/null
+++ b/packages/react-router/test/base/src/pages/ion-route-props/IonRouteProps.tsx
@@ -0,0 +1,78 @@
+import {
+ IonButton,
+ IonContent,
+ IonHeader,
+ IonPage,
+ IonRouterOutlet,
+ IonTitle,
+ IonToolbar,
+ IonRoute,
+} from '@ionic/react';
+import React from 'react';
+import { Route } from 'react-router-dom';
+
+/**
+ * Test page for IonRoute index and caseSensitive props.
+ *
+ * Verifies that:
+ * 1. renders as the default when navigating to the parent path
+ * 2. correctly enforces case-sensitive path matching
+ */
+
+const IndexHome: React.FC = () => (
+
+
+
+ Index Home
+
+
+
+ This is the index route
+
+ Go to Details
+
+
+
+);
+
+const Details: React.FC = () => (
+
+
+
+ Details
+
+
+
+ Details page
+
+ Back to Home
+
+
+
+);
+
+const CaseSensitivePage: React.FC = () => (
+
+
+
+ Case Sensitive Match
+
+
+
+ Matched case-sensitive route
+
+
+);
+
+const IonRoutePropsTest: React.FC = () => (
+
+ {/* Index route: should render when navigating to /ion-route-props */}
+ } />
+ } />
+ } />
+ {/* Catch-all for unmatched routes */}
+ } />
+
+);
+
+export default IonRoutePropsTest;
diff --git a/packages/react-router/test/base/src/pages/modal-aria-hidden/ModalAriaHidden.tsx b/packages/react-router/test/base/src/pages/modal-aria-hidden/ModalAriaHidden.tsx
new file mode 100644
index 00000000000..0c884ca4979
--- /dev/null
+++ b/packages/react-router/test/base/src/pages/modal-aria-hidden/ModalAriaHidden.tsx
@@ -0,0 +1,98 @@
+import {
+ IonButton,
+ IonContent,
+ IonHeader,
+ IonModal,
+ IonPage,
+ IonRouterOutlet,
+ IonTitle,
+ IonToolbar,
+} from '@ionic/react';
+import React, { useState } from 'react';
+import { Navigate, Route, useNavigate } from 'react-router-dom';
+
+/**
+ * Tests that aria-hidden is cleaned up on the root router outlet
+ * when a modal is removed from the DOM without being dismissed
+ * (e.g., navigating to a different nested outlet while the modal is open).
+ *
+ * Structure:
+ * - Parent IonRouterOutlet (id="modal-aria-hidden-root")
+ * - SectionA (nested IonRouterOutlet with ionPage)
+ * - PageA: has a modal with a button that navigates to SectionB
+ * - SectionB (nested IonRouterOutlet with ionPage)
+ * - PageB: simple page
+ */
+
+const PageA: React.FC = () => {
+ const [isOpen, setIsOpen] = useState(false);
+ const navigate = useNavigate();
+
+ return (
+
+
+
+ Page A
+
+
+
+ setIsOpen(true)}>
+ Open Modal
+
+ setIsOpen(false)}>
+
+ {
+ // Navigate without dismissing the modal first.
+ // The modal will be auto-removed when React unmounts it,
+ // but aria-hidden on the root outlet should be cleaned up.
+ navigate('/modal-aria-hidden/section-b');
+ }}
+ >
+ Navigate to Section B
+
+
+
+
+
+ );
+};
+
+const PageB: React.FC = () => (
+
+
+
+ Page B
+
+
+
+ Page B Content
+
+
+);
+
+const SectionA: React.FC = () => (
+
+ } />
+
+);
+
+const SectionB: React.FC = () => (
+
+ } />
+
+);
+
+const ModalAriaHidden: React.FC = () => (
+
+ } />
+ } />
+ }
+ />
+
+);
+
+export default ModalAriaHidden;
diff --git a/packages/react-router/test/base/src/pages/muiltiple-tabs/Menu.tsx b/packages/react-router/test/base/src/pages/muiltiple-tabs/Menu.tsx
index dbde2166f6d..92f8e378b86 100644
--- a/packages/react-router/test/base/src/pages/muiltiple-tabs/Menu.tsx
+++ b/packages/react-router/test/base/src/pages/muiltiple-tabs/Menu.tsx
@@ -1,5 +1,6 @@
-import React from 'react';
import { IonMenu, IonContent, IonList, IonItem, IonLabel, IonMenuToggle } from '@ionic/react';
+import React from 'react';
+
export const Menu: React.FC = () => {
return (
diff --git a/packages/react-router/test/base/src/pages/muiltiple-tabs/MultipleTabs.tsx b/packages/react-router/test/base/src/pages/muiltiple-tabs/MultipleTabs.tsx
index a0efdf560fd..df7225cbbf9 100644
--- a/packages/react-router/test/base/src/pages/muiltiple-tabs/MultipleTabs.tsx
+++ b/packages/react-router/test/base/src/pages/muiltiple-tabs/MultipleTabs.tsx
@@ -1,4 +1,3 @@
-import React from 'react';
import {
IonSplitPane,
IonRouterOutlet,
@@ -15,10 +14,12 @@ import {
IonTitle,
IonToolbar,
} from '@ionic/react';
+import { triangle, ellipse, square, rocket } from 'ionicons/icons';
+import React from 'react';
+import { Route, Navigate } from 'react-router';
-import { Route, Redirect } from 'react-router';
import { Menu } from './Menu';
-import { triangle, ellipse, square, rocket } from 'ionicons/icons';
+
const MultipleTabs: React.FC = () => {
return (
@@ -26,24 +27,14 @@ const MultipleTabs: React.FC = () => {
{
- return ;
- }}
- exact={false}
- />
- {
- return ;
- }}
- exact={false}
+ path="/multiple-tabs/tab1/*"
+ element={ }
/>
}
- exact={true}
+ path="/multiple-tabs/tab2/*"
+ element={ }
/>
+ } />
);
@@ -68,12 +59,10 @@ const Tab1: React.FC = () => {
}
- exact={true}
+ element={ }
/>
- {/* */}
- } exact={true} />
- } exact={true} />
+ } />
+ } />
);
@@ -95,12 +84,10 @@ const Tab2: React.FC = () => {
}
- exact={true}
+ element={ }
/>
- {/* */}
- } exact={true} />
- } exact={true} />
+ } />
+ } />
);
diff --git a/packages/react-router/test/base/src/pages/multi-step-back/MultiStepBack.tsx b/packages/react-router/test/base/src/pages/multi-step-back/MultiStepBack.tsx
new file mode 100644
index 00000000000..c2791f8a9ab
--- /dev/null
+++ b/packages/react-router/test/base/src/pages/multi-step-back/MultiStepBack.tsx
@@ -0,0 +1,118 @@
+import {
+ IonContent,
+ IonHeader,
+ IonPage,
+ IonTitle,
+ IonToolbar,
+ IonButton,
+ IonRouterOutlet,
+ IonBackButton,
+ IonButtons,
+} from '@ionic/react';
+import React from 'react';
+import { Route, useNavigate } from 'react-router-dom';
+
+/**
+ * Tests for navigate(-n) where n > 1 (multi-step back navigation).
+ * Verifies that the correct view is shown when skipping multiple
+ * entries in the history stack.
+ */
+const PageA: React.FC = () => {
+ const navigate = useNavigate();
+ return (
+
+
+
+ Page A
+
+
+
+ navigate('/multi-step-back/b')}>
+ Go to Page B
+
+
+
+ );
+};
+
+const PageB: React.FC = () => {
+ const navigate = useNavigate();
+ return (
+
+
+
+
+
+
+ Page B
+
+
+
+ navigate('/multi-step-back/c')}>
+ Go to Page C
+
+
+
+ );
+};
+
+const PageC: React.FC = () => {
+ const navigate = useNavigate();
+ return (
+
+
+
+
+
+
+ Page C
+
+
+
+ navigate('/multi-step-back/d')}>
+ Go to Page D
+
+ navigate(-2)}>
+ Go Back 2 (to A)
+
+
+
+ );
+};
+
+const PageD: React.FC = () => {
+ const navigate = useNavigate();
+ return (
+
+
+
+
+
+
+ Page D
+
+
+
+ navigate(-2)}>
+ Go Back 2 (to B)
+
+ navigate(-3)}>
+ Go Back 3 (to A)
+
+
+
+ );
+};
+
+const MultiStepBack: React.FC = () => {
+ return (
+
+ } />
+ } />
+ } />
+ } />
+
+ );
+};
+
+export default MultiStepBack;
diff --git a/packages/react-router/test/base/src/pages/nested-outlet/NestedOutlet.tsx b/packages/react-router/test/base/src/pages/nested-outlet/NestedOutlet.tsx
index 62aa9e1e7f6..a3ca6da2027 100644
--- a/packages/react-router/test/base/src/pages/nested-outlet/NestedOutlet.tsx
+++ b/packages/react-router/test/base/src/pages/nested-outlet/NestedOutlet.tsx
@@ -7,9 +7,8 @@ import {
IonTitle,
IonToolbar,
} from '@ionic/react';
-import { useEffect } from 'react';
-import React from 'react';
-import { Route, Redirect } from 'react-router';
+import React, { useEffect } from 'react';
+import { Route, Navigate } from 'react-router';
const Page: React.FC = () => {
useEffect(() => {
@@ -48,10 +47,9 @@ const SecondPage: React.FC = () => {
}
+ element={ }
/>
-
+ } />
);
};
@@ -81,8 +79,8 @@ const FirstPage: React.FC = () => {
const NestedOutlet: React.FC = () => (
-
-
+ } />
+ } />
);
diff --git a/packages/react-router/test/base/src/pages/nested-outlet/NestedOutlet2.tsx b/packages/react-router/test/base/src/pages/nested-outlet/NestedOutlet2.tsx
index d62911046c8..dcd6d6cfa97 100644
--- a/packages/react-router/test/base/src/pages/nested-outlet/NestedOutlet2.tsx
+++ b/packages/react-router/test/base/src/pages/nested-outlet/NestedOutlet2.tsx
@@ -1,5 +1,3 @@
-import React from 'react';
-import { Redirect, Route, RouteComponentProps } from 'react-router-dom';
import {
IonBackButton,
IonButtons,
@@ -13,12 +11,14 @@ import {
IonTitle,
IonToolbar,
} from '@ionic/react';
+import React from 'react';
+import { Navigate, Route, useParams } from 'react-router-dom';
-const ListPage: React.FC = ({ match }) => {
+const ListPage: React.FC = () => {
return (
-
-
+ } />
+ } />
);
};
@@ -51,7 +51,9 @@ const List: React.FC = () => {
);
};
-const Item: React.FC> = ({ match }) => {
+const Item: React.FC = () => {
+ const { id } = useParams<{ id: string }>();
+
return (
@@ -62,16 +64,16 @@ const Item: React.FC> = ({ match }) => {
Item
- Detail of item #{match.params.id}
+ Detail of item #{id}
);
};
-const HomePage: React.FC = ({ match }) => {
+const HomePage: React.FC = () => {
return (
-
-
+ } />
+ } />
);
};
@@ -98,7 +100,7 @@ const Welcome: React.FC = () => {
);
};
-const Home: React.FC> = ({ match }) => {
+const Home: React.FC = () => {
return (
@@ -122,12 +124,11 @@ const Home: React.FC> = ({ match }) => {
const NestedOutlet2: React.FC = () => (
-
-
+ } />
+ } />
}
- exact={true}
+ element={ }
/>
);
diff --git a/packages/react-router/test/base/src/pages/nested-params/NestedParams.tsx b/packages/react-router/test/base/src/pages/nested-params/NestedParams.tsx
new file mode 100644
index 00000000000..be9c6ebe7a2
--- /dev/null
+++ b/packages/react-router/test/base/src/pages/nested-params/NestedParams.tsx
@@ -0,0 +1,187 @@
+import {
+ IonButton,
+ IonContent,
+ IonHeader,
+ IonLabel,
+ IonPage,
+ IonRouterOutlet,
+ IonTitle,
+ IonToolbar,
+} from '@ionic/react';
+import React from 'react';
+import { Navigate, Route } from 'react-router';
+import { useParams } from 'react-router-dom';
+
+const NestedParamsRoot: React.FC = () => (
+
+
+
+ Nested Params
+
+
+
+
+ } />
+ } />
+
+
+
+);
+
+const Landing: React.FC = () => (
+
+
+
+ Select a User
+
+
+
+ A nested route will try to read the parent :userId parameter.
+
+ Go to User 42 Details
+
+
+ Go to User 99 Details
+
+
+
+);
+
+const UserLayout: React.FC = () => {
+ const { userId } = useParams<{ userId: string }>();
+
+ return (
+
+
+
+ User {userId ?? 'missing'}
+
+
+
+ Layout sees user: {userId ?? 'missing'}
+
+ } />
+ } />
+ } />
+ } />
+
+
+
+ );
+};
+
+const UserDetails: React.FC = () => {
+ const { userId } = useParams<{ userId: string }>();
+
+ return (
+
+
+
+ Details
+
+
+
+ Details view user: {userId ?? 'missing'}
+
+ Back to Landing
+
+
+ Go to Settings
+
+
+ Go to Profile Edit
+
+
+
+ );
+};
+
+/**
+ * Deeply nested layout: /nested-params/user/:userId/profile/*
+ * This tests 4 levels of outlet nesting with relative paths to validate
+ * that derivePathnameToMatch correctly handles depth > 2 parent segments.
+ */
+const ProfileLayout: React.FC = () => {
+ const { userId } = useParams<{ userId: string }>();
+
+ return (
+
+
+
+ Profile Layout
+
+
+
+ Profile layout user: {userId ?? 'missing'}
+
+ } />
+ } />
+ } />
+
+
+
+ );
+};
+
+const ProfileEdit: React.FC = () => {
+ const { userId } = useParams<{ userId: string }>();
+
+ return (
+
+
+
+ Profile Edit
+
+
+
+ Profile edit user: {userId ?? 'missing'}
+
+ Go to Profile View
+
+
+ Back to Details
+
+
+
+ );
+};
+
+const ProfileView: React.FC = () => {
+ const { userId } = useParams<{ userId: string }>();
+
+ return (
+
+
+
+ Profile View
+
+
+
+ Profile view user: {userId ?? 'missing'}
+
+ Back to Profile Edit
+
+
+
+ );
+};
+
+const UserSettings: React.FC = () => {
+ const { userId } = useParams<{ userId: string }>();
+
+ return (
+
+
+
+ Settings
+
+
+
+ Settings view user: {userId ?? 'missing'}
+ Back to Details
+
+
+ );
+};
+
+export default NestedParamsRoot;
diff --git a/packages/react-router/test/base/src/pages/nested-tabs-relative-links/NestedTabsRelativeLinks.tsx b/packages/react-router/test/base/src/pages/nested-tabs-relative-links/NestedTabsRelativeLinks.tsx
new file mode 100644
index 00000000000..8b51f82d39f
--- /dev/null
+++ b/packages/react-router/test/base/src/pages/nested-tabs-relative-links/NestedTabsRelativeLinks.tsx
@@ -0,0 +1,194 @@
+import {
+ IonContent,
+ IonHeader,
+ IonPage,
+ IonTitle,
+ IonToolbar,
+ IonRouterOutlet,
+ IonTabs,
+ IonTabBar,
+ IonTabButton,
+ IonIcon,
+ IonLabel,
+ IonBackButton,
+ IonButtons,
+} from '@ionic/react';
+import { triangle, ellipse, square } from 'ionicons/icons';
+import React from 'react';
+import { Link, Navigate, Route } from 'react-router-dom';
+
+/**
+ * This test page verifies that relative links work correctly within
+ * nested IonRouterOutlet components, specifically in a tabs-based layout.
+ *
+ * Issue: When using React Router's inside the tab1 route
+ * with nested outlets and index routes, the relative path resolution can produce
+ * incorrect URLs (e.g., /tab1/tab1/page-a instead of /tab1/page-a).
+ *
+ * This test also verifies that absolute links work when a catch-all route
+ * is present.
+ */
+
+// Tab content with relative links for testing
+const Tab1Content: React.FC = () => {
+ return (
+
+
+
+ Tab 1
+
+
+
+
+
Tab 1 - Home Page
+ {/* Relative link - should navigate to /nested-tabs-relative-links/tab1/page-a */}
+
+ Go to Page A (relative)
+
+
+ {/* Absolute link - should also work */}
+
+ Go to Page A (absolute)
+
+
+ {/* Another relative link */}
+
+ Go to Page B (relative)
+
+
+
+
+ );
+};
+
+const PageA: React.FC = () => {
+ return (
+
+
+
+
+
+
+ Page A
+
+
+
+
+ This is Page A within Tab 1
+
+
+
+ );
+};
+
+const PageB: React.FC = () => {
+ return (
+
+
+
+
+
+
+ Page B
+
+
+
+
+ This is Page B within Tab 1
+
+
+
+ );
+};
+
+// Nested router outlet for Tab 1 - similar to user's RouterOutletTab1
+const Tab1RouterOutlet: React.FC = () => {
+ return (
+
+ } />
+ } />
+ } />
+
+ );
+};
+
+const Tab2Content: React.FC = () => {
+ return (
+
+
+
+ Tab 2
+
+
+
+
+ Tab 2 Content
+
+
+
+ );
+};
+
+const Tab3Content: React.FC = () => {
+ return (
+
+
+
+ Tab 3
+
+
+
+
+ Tab 3 Content
+
+
+
+ );
+};
+
+// Main tabs component - wraps tabs with catch-all route (similar to user's reproduction)
+const TabsContainer: React.FC = () => (
+
+
+ {/* Tab 1 has nested routes with index route */}
+ } />
+ } />
+ } />
+ } />
+ {/* Catch-all 404 route - this presence caused issues with absolute links */}
+
+
+ 404 - Not Found
+
+
+ }
+ />
+
+
+
+
+ Tab 1
+
+
+
+ Tab 2
+
+
+
+ Tab 3
+
+
+
+);
+
+// Top-level component - splat route renders tabs
+const NestedTabsRelativeLinks: React.FC = () => (
+
+ } />
+
+);
+
+export default NestedTabsRelativeLinks;
diff --git a/packages/react-router/test/base/src/pages/outlet-ref/OutletRef.tsx b/packages/react-router/test/base/src/pages/outlet-ref/OutletRef.tsx
index 9aa3e9e0ba9..53c9c2ffebc 100644
--- a/packages/react-router/test/base/src/pages/outlet-ref/OutletRef.tsx
+++ b/packages/react-router/test/base/src/pages/outlet-ref/OutletRef.tsx
@@ -1,4 +1,3 @@
-import React, { useRef, useEffect } from 'react';
import {
IonRouterOutlet,
IonPage,
@@ -7,24 +6,25 @@ import {
IonTitle,
IonContent,
} from '@ionic/react';
+import React, { useRef, useEffect, useState } from 'react';
import { Route } from 'react-router';
-interface OutletRefProps {}
-
-export const OutletRef: React.FC = () => {
+export const OutletRef: React.FC = () => {
const ref = useRef(null);
+ const [outletId, setOutletId] = useState(undefined);
useEffect(() => {
- console.log(ref);
+ // Update the outlet id once the ref is populated
+ if (ref.current?.id) {
+ setOutletId(ref.current.id);
+ }
}, []);
return (
{
- return ;
- }}
+ element={ }
/>
);
diff --git a/packages/react-router/test/base/src/pages/overlays/Overlays.tsx b/packages/react-router/test/base/src/pages/overlays/Overlays.tsx
index 9343df83b97..19fe9575bb8 100644
--- a/packages/react-router/test/base/src/pages/overlays/Overlays.tsx
+++ b/packages/react-router/test/base/src/pages/overlays/Overlays.tsx
@@ -1,15 +1,15 @@
import { IonButton, IonContent, IonModal } from '@ionic/react';
import { useState } from 'react';
-import { useHistory } from 'react-router';
+import { useNavigate } from 'react-router-dom';
const Overlays: React.FC = () => {
const [isOpen, setIsOpen] = useState(false);
- const history = useHistory();
+ const navigate = useNavigate();
- const goBack = () => history.goBack();
- const replace = () => history.replace('/');
- const push = () => history.push('/');
+ const goBack = () => navigate(-1);
+ const replace = () => navigate('/', { replace: true });
+ const push = () => navigate('/');
return (
<>
diff --git a/packages/react-router/test/base/src/pages/param-swipe-back/ParamSwipeBack.tsx b/packages/react-router/test/base/src/pages/param-swipe-back/ParamSwipeBack.tsx
new file mode 100644
index 00000000000..b8536c15901
--- /dev/null
+++ b/packages/react-router/test/base/src/pages/param-swipe-back/ParamSwipeBack.tsx
@@ -0,0 +1,164 @@
+import {
+ IonRouterOutlet,
+ IonPage,
+ IonHeader,
+ IonToolbar,
+ IonTitle,
+ IonContent,
+ IonItem,
+ IonButton,
+ IonButtons,
+ IonBackButton,
+} from '@ionic/react';
+import React from 'react';
+import { Route } from 'react-router';
+import { useParams, useNavigate } from 'react-router-dom';
+
+/**
+ * Test scenarios for issue #27900:
+ * Swipe back gesture breaks when navigating between parameterized routes.
+ *
+ * Reproduction A: /user/alex -> /middle -> /user/sean -> swipe back -> swipe back (broken)
+ * Reproduction B: /item/one -> replace /item/two -> /item/two/details -> swipe back (broken)
+ */
+
+// --- Reproduction A ---
+export const ParamSwipeBack: React.FC = () => {
+ return (
+
+ } />
+ } />
+ } />
+
+ );
+};
+
+const Start: React.FC = () => {
+ return (
+
+
+
+ Param Swipe Back
+
+
+
+
+ Go to User Alex
+
+
+ Go to User Sean
+
+
+ Go to Middle
+
+
+
+ );
+};
+
+const UserPage: React.FC = () => {
+ const { name } = useParams<{ name: string }>();
+ return (
+
+
+
+
+
+
+ User {name}
+
+
+
+ User: {name}
+
+ Go to Middle
+
+
+ Go to User Alex
+
+
+ Go to User Sean
+
+
+
+ );
+};
+
+const MiddlePage: React.FC = () => {
+ return (
+
+
+
+
+
+
+ Middle Page
+
+
+
+ Middle Page
+
+ Go to User Alex
+
+
+ Go to User Sean
+
+
+
+ );
+};
+
+// --- Reproduction B ---
+export const ParamSwipeBackB: React.FC = () => {
+ return (
+
+ } />
+ } />
+
+ );
+};
+
+const ItemPage: React.FC = () => {
+ const { name } = useParams<{ name: string }>();
+ const navigate = useNavigate();
+ return (
+
+
+
+
+
+
+ Item {name}
+
+
+
+ Item: {name}
+ navigate('/param-swipe-back-b/item/two', { replace: true })}>
+ Replace with /item/two
+
+
+ Go to Details
+
+
+
+ );
+};
+
+const ItemDetails: React.FC = () => {
+ const { name } = useParams<{ name: string }>();
+ return (
+
+
+
+
+
+
+ Details for {name}
+
+
+
+ Details for item: {name}
+
+
+ );
+};
diff --git a/packages/react-router/test/base/src/pages/params/Params.tsx b/packages/react-router/test/base/src/pages/params/Params.tsx
index 18f49d45888..b60903bb1c3 100644
--- a/packages/react-router/test/base/src/pages/params/Params.tsx
+++ b/packages/react-router/test/base/src/pages/params/Params.tsx
@@ -1,4 +1,3 @@
-import React from 'react';
import {
IonButtons,
IonBackButton,
@@ -9,30 +8,29 @@ import {
IonTitle,
IonToolbar,
} from '@ionic/react';
-import { RouteComponentProps } from 'react-router';
-
-interface PageProps
-extends RouteComponentProps<{
- id: string;
-}> {}
+import React from 'react';
+import { useParams } from 'react-router-dom';
+const Page: React.FC = () => {
+ const { id } = useParams<{ id: string }>();
+ const parseID = id ? parseInt(id) : NaN;
+ const displayId = id || 'N/A';
+ const nextParamLink = !isNaN(parseID) ? `/params/${parseID + 1}` : '/params/1';
-const Page: React.FC = ({ match }) => {
- const parseID = parseInt(match.params.id);
return (
-
+
- Params { match.params.id }
+ Params { displayId }
- Go to next param
+ Go to next param
- Page ID: { match.params.id }
+ Page ID: { displayId }
);
diff --git a/packages/react-router/test/base/src/pages/prefix-match-wildcard/PrefixMatchWildcard.tsx b/packages/react-router/test/base/src/pages/prefix-match-wildcard/PrefixMatchWildcard.tsx
new file mode 100644
index 00000000000..44d12a1a4b6
--- /dev/null
+++ b/packages/react-router/test/base/src/pages/prefix-match-wildcard/PrefixMatchWildcard.tsx
@@ -0,0 +1,85 @@
+import {
+ IonContent,
+ IonHeader,
+ IonPage,
+ IonTitle,
+ IonToolbar,
+ IonRouterOutlet,
+ IonButton,
+} from '@ionic/react';
+import React from 'react';
+import { Route, useNavigate } from 'react-router-dom';
+
+/**
+ * Test page for verifying that wildcard routes work correctly when
+ * specific routes share a common prefix with the navigation target.
+ *
+ * Bug: couldSpecificRouteMatch uses a 3-char prefix heuristic that
+ * falsely suppresses wildcard matches when routes share a prefix
+ * (e.g., "settings" vs "setup" both start with "set").
+ */
+
+const SettingsPage: React.FC = () => (
+
+
+
+ Settings
+
+
+
+ Settings Page
+
+
+);
+
+const CatchAllPage: React.FC = () => (
+
+
+
+ Catch All
+
+
+
+ Wildcard Catch-All Page
+
+
+);
+
+const PrefixMatchHome: React.FC = () => {
+ const navigate = useNavigate();
+
+ return (
+
+
+
+ Prefix Match Test
+
+
+
+
+ navigate('settings')}>
+ Go to Settings
+
+ navigate('setup')}>
+ Go to Setup (should hit wildcard)
+
+ navigate('unknown')}>
+ Go to Unknown (should hit wildcard)
+
+
+
+
+ );
+};
+
+const PrefixMatchWildcard: React.FC = () => {
+ return (
+
+ } />
+ } />
+ } />
+
+ );
+};
+
+export default PrefixMatchWildcard;
diff --git a/packages/react-router/test/base/src/pages/redirect-params/RedirectParams.tsx b/packages/react-router/test/base/src/pages/redirect-params/RedirectParams.tsx
new file mode 100644
index 00000000000..9bea7e6c2c4
--- /dev/null
+++ b/packages/react-router/test/base/src/pages/redirect-params/RedirectParams.tsx
@@ -0,0 +1,177 @@
+/**
+ * Verifies that useParams() returns correct values after a Navigate
+ * (catch-all redirect) fires inside IonRouterOutlet.
+ */
+
+import {
+ IonTabs,
+ IonRouterOutlet,
+ IonTabBar,
+ IonTabButton,
+ IonIcon,
+ IonLabel,
+ IonPage,
+ IonHeader,
+ IonToolbar,
+ IonTitle,
+ IonContent,
+ IonButton,
+} from '@ionic/react';
+import { triangle, square } from 'ionicons/icons';
+import React from 'react';
+import { Route, Navigate, useParams } from 'react-router-dom';
+
+/**
+ * Scenario 1: Tabs with catch-all Navigate redirect
+ *
+ * Structure:
+ * /redirect-params/tabs β redirects to /redirect-params/tabs/tab2
+ * /redirect-params/tabs/tab1/:id β Tab1 with param
+ * /redirect-params/tabs/tab2 β Tab2
+ */
+const TabsWithRedirect: React.FC = () => {
+ return (
+
+
+ } />
+ } />
+ } />
+
+
+
+
+ Tab1
+
+
+
+ Tab2
+
+
+
+ );
+};
+
+const Tab1WithParam: React.FC = () => {
+ const { id } = useParams<{ id: string }>();
+ return (
+
+
+
+ Tab 1
+
+
+
+ {id}
+ Tab 1 with param: {id || 'undefined'}
+
+
+ );
+};
+
+const Tab2Page: React.FC = () => {
+ return (
+
+
+
+ Tab 2
+
+
+
+ Tab 2 loaded
+
+ Go to Tab 1 with param
+
+
+
+ );
+};
+
+/**
+ * Scenario 2: Flat outlet with catch-all Navigate redirect
+ *
+ * Structure:
+ * /redirect-params/flat β redirects to /redirect-params/flat/home
+ * /redirect-params/flat/home β Home page
+ * /redirect-params/flat/details/:id β Details with param
+ */
+const FlatOutletWithRedirect: React.FC = () => {
+ return (
+
+ } />
+ } />
+ } />
+
+ );
+};
+
+const HomePage: React.FC = () => {
+ return (
+
+
+
+ Home
+
+
+
+ Home loaded
+
+ Go to Details 42
+
+
+ Go to Details 99
+
+
+
+ );
+};
+
+const DetailsPage: React.FC = () => {
+ const { id } = useParams<{ id: string }>();
+ return (
+
+
+
+ Details
+
+
+
+ {id}
+ Detail ID: {id || 'undefined'}
+
+
+ );
+};
+
+/**
+ * Main entry: routes into tabs vs flat scenarios
+ */
+const RedirectParams: React.FC = () => {
+ return (
+
+ } />
+ } />
+
+
+
+ Redirect Params Test
+
+
+
+
+ Tabs Scenario
+
+
+ Flat Outlet Scenario
+
+
+
+ }
+ />
+
+ );
+};
+
+export default RedirectParams;
diff --git a/packages/react-router/test/base/src/pages/refs/Refs.tsx b/packages/react-router/test/base/src/pages/refs/Refs.tsx
index 7ac15c83ab3..667f7be5f9f 100644
--- a/packages/react-router/test/base/src/pages/refs/Refs.tsx
+++ b/packages/react-router/test/base/src/pages/refs/Refs.tsx
@@ -1,27 +1,25 @@
-import React, { useRef } from "react";
import {
IonContent,
IonHeader,
IonPage,
IonRouterOutlet,
IonTitle,
+ IonText,
IonToolbar,
} from "@ionic/react";
+import React, { useRef } from "react";
import { Route } from "react-router";
-interface RefsProps {}
-
const Refs: React.FC = () => {
return (
- {/* } /> */}
-
-
+ } />
+ } />
);
};
-const RefsFC: React.FC = () => {
+const RefsFC: React.FC = () => {
const contentRef = useRef(null);
return (
@@ -30,7 +28,11 @@ const RefsFC: React.FC = () => {
Refs FC
-
+
+
+ This view is used for automated ref regression tests.
+
+
);
};
@@ -45,7 +47,11 @@ class RefsClass extends React.Component {
Refs Class
-
+
+
+ This view is used for automated ref regression tests.
+
+
);
}
diff --git a/packages/react-router/test/base/src/pages/relative-paths/RelativePaths.tsx b/packages/react-router/test/base/src/pages/relative-paths/RelativePaths.tsx
new file mode 100644
index 00000000000..39403284443
--- /dev/null
+++ b/packages/react-router/test/base/src/pages/relative-paths/RelativePaths.tsx
@@ -0,0 +1,129 @@
+import {
+ IonContent,
+ IonHeader,
+ IonPage,
+ IonTitle,
+ IonToolbar,
+ IonRouterOutlet,
+ IonList,
+ IonItem,
+ IonLabel,
+ IonBackButton,
+ IonButtons,
+} from '@ionic/react';
+import React from 'react';
+import { Route } from 'react-router-dom';
+
+/**
+ * This test page verifies that IonRouterOutlet correctly handles
+ * relative paths (paths without a leading slash) the same way
+ * React Router 6's Routes component does.
+ */
+
+const RelativePathsHome: React.FC = () => {
+ return (
+
+
+
+
+
+
+ Relative Paths Test
+
+
+
+
+
+ Go to Page A (absolute path route)
+
+
+ Go to Page B (relative path route)
+
+
+ Go to Unknown Page (catch-all route)
+
+
+
+
+ );
+};
+
+const PageA: React.FC = () => {
+ return (
+
+
+
+
+
+
+ Page A
+
+
+
+
+ This is Page A - route defined with absolute path
+
+
+
+ );
+};
+
+const PageB: React.FC = () => {
+ return (
+
+
+
+
+
+
+ Page B
+
+
+
+
+ This is Page B - route defined with relative path (no leading slash)
+
+
+
+ );
+};
+
+const CatchAllPage: React.FC = () => {
+ return (
+
+
+
+
+
+
+ Not Found
+
+
+
+
+ This page was not found - caught by relative * route
+
+
+
+ );
+};
+
+const RelativePaths: React.FC = () => {
+ return (
+
+ {/* Route with absolute path (has leading slash) - this should work */}
+ } />
+
+ {/* Route with relative path (no leading slash) */}
+ } />
+
+ {/* Catch-all route - using relative wildcard */}
+ } />
+
+ {/* Home route - using relative path */}
+ } />
+
+ );
+};
+
+export default RelativePaths;
diff --git a/packages/react-router/test/base/src/pages/replace-action/Replace.tsx b/packages/react-router/test/base/src/pages/replace-action/Replace.tsx
index 2f4e3f38de7..f9a3e435379 100644
--- a/packages/react-router/test/base/src/pages/replace-action/Replace.tsx
+++ b/packages/react-router/test/base/src/pages/replace-action/Replace.tsx
@@ -1,4 +1,3 @@
-import React from 'react';
import {
IonContent,
IonHeader,
@@ -6,24 +5,13 @@ import {
IonTitle,
IonToolbar,
IonButton,
- IonRouterOutlet,
IonButtons,
IonBackButton,
} from '@ionic/react';
-import { Route, Redirect, useHistory } from 'react-router';
-
-interface TopPageProps {}
+import React from 'react';
+import { useNavigate } from 'react-router-dom';
-const ReplaceAction: React.FC = () => {
- return (
-
-
-
-
- } />
-
- );
-};
+const ReplaceAction: React.FC = () => null;
const Page1: React.FC = () => (
@@ -42,10 +30,10 @@ const Page1: React.FC = () => (
);
const Page2: React.FC = () => {
- const history = useHistory();
+ const navigate = useNavigate();
const clickButton = () => {
- history.replace('/replace-action/page3');
+ navigate('/replace-action/page3', { replace: true });
};
return (
@@ -78,9 +66,11 @@ const Page3: React.FC = () => {
Page 3
+ Note: The back button navigates to Page 1 (not Page 2) because Page 2 used replace navigation.
);
};
export default ReplaceAction;
+export { Page1, Page2, Page3 };
diff --git a/packages/react-router/test/base/src/pages/replace-params/ReplaceParams.tsx b/packages/react-router/test/base/src/pages/replace-params/ReplaceParams.tsx
new file mode 100644
index 00000000000..6dc40590874
--- /dev/null
+++ b/packages/react-router/test/base/src/pages/replace-params/ReplaceParams.tsx
@@ -0,0 +1,132 @@
+import {
+ IonContent,
+ IonHeader,
+ IonPage,
+ IonTitle,
+ IonToolbar,
+ IonButton,
+ IonButtons,
+ IonBackButton,
+} from '@ionic/react';
+import React, { useState } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+
+/**
+ * Test for issue #25640: Replace navigation with parameterized routes
+ * should properly clean up leaving views from the DOM.
+ *
+ * Flow: step1 β (replace) step2/:id β (replace) step3/:id β (push) step4/:id
+ * β (back) β step1 β (replace) step2/:id β (replace) step3/:id
+ *
+ * On the second visit to step3, it should be a fresh component instance
+ * with updated params (verified via a unique mount ID).
+ */
+
+const Step1: React.FC = () => {
+ const navigate = useNavigate();
+
+ return (
+
+
+
+ Step 1
+
+
+
+ navigate('/replace-params/step2/first', { replace: true })}
+ >
+ Replace to Step 2 (first)
+
+ navigate('/replace-params/step2/second', { replace: true })}
+ >
+ Replace to Step 2 (second)
+
+
+
+ );
+};
+
+const Step2: React.FC = () => {
+ const navigate = useNavigate();
+ const { id } = useParams<{ id: string }>();
+
+ return (
+
+
+
+ Step 2
+
+
+
+ {id}
+ navigate(`/replace-params/step3/${id}`, { replace: true })}
+ >
+ Replace to Step 3
+
+
+
+ );
+};
+
+const Step3: React.FC = () => {
+ const navigate = useNavigate();
+ const { id } = useParams<{ id: string }>();
+ const [mountId] = useState(() => Math.random().toString(36).slice(2, 8));
+
+ return (
+
+
+
+ Step 3
+
+
+
+ {id}
+ {mountId}
+ navigate(`/replace-params/step4/${id}`)}
+ >
+ Push to Step 4
+
+
+
+ );
+};
+
+const Step4: React.FC = () => {
+ const navigate = useNavigate();
+ const { id } = useParams<{ id: string }>();
+
+ return (
+
+
+
+
+
+
+ Step 4
+
+
+
+ {id}
+ navigate('/replace-params/step1')}
+ >
+ Go to Step 1
+
+
+
+ );
+};
+
+const ReplaceParams: React.FC = () => null;
+export default ReplaceParams;
+export { Step1, Step2, Step3, Step4 };
diff --git a/packages/react-router/test/base/src/pages/root-splat-tabs/RootSplatTabs.tsx b/packages/react-router/test/base/src/pages/root-splat-tabs/RootSplatTabs.tsx
new file mode 100644
index 00000000000..f967ac96ffd
--- /dev/null
+++ b/packages/react-router/test/base/src/pages/root-splat-tabs/RootSplatTabs.tsx
@@ -0,0 +1,163 @@
+import {
+ IonContent,
+ IonHeader,
+ IonPage,
+ IonTitle,
+ IonToolbar,
+ IonRouterOutlet,
+ IonTabs,
+ IonTabBar,
+ IonTabButton,
+ IonIcon,
+ IonLabel,
+ IonBackButton,
+ IonButtons,
+} from '@ionic/react';
+import { triangle, ellipse, square } from 'ionicons/icons';
+import React from 'react';
+import { Link, Navigate, Route } from 'react-router-dom';
+
+/**
+ * Test page for root-level splat routes with relative tab paths.
+ *
+ * Structure: Outer splat route "*" renders IonTabs, with relative paths
+ * like "tab1/*" (no leading slash) inside the tabs outlet.
+ *
+ * This tests the fix for routes with relative paths inside root-level splat routes.
+ */
+
+// Tab content with relative links for testing
+const Tab1Content: React.FC = () => {
+ return (
+
+
+
+ Tab 1
+
+
+
+
+
Tab 1 - Home Page (Root Splat Test)
+
+ Go to Page A (relative)
+
+
+
+ Go to Page A (absolute)
+
+
+
+
+ );
+};
+
+const PageA: React.FC = () => {
+ return (
+
+
+
+
+
+
+ Page A
+
+
+
+
+ This is Page A within Tab 1 (Root Splat Test)
+
+
+
+ );
+};
+
+// Nested router outlet for Tab 1 - matches customer's RouterOutletTab1
+const Tab1RouterOutlet: React.FC = () => {
+ return (
+
+
+ } />
+ } />
+
+
+ );
+};
+
+const Tab2Content: React.FC = () => {
+ return (
+
+
+
+ Tab 2
+
+
+
+
+ Tab 2 Content (Root Splat Test)
+
+
+
+ );
+};
+
+const Tab3Content: React.FC = () => {
+ return (
+
+
+
+ Tab 3
+
+
+
+
+ Tab 3 Content (Root Splat Test)
+
+
+
+ );
+};
+
+const NotFoundPage: React.FC = () => {
+ return (
+
+
+ 404 - Not Found (Root Splat Test)
+
+
+ );
+};
+
+// Tabs rendered directly inside a catch-all splat route
+const TabsWithSplatRoutes: React.FC = () => {
+ return (
+
+
+ {/* Using RELATIVE path "tab1/*" (no leading slash) - the key test case */}
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+ Tab 1
+
+
+
+ Tab 2
+
+
+
+ Tab 3
+
+
+
+ );
+};
+
+// Main component - renders tabs directly (no outlet wrapper)
+const RootSplatTabs: React.FC = () => ;
+
+export default RootSplatTabs;
diff --git a/packages/react-router/test/base/src/pages/route-context-shape/RouteContextShape.tsx b/packages/react-router/test/base/src/pages/route-context-shape/RouteContextShape.tsx
new file mode 100644
index 00000000000..1d082311e22
--- /dev/null
+++ b/packages/react-router/test/base/src/pages/route-context-shape/RouteContextShape.tsx
@@ -0,0 +1,164 @@
+/**
+ * Test page that validates the UNSAFE_RouteContext shape at runtime.
+ *
+ * Context layers in Ionic React:
+ * 1. RR6 provides native RouteContext at the router level
+ * 2. StackManager.tsx consumes native RR6 context and validates it via validateRouteContext()
+ * 3. ReactRouterViewStack.renderViewItem wraps each view in RouteContext.Provider
+ * with Ionic's constructed context (built by buildContextMatches)
+ * 4. Components inside IonRouterOutlet read Ionic's constructed context
+ *
+ * The RouteContextValidator components here read layer 3/4 (Ionic's constructed context).
+ * They verify that Ionic's buildContextMatches produces the correct shape.
+ * The native RR6 context (layer 1) is validated by the validateRouteContext() call
+ * in StackManager.tsx β the Cypress spec checks this via the console warning assertion.
+ */
+import {
+ IonContent,
+ IonHeader,
+ IonPage,
+ IonTitle,
+ IonToolbar,
+ IonRouterOutlet,
+ IonButton,
+ IonLabel,
+ IonItem,
+ IonList,
+} from '@ionic/react';
+import React, { useContext, useMemo } from 'react';
+import { Route, useParams, useNavigate, UNSAFE_RouteContext } from 'react-router-dom';
+
+/**
+ * Validates a single match entry from the RouteContext matches array.
+ */
+function validateMatchEntry(entry: unknown): { valid: boolean; missing: string[] } {
+ const missing: string[] = [];
+ if (typeof entry !== 'object' || entry === null) {
+ return { valid: false, missing: ['not-an-object'] };
+ }
+
+ const e = entry as Record;
+
+ if (typeof e.params !== 'object' || e.params === null) missing.push('params');
+ if (typeof e.pathname !== 'string') missing.push('pathname');
+ if (typeof e.pathnameBase !== 'string') missing.push('pathnameBase');
+
+ if (typeof e.route !== 'object' || e.route === null) {
+ missing.push('route');
+ } else {
+ const route = e.route as Record;
+ if (typeof route.id !== 'string') missing.push('route.id');
+ if (typeof route.hasErrorBoundary !== 'boolean') missing.push('route.hasErrorBoundary');
+ }
+
+ return { valid: missing.length === 0, missing };
+}
+
+/**
+ * Component that reads RouteContext and exposes validation results as data attributes.
+ * Note: Inside IonRouterOutlet, this reads Ionic's constructed context, not RR6's native context.
+ */
+const RouteContextValidator: React.FC<{ id: string }> = ({ id }) => {
+ const routeContext = useContext(UNSAFE_RouteContext);
+
+ const validation = useMemo(() => {
+ if (!routeContext) {
+ return { hasContext: false, matchCount: 0, allValid: false, details: 'no-context' };
+ }
+
+ const matches = routeContext.matches;
+ if (!Array.isArray(matches)) {
+ return { hasContext: true, matchCount: 0, allValid: false, details: 'matches-not-array' };
+ }
+
+ const results = matches.map((m, i) => {
+ const { valid, missing } = validateMatchEntry(m);
+ return { index: i, valid, missing };
+ });
+
+ const allValid = results.every((r) => r.valid);
+ const invalidEntries = results.filter((r) => !r.valid);
+ const details = allValid
+ ? 'all-valid'
+ : invalidEntries.map((e) => `match[${e.index}]:${e.missing.join(',')}`).join(';');
+
+ return { hasContext: true, matchCount: matches.length, allValid, details };
+ }, [routeContext]);
+
+ return (
+
+
Context: {validation.hasContext ? 'yes' : 'no'}
+
Matches: {validation.matchCount}
+
Valid: {validation.allValid ? 'yes' : 'no'}
+
Details: {validation.details}
+
+ );
+};
+
+/**
+ * A nested page that validates context at a deeper level.
+ */
+const NestedPage: React.FC = () => {
+ const params = useParams();
+ return (
+
+
+
+ Nested (id: {params.id})
+
+
+
+
+ {JSON.stringify(params)}
+
+
+ );
+};
+
+/**
+ * Root page for the route-context-shape test.
+ */
+const RouteContextShape: React.FC = () => {
+ const navigate = useNavigate();
+
+ return (
+
+ }
+ />
+
+
+
+ Route Context Shape
+
+
+
+
+
+
+
+ navigate('details/42')}>
+ Go to Nested
+
+
+
+
+
+
+ }
+ />
+
+ );
+};
+
+export default RouteContextShape;
diff --git a/packages/react-router/test/base/src/pages/routing/Details.tsx b/packages/react-router/test/base/src/pages/routing/Details.tsx
index 94aeeb6dbaf..5e97a48cae3 100644
--- a/packages/react-router/test/base/src/pages/routing/Details.tsx
+++ b/packages/react-router/test/base/src/pages/routing/Details.tsx
@@ -1,4 +1,3 @@
-import React, { useEffect } from 'react';
import {
IonContent,
IonHeader,
@@ -10,11 +9,10 @@ import {
IonLabel,
IonButton,
} from '@ionic/react';
-import { useParams, useLocation } from 'react-router';
-
-interface DetailsProps {}
+import React, { useEffect } from 'react';
+import { useParams, useLocation } from 'react-router-dom';
-const Details: React.FC = () => {
+const Details: React.FC = () => {
const { id } = useParams<{ id: string }>();
const location = useLocation();
@@ -24,7 +22,7 @@ const Details: React.FC = () => {
return () => console.log('Home Details unmount');
}, []);
- const nextId = parseInt(id, 10) + 1;
+ const nextId = parseInt(id ?? '0', 10) + 1;
return (
diff --git a/packages/react-router/test/base/src/pages/routing/Favorites.tsx b/packages/react-router/test/base/src/pages/routing/Favorites.tsx
index 7a3c80d2e39..7c8b9cb1142 100644
--- a/packages/react-router/test/base/src/pages/routing/Favorites.tsx
+++ b/packages/react-router/test/base/src/pages/routing/Favorites.tsx
@@ -1,4 +1,3 @@
-import React, { useEffect } from 'react';
import {
IonContent,
IonHeader,
@@ -9,10 +8,9 @@ import {
IonMenuButton,
useIonViewWillEnter,
} from '@ionic/react';
+import React, { useEffect } from 'react';
-interface FavoritesProps {}
-
-const Favorites: React.FC = () => {
+const Favorites: React.FC = () => {
useIonViewWillEnter(() => {
console.log('IVWE on Favorites');
});
diff --git a/packages/react-router/test/base/src/pages/routing/Menu.tsx b/packages/react-router/test/base/src/pages/routing/Menu.tsx
index d53d9ad0a70..763a31a56bd 100644
--- a/packages/react-router/test/base/src/pages/routing/Menu.tsx
+++ b/packages/react-router/test/base/src/pages/routing/Menu.tsx
@@ -8,10 +8,8 @@ import {
IonMenu,
IonMenuToggle,
} from '@ionic/react';
-import React from 'react';
import { heartOutline, heartSharp, mailOutline, mailSharp } from 'ionicons/icons';
-
-interface MenuProps {}
+import React from 'react';
interface AppPage {
url: string;
@@ -53,7 +51,7 @@ const appPages: AppPage[] = [
},
];
-const Menu: React.FunctionComponent = () => {
+const Menu: React.FunctionComponent = () => {
return (
diff --git a/packages/react-router/test/base/src/pages/routing/OtherPage.tsx b/packages/react-router/test/base/src/pages/routing/OtherPage.tsx
index 6f1138329fc..a8c6fe67f6f 100644
--- a/packages/react-router/test/base/src/pages/routing/OtherPage.tsx
+++ b/packages/react-router/test/base/src/pages/routing/OtherPage.tsx
@@ -1,4 +1,3 @@
-import React, { useEffect } from 'react';
import {
IonContent,
IonHeader,
@@ -10,10 +9,9 @@ import {
useIonViewWillEnter,
IonButton,
} from '@ionic/react';
+import React, { useEffect } from 'react';
-interface OtherPageProps {}
-
-const OtherPage: React.FC = () => {
+const OtherPage: React.FC = () => {
useIonViewWillEnter(() => {
console.log('IVWE on otherpage');
});
@@ -24,8 +22,6 @@ const OtherPage: React.FC = () => {
}, []);
return (
- //
- // (
@@ -39,8 +35,6 @@ const OtherPage: React.FC = () => {
Go to tab3
- // )}>
- //
);
};
diff --git a/packages/react-router/test/base/src/pages/routing/PropsTest.tsx b/packages/react-router/test/base/src/pages/routing/PropsTest.tsx
index d8cc386a1b8..a0d33c238e6 100644
--- a/packages/react-router/test/base/src/pages/routing/PropsTest.tsx
+++ b/packages/react-router/test/base/src/pages/routing/PropsTest.tsx
@@ -1,4 +1,3 @@
-import React, { useState, useEffect } from 'react';
import {
IonContent,
IonHeader,
@@ -8,11 +7,10 @@ import {
IonButton,
IonRouterOutlet,
} from '@ionic/react';
+import React, { useState, useEffect } from 'react';
import { Route } from 'react-router';
-interface PropsTestProps {}
-
-const PropsTest: React.FC = () => {
+const PropsTest: React.FC = () => {
const [count, setCount] = useState(1);
useEffect(() => {
console.log(count);
@@ -21,7 +19,7 @@ const PropsTest: React.FC = () => {
}
+ element={ }
/>
);
diff --git a/packages/react-router/test/base/src/pages/routing/RedirectRouting.tsx b/packages/react-router/test/base/src/pages/routing/RedirectRouting.tsx
index cd63689b779..559cf6a9d4f 100644
--- a/packages/react-router/test/base/src/pages/routing/RedirectRouting.tsx
+++ b/packages/react-router/test/base/src/pages/routing/RedirectRouting.tsx
@@ -1,5 +1,6 @@
-import React, { useEffect, useContext } from 'react';
import { IonRouterContext } from '@ionic/react';
+import type React from 'react';
+import { useEffect, useContext } from 'react';
const RedirectRouting: React.FC = () => {
const ionRouterContext = useContext(IonRouterContext);
diff --git a/packages/react-router/test/base/src/pages/routing/Routing.tsx b/packages/react-router/test/base/src/pages/routing/Routing.tsx
index 5fcf7c133fc..ef9cf5dc4a6 100644
--- a/packages/react-router/test/base/src/pages/routing/Routing.tsx
+++ b/packages/react-router/test/base/src/pages/routing/Routing.tsx
@@ -1,57 +1,43 @@
-import React from 'react';
import {
IonContent,
IonPage,
IonRouterOutlet,
IonSplitPane,
} from '@ionic/react';
-import Menu from './Menu';
-import { Route, Redirect } from 'react-router';
-import Tabs from './Tabs';
+import React from 'react';
+import { Route, Navigate } from 'react-router';
+
import Favorites from './Favorites';
+import Menu from './Menu';
import OtherPage from './OtherPage';
import PropsTest from './PropsTest';
import RedirectRouting from './RedirectRouting';
+import Tabs from './Tabs';
-interface RoutingProps {}
-
-const Routing: React.FC = () => {
+const Routing: React.FC = () => {
return (
- } />
- {/* */}
- } exact />
-
- {/* {
- return (
-
-
-
- );
- }} /> */}
- {/* {
- return (
-
-
-
- );
- }} /> */}
-
-
- } />
- } />
+ } />
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
(
+ path="*"
+ element={
- Not found
+ Not found in routing.tsx
- )}
+ }
/>
- {/* } /> */}
);
diff --git a/packages/react-router/test/base/src/pages/routing/SettingsDetails.tsx b/packages/react-router/test/base/src/pages/routing/SettingsDetails.tsx
index 0dc5b0be6ff..335aefbf8c3 100644
--- a/packages/react-router/test/base/src/pages/routing/SettingsDetails.tsx
+++ b/packages/react-router/test/base/src/pages/routing/SettingsDetails.tsx
@@ -1,4 +1,3 @@
-import React, { useEffect } from 'react';
import {
IonContent,
IonHeader,
@@ -10,11 +9,10 @@ import {
IonLabel,
IonButton,
} from '@ionic/react';
-import { useParams } from 'react-router';
-
-interface DetailsProps {}
+import React, { useEffect } from 'react';
+import { useParams } from 'react-router-dom';
-const SettingsDetails: React.FC = () => {
+const SettingsDetails: React.FC = () => {
const { id } = useParams<{ id: string }>();
useEffect(() => {
@@ -22,7 +20,7 @@ const SettingsDetails: React.FC = () => {
return () => console.log('Settings Details unmount');
}, []);
- const nextId = parseInt(id, 10) + 1;
+ const nextId = parseInt(id ?? '0', 10) + 1;
// LEFT OFF - why is back button not working for multiple entries?
return (
diff --git a/packages/react-router/test/base/src/pages/routing/Tab1.tsx b/packages/react-router/test/base/src/pages/routing/Tab1.tsx
index bf14ce1456d..d9e204d55ad 100644
--- a/packages/react-router/test/base/src/pages/routing/Tab1.tsx
+++ b/packages/react-router/test/base/src/pages/routing/Tab1.tsx
@@ -1,4 +1,3 @@
-import React, { useEffect, useContext } from 'react';
import {
IonContent,
IonHeader,
@@ -14,6 +13,7 @@ import {
IonButton,
IonRouterContext,
} from '@ionic/react';
+import React, { useEffect, useContext } from 'react';
import './Tab1.css';
import { Link } from 'react-router-dom';
@@ -54,8 +54,8 @@ const Tab1: React.FC = () => {
Details 1
-
- Details 1 & Unmount
+
+ Details 1 (alt)
Details 1 with Query Params
diff --git a/packages/react-router/test/base/src/pages/routing/Tab2.tsx b/packages/react-router/test/base/src/pages/routing/Tab2.tsx
index a6437dec41f..e78d75b1fb5 100644
--- a/packages/react-router/test/base/src/pages/routing/Tab2.tsx
+++ b/packages/react-router/test/base/src/pages/routing/Tab2.tsx
@@ -1,4 +1,3 @@
-import React, { useEffect } from 'react';
import {
IonContent,
IonHeader,
@@ -12,11 +11,12 @@ import {
IonMenuButton,
IonButton,
} from '@ionic/react';
+import React, { useEffect } from 'react';
import './Tab2.css';
-import { useHistory } from 'react-router';
+import { useNavigate } from 'react-router-dom';
const Tab2: React.FC = () => {
- const history = useHistory();
+ const navigate = useNavigate();
useEffect(() => {
console.log('Settings mount');
@@ -51,10 +51,10 @@ const Tab2: React.FC = () => {
{
- history.push('/routing/tabs/settings/details/1', { routerOptions: { unmount: true } });
+ navigate('/routing/tabs/settings/details/1');
}}
>
- Details with Unmount via history.push
+ Details via navigate
diff --git a/packages/react-router/test/base/src/pages/routing/Tab3.tsx b/packages/react-router/test/base/src/pages/routing/Tab3.tsx
index d4564de0a37..d05a3ab9bc6 100644
--- a/packages/react-router/test/base/src/pages/routing/Tab3.tsx
+++ b/packages/react-router/test/base/src/pages/routing/Tab3.tsx
@@ -1,4 +1,3 @@
-import React from 'react';
import {
IonContent,
IonHeader,
@@ -10,6 +9,7 @@ import {
IonMenuButton,
IonButton,
} from '@ionic/react';
+import React from 'react';
import './Tab3.css';
class Tab3 extends React.Component {
diff --git a/packages/react-router/test/base/src/pages/routing/Tabs.tsx b/packages/react-router/test/base/src/pages/routing/Tabs.tsx
index 3ded8d9ee99..ced79fe816f 100644
--- a/packages/react-router/test/base/src/pages/routing/Tabs.tsx
+++ b/packages/react-router/test/base/src/pages/routing/Tabs.tsx
@@ -1,41 +1,40 @@
+import { IonTabs, IonRouterOutlet, IonTabBar, IonTabButton, IonIcon, IonLabel, IonPage, IonContent } from '@ionic/react';
+import { triangle, ellipse, square } from 'ionicons/icons';
import React from 'react';
-import { IonTabs, IonRouterOutlet, IonTabBar, IonTabButton, IonIcon, IonLabel } from '@ionic/react';
-import { Route, Redirect } from 'react-router';
-import Tab1 from './Tab1';
+import { Route, Navigate } from 'react-router';
+
import Details from './Details';
+import SettingsDetails from './SettingsDetails';
+import Tab1 from './Tab1';
import Tab2 from './Tab2';
import Tab3 from './Tab3';
-import { triangle, ellipse, square } from 'ionicons/icons';
-import SettingsDetails from './SettingsDetails';
-interface TabsProps {}
-const Tabs: React.FC = () => {
+
+const Tabs: React.FC = () => {
return (
-
-
- {/* {
- return
- }} exact={true} /> */}
-
-
-
- }
- exact={true}
- />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
}
- exact={true}
+ path="*"
+ element={
+
+
+ Not found in tabs.tsx
+
+
+ }
/>
- {/* } />} /> */}
-
+
Home
diff --git a/packages/react-router/test/base/src/pages/search-params/SearchParams.tsx b/packages/react-router/test/base/src/pages/search-params/SearchParams.tsx
new file mode 100644
index 00000000000..f69a1ca7e7d
--- /dev/null
+++ b/packages/react-router/test/base/src/pages/search-params/SearchParams.tsx
@@ -0,0 +1,50 @@
+import {
+ IonBackButton,
+ IonButton,
+ IonButtons,
+ IonContent,
+ IonHeader,
+ IonPage,
+ IonTitle,
+ IonToolbar,
+} from '@ionic/react';
+import React from 'react';
+import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
+
+const SearchParams: React.FC = () => {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const [searchParams] = useSearchParams();
+ const query = searchParams.get('q') || '';
+
+ return (
+
+
+
+
+
+
+ Search Params
+
+
+
+ {location.pathname + location.search}
+ {query}
+ navigate('/search-params?q=test')}>
+ Add Search Param
+
+ navigate('/search-params?q=changed')}>
+ Change Search Param
+
+ navigate('/search-params')}>
+ Remove Search Param
+
+
+ Go Home
+
+
+
+ );
+};
+
+export default SearchParams;
diff --git a/packages/react-router/test/base/src/pages/stale-view-cleanup/StaleViewCleanup.tsx b/packages/react-router/test/base/src/pages/stale-view-cleanup/StaleViewCleanup.tsx
new file mode 100644
index 00000000000..c391ff0c72b
--- /dev/null
+++ b/packages/react-router/test/base/src/pages/stale-view-cleanup/StaleViewCleanup.tsx
@@ -0,0 +1,39 @@
+import { IonRouterOutlet } from '@ionic/react';
+import React from 'react';
+import { Route, useNavigate } from 'react-router-dom';
+
+/**
+ * A component without IonPage wrapper.
+ * When navigated away from, this should be cleaned up from the DOM.
+ */
+const NonIonPageSource: React.FC = () => {
+ const navigate = useNavigate();
+
+ return (
+
+
Non-IonPage Source
+ navigate('/stale-view-cleanup/target')}>
+ Go to Target
+
+
+ );
+};
+
+const NonIonPageTarget: React.FC = () => {
+ return (
+
+ );
+};
+
+const StaleViewCleanup: React.FC = () => {
+ return (
+
+ } />
+ } />
+
+ );
+};
+
+export default StaleViewCleanup;
diff --git a/packages/react-router/test/base/src/pages/swipe-to-go-back/SwipToGoBack.tsx b/packages/react-router/test/base/src/pages/swipe-to-go-back/SwipeToGoBack.tsx
similarity index 55%
rename from packages/react-router/test/base/src/pages/swipe-to-go-back/SwipToGoBack.tsx
rename to packages/react-router/test/base/src/pages/swipe-to-go-back/SwipeToGoBack.tsx
index 2b88551b85e..920533dee42 100644
--- a/packages/react-router/test/base/src/pages/swipe-to-go-back/SwipToGoBack.tsx
+++ b/packages/react-router/test/base/src/pages/swipe-to-go-back/SwipeToGoBack.tsx
@@ -1,4 +1,3 @@
-import React from 'react';
import {
IonRouterOutlet,
IonPage,
@@ -10,15 +9,15 @@ import {
IonButtons,
IonBackButton,
} from '@ionic/react';
+import React from 'react';
import { Route } from 'react-router';
-interface SwipeToGoBackProps {}
-
-export const SwipeToGoBack: React.FC = () => {
+export const SwipeToGoBack: React.FC = () => {
return (
-
-
+ } />
+ } />
+ } />
);
};
@@ -43,7 +42,7 @@ const Details: React.FC = () => {
-
+
Details
@@ -51,6 +50,25 @@ const Details: React.FC = () => {
Details
+ Details 2
+
+
+ );
+};
+
+const Details2: React.FC = () => {
+ return (
+
+
+
+
+
+
+ Details 2
+
+
+
+ Details 2
);
diff --git a/packages/react-router/test/base/src/pages/tab-context/TabContext.tsx b/packages/react-router/test/base/src/pages/tab-context/TabContext.tsx
index b89cface216..e200f1e8112 100644
--- a/packages/react-router/test/base/src/pages/tab-context/TabContext.tsx
+++ b/packages/react-router/test/base/src/pages/tab-context/TabContext.tsx
@@ -1,4 +1,3 @@
-import React, { useContext } from 'react';
import {
IonTabs,
IonRouterOutlet,
@@ -16,21 +15,20 @@ import {
IonTabsContext,
IonButton,
} from '@ionic/react';
-import { Route, Redirect } from 'react-router';
import { triangle, square } from 'ionicons/icons';
+import React, { useContext } from 'react';
+import { Route, Navigate } from 'react-router';
-interface TabsContextProps {}
-
-const TabsContext: React.FC = () => {
+const TabsContext: React.FC = () => {
return (
-
-
-
+ } />
+ } />
+ } />
-
+
Tab1
diff --git a/packages/react-router/test/base/src/pages/tab-history-isolation/TabHistoryIsolation.tsx b/packages/react-router/test/base/src/pages/tab-history-isolation/TabHistoryIsolation.tsx
new file mode 100644
index 00000000000..a3ea324c85c
--- /dev/null
+++ b/packages/react-router/test/base/src/pages/tab-history-isolation/TabHistoryIsolation.tsx
@@ -0,0 +1,162 @@
+import {
+ IonTabs,
+ IonRouterOutlet,
+ IonTabBar,
+ IonTabButton,
+ IonIcon,
+ IonLabel,
+ IonPage,
+ IonHeader,
+ IonToolbar,
+ IonButtons,
+ IonBackButton,
+ IonTitle,
+ IonContent,
+ IonButton,
+} from '@ionic/react';
+import { triangle, square, ellipse } from 'ionicons/icons';
+import React from 'react';
+import { Route, Navigate } from 'react-router';
+
+const TabHistoryIsolation: React.FC = () => {
+ return (
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+ Tab A
+
+
+
+ Tab B
+
+
+
+ Tab C
+
+
+
+ );
+};
+
+const TabA = () => {
+ return (
+
+
+
+
+
+
+ Tab A
+
+
+
+ Tab A
+
+ Go to A Details
+
+
+
+ );
+};
+
+const TabB = () => {
+ return (
+
+
+
+
+
+
+ Tab B
+
+
+
+ Tab B
+
+ Go to B Details
+
+
+
+ );
+};
+
+const TabC = () => {
+ return (
+
+
+
+
+
+
+ Tab C
+
+
+
+ Tab C
+
+ Go to C Details
+
+
+
+ );
+};
+
+const TabADetails = () => {
+ return (
+
+
+
+
+
+
+ Tab A Details
+
+
+ Tab A Details
+
+ );
+};
+
+const TabBDetails = () => {
+ return (
+
+
+
+
+
+
+ Tab B Details
+
+
+ Tab B Details
+
+ );
+};
+
+const TabCDetails = () => {
+ return (
+
+
+
+
+
+
+ Tab C Details
+
+
+ Tab C Details
+
+ );
+};
+
+export default TabHistoryIsolation;
diff --git a/packages/react-router/test/base/src/pages/tab-lifecycle/TabLifecycle.tsx b/packages/react-router/test/base/src/pages/tab-lifecycle/TabLifecycle.tsx
new file mode 100644
index 00000000000..4223abffc85
--- /dev/null
+++ b/packages/react-router/test/base/src/pages/tab-lifecycle/TabLifecycle.tsx
@@ -0,0 +1,94 @@
+import {
+ IonTabs,
+ IonRouterOutlet,
+ IonTabBar,
+ IonTabButton,
+ IonIcon,
+ IonLabel,
+ IonPage,
+ IonHeader,
+ IonToolbar,
+ IonTitle,
+ IonContent,
+ IonButton,
+ useIonViewDidEnter,
+ useIonViewDidLeave,
+ useIonViewWillEnter,
+ useIonViewWillLeave,
+} from '@ionic/react';
+import { triangle, square } from 'ionicons/icons';
+import React from 'react';
+import { Route, Navigate } from 'react-router';
+
+const pushEvent = (event: string) => {
+ (window as any).lifecycleEvents = (window as any).lifecycleEvents || [];
+ (window as any).lifecycleEvents.push(event);
+};
+
+const TabLifecycle: React.FC = () => {
+ return (
+
+
+ } />
+ } />
+ } />
+
+
+
+
+ Home
+
+
+
+ Settings
+
+
+
+ );
+};
+
+const HomeTab: React.FC = () => {
+ useIonViewWillEnter(() => pushEvent('home:ionViewWillEnter'));
+ useIonViewDidEnter(() => pushEvent('home:ionViewDidEnter'));
+ useIonViewWillLeave(() => pushEvent('home:ionViewWillLeave'));
+ useIonViewDidLeave(() => pushEvent('home:ionViewDidLeave'));
+
+ return (
+
+
+
+ Home Tab
+
+
+
+
+ Go Outside Tabs
+
+
+
+ );
+};
+
+const SettingsTab: React.FC = () => {
+ useIonViewWillEnter(() => pushEvent('settings:ionViewWillEnter'));
+ useIonViewDidEnter(() => pushEvent('settings:ionViewDidEnter'));
+ useIonViewWillLeave(() => pushEvent('settings:ionViewWillLeave'));
+ useIonViewDidLeave(() => pushEvent('settings:ionViewDidLeave'));
+
+ return (
+
+
+
+ Settings Tab
+
+
+
+
+ Go Outside Tabs
+
+
+
+ );
+};
+
+export default TabLifecycle;
diff --git a/packages/react-router/test/base/src/pages/tab-lifecycle/TabLifecycleOutside.tsx b/packages/react-router/test/base/src/pages/tab-lifecycle/TabLifecycleOutside.tsx
new file mode 100644
index 00000000000..52a602f828d
--- /dev/null
+++ b/packages/react-router/test/base/src/pages/tab-lifecycle/TabLifecycleOutside.tsx
@@ -0,0 +1,33 @@
+import {
+ IonPage,
+ IonHeader,
+ IonToolbar,
+ IonTitle,
+ IonContent,
+ IonButton,
+ IonButtons,
+ IonBackButton,
+} from '@ionic/react';
+import React from 'react';
+
+const TabLifecycleOutside: React.FC = () => {
+ return (
+
+
+
+
+
+
+ Outside Tabs
+
+
+
+
+ Back to Tabs
+
+
+
+ );
+};
+
+export default TabLifecycleOutside;
diff --git a/packages/react-router/test/base/src/pages/tab-search-params/TabSearchParams.tsx b/packages/react-router/test/base/src/pages/tab-search-params/TabSearchParams.tsx
new file mode 100644
index 00000000000..152ae3353c7
--- /dev/null
+++ b/packages/react-router/test/base/src/pages/tab-search-params/TabSearchParams.tsx
@@ -0,0 +1,79 @@
+import {
+ IonTabs,
+ IonRouterOutlet,
+ IonTabBar,
+ IonTabButton,
+ IonIcon,
+ IonLabel,
+ IonPage,
+ IonHeader,
+ IonToolbar,
+ IonTitle,
+ IonContent,
+} from '@ionic/react';
+import { triangle, square } from 'ionicons/icons';
+import React from 'react';
+import { Route, Navigate } from 'react-router';
+import { useSearchParams } from 'react-router-dom';
+
+const TabSearchParams: React.FC = () => {
+ return (
+
+
+ } />
+ } />
+ } />
+
+
+
+
+ Tab1
+
+
+
+ Tab2
+
+
+
+ );
+};
+
+const Tab1 = () => {
+ const [searchParams] = useSearchParams();
+ const foo = searchParams.get('foo') || '';
+
+ return (
+
+
+
+ Tab 1
+
+
+
+ {foo}
+ {searchParams.toString()}
+
+
+ );
+};
+
+const Tab2 = () => {
+ const [searchParams] = useSearchParams();
+ const baz = searchParams.get('baz') || '';
+
+ return (
+
+
+
+ Tab 2
+
+
+
+ {baz}
+ {searchParams.toString()}
+
+
+ );
+};
+
+export default TabSearchParams;
diff --git a/packages/react-router/test/base/src/pages/tabs/Tabs.tsx b/packages/react-router/test/base/src/pages/tabs/Tabs.tsx
index d27907821d9..3f887829be9 100644
--- a/packages/react-router/test/base/src/pages/tabs/Tabs.tsx
+++ b/packages/react-router/test/base/src/pages/tabs/Tabs.tsx
@@ -1,4 +1,3 @@
-import React from 'react';
import {
IonTabs,
IonRouterOutlet,
@@ -15,20 +14,19 @@ import {
IonContent,
IonButton,
} from '@ionic/react';
-import { Route, Redirect } from 'react-router';
import { triangle, square } from 'ionicons/icons';
+import React from 'react';
+import { Route, Navigate } from 'react-router';
-interface TabsProps {}
-
-const Tabs: React.FC = () => {
+const Tabs: React.FC = () => {
return (
-
-
-
-
-
+ } />
+ } />
+ } />
+ } />
+ } />
diff --git a/packages/react-router/test/base/src/pages/tabs/TabsSecondary.tsx b/packages/react-router/test/base/src/pages/tabs/TabsSecondary.tsx
index ab4103340d0..0df9a12460f 100644
--- a/packages/react-router/test/base/src/pages/tabs/TabsSecondary.tsx
+++ b/packages/react-router/test/base/src/pages/tabs/TabsSecondary.tsx
@@ -1,4 +1,3 @@
-import React from 'react';
import {
IonTabs,
IonRouterOutlet,
@@ -14,18 +13,17 @@ import {
IonTitle,
IonContent,
} from '@ionic/react';
-import { Route, Redirect } from 'react-router';
import { triangle, square } from 'ionicons/icons';
+import React from 'react';
+import { Route, Navigate } from 'react-router';
-interface TabsSecondaryProps {}
-
-const TabsSecondary: React.FC = () => {
+const TabsSecondary: React.FC = () => {
return (
-
-
-
+ } />
+ } />
+ } />
diff --git a/packages/react-router/test/base/src/pages/tail-slice-ambiguity/TailSliceAmbiguity.tsx b/packages/react-router/test/base/src/pages/tail-slice-ambiguity/TailSliceAmbiguity.tsx
new file mode 100644
index 00000000000..25d80307bcc
--- /dev/null
+++ b/packages/react-router/test/base/src/pages/tail-slice-ambiguity/TailSliceAmbiguity.tsx
@@ -0,0 +1,97 @@
+import {
+ IonButton,
+ IonContent,
+ IonHeader,
+ IonLabel,
+ IonPage,
+ IonRouterOutlet,
+ IonTitle,
+ IonToolbar,
+} from '@ionic/react';
+import React from 'react';
+import { Route, useLocation, useParams } from 'react-router-dom';
+
+/**
+ * Test page for tail-slice ambiguity in derivePathnameToMatch.
+ *
+ * Route structure:
+ * /tail-slice-ambiguity/*
+ * βββ
+ * βββ index β ListPage
+ * βββ details/:id β DetailsPage
+ * βββ * β CatchAllPage
+ *
+ * Bug scenario:
+ * 1. Navigate to /tail-slice-ambiguity/details/42 β creates details/:id view
+ * 2. Navigate to /tail-slice-ambiguity/extra/details/99
+ * 3. derivePathnameToMatch tail-slices the last 2 segments ["details", "99"]
+ * and matchComponent falsely matches details/:id
+ * 4. The catch-all * view is incorrectly deactivated because hasSpecificMatch
+ * finds the false positive from details/:id
+ * 5. User sees nothing instead of the catch-all page
+ */
+const TailSliceAmbiguity: React.FC = () => (
+
+ } />
+ } />
+ } />
+
+);
+
+const ListPage: React.FC = () => (
+
+
+
+ List
+
+
+
+
+ Details 42
+
+
+ Ambiguous Path
+
+
+
+);
+
+const DetailsPage: React.FC = () => {
+ const { id } = useParams<{ id: string }>();
+ return (
+
+
+
+ Details
+
+
+
+ Details ID: {id}
+
+ Back to List
+
+
+
+ );
+};
+
+const CatchAllPage: React.FC = () => {
+ const location = useLocation();
+ return (
+
+
+
+ Catch All
+
+
+
+ Catch-all: {location.pathname}
+
+ Back to List
+
+
+
+ );
+};
+
+export default TailSliceAmbiguity;
diff --git a/packages/react-router/test/base/src/pages/wildcard-no-heuristic/WildcardNoHeuristic.tsx b/packages/react-router/test/base/src/pages/wildcard-no-heuristic/WildcardNoHeuristic.tsx
new file mode 100644
index 00000000000..777b66fe113
--- /dev/null
+++ b/packages/react-router/test/base/src/pages/wildcard-no-heuristic/WildcardNoHeuristic.tsx
@@ -0,0 +1,110 @@
+import {
+ IonContent,
+ IonHeader,
+ IonPage,
+ IonTitle,
+ IonToolbar,
+ IonRouterOutlet,
+ IonButton,
+} from '@ionic/react';
+import React from 'react';
+import { Route, useNavigate } from 'react-router-dom';
+
+/**
+ * Test page that validates wildcard routing works without relying on
+ * string-similarity heuristics. The route names are intentionally chosen
+ * to be very similar to navigation targets so that any prefix/similarity
+ * heuristic would produce false positives.
+ *
+ * Routes: "page", "ab", and "*"
+ * Navigation targets that should hit wildcard:
+ * - "page2" (shares 4-char prefix "page" with route "page")
+ * - "abc" (shares 2-char prefix "ab" with route "ab")
+ * - "pager" (shares 4-char prefix "page" with route "page")
+ */
+
+const SpecificPageA: React.FC = () => (
+
+
+
+ Page
+
+
+
+ Page Route
+
+
+);
+
+const SpecificPageB: React.FC = () => (
+
+
+
+ AB
+
+
+
+ AB Route
+
+
+);
+
+const CatchAllPage: React.FC = () => (
+
+
+
+ Catch All
+
+
+
+ Wildcard Catch-All Page
+
+
+);
+
+const HeuristicHome: React.FC = () => {
+ const navigate = useNavigate();
+
+ return (
+
+
+
+ Wildcard No Heuristic Test
+
+
+
+ navigate('page')}>
+ Go to Page (specific)
+
+ navigate('ab')}>
+ Go to AB (specific)
+
+ navigate('page2')}>
+ Go to Page2 (wildcard)
+
+ navigate('abc')}>
+ Go to ABC (wildcard)
+
+
+ navigate('unknown')}>
+ Go to Unknown (wildcard)
+
+
+
+ );
+};
+
+const WildcardNoHeuristic: React.FC = () => {
+ return (
+
+ } />
+ } />
+ } />
+ } />
+
+ );
+};
+
+export default WildcardNoHeuristic;
diff --git a/packages/react-router/test/base/src/react-app-env.d.ts b/packages/react-router/test/base/src/react-app-env.d.ts
index 1f35a7e91aa..9a8fcf86803 100644
--- a/packages/react-router/test/base/src/react-app-env.d.ts
+++ b/packages/react-router/test/base/src/react-app-env.d.ts
@@ -40,7 +40,7 @@ declare module '*.webp' {
}
declare module '*.svg' {
- import * as React from 'react';
+ import type * as React from 'react';
export const ReactComponent: React.FunctionComponent<
React.SVGProps & { title?: string }
diff --git a/packages/react-router/test/base/src/utils/LocationHistory.ts b/packages/react-router/test/base/src/utils/LocationHistory.ts
deleted file mode 100644
index ec5eee44ef6..00000000000
--- a/packages/react-router/test/base/src/utils/LocationHistory.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { Location as HistoryLocation } from 'history';
-
-const RESTRICT_SIZE = 25;
-
-export class LocationHistory {
- private locationHistory: HistoryLocation[] = [];
-
- add(location: HistoryLocation) {
- this.locationHistory.push(location);
- if (this.locationHistory.length > RESTRICT_SIZE) {
- this.locationHistory.splice(0, 10);
- }
- }
-
- pop() {
- this.locationHistory.pop();
- }
-
- replace(location: HistoryLocation) {
- this.locationHistory.pop();
- this.locationHistory.push(location);
- }
-
- clear() {
- this.locationHistory = [];
- }
-
- findLastLocationByUrl(url: string) {
- for (let i = this.locationHistory.length - 1; i >= 0; i--) {
- const location = this.locationHistory[i];
- if (location.pathname.toLocaleLowerCase() === url.toLocaleLowerCase()) {
- return location;
- }
- }
- return undefined;
- }
-
- previous() {
- return this.locationHistory[this.locationHistory.length - 2];
- }
-
- current() {
- return this.locationHistory[this.locationHistory.length - 1];
- }
-}
diff --git a/packages/react-router/test/base/src/utils/generateId.ts b/packages/react-router/test/base/src/utils/generateId.ts
index 06bb0a39a6f..6a37c44c6ed 100644
--- a/packages/react-router/test/base/src/utils/generateId.ts
+++ b/packages/react-router/test/base/src/utils/generateId.ts
@@ -1,7 +1,7 @@
const ids: { [key: string]: number } = { main: 1 };
-export const generateId = (type: string = 'main') => {
- let id = (ids[type] ?? 1) + 1;
+export const generateId = (type = 'main') => {
+ const id = (ids[type] ?? 1) + 1;
ids[type] = id;
return id.toString();
};
diff --git a/packages/react-router/test/base/tests/e2e/playwright/overlays.spec.ts b/packages/react-router/test/base/tests/e2e/playwright/overlays.spec.ts
new file mode 100644
index 00000000000..e5fb6c7521c
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/playwright/overlays.spec.ts
@@ -0,0 +1,42 @@
+import { test, expect } from '@playwright/test';
+import { withTestingMode } from './utils/test-utils';
+
+test.describe('Overlays', () => {
+ test('should remove the overlay when going back to the previous route', async ({ page }) => {
+ // Requires navigation history to perform a pop
+ await page.goto(withTestingMode('/'));
+ await page.goto(withTestingMode('/overlays'));
+
+ await page.locator('#openModal').click();
+
+ await expect(page.locator('ion-modal')).toBeAttached();
+
+ await page.locator('#goBack').click();
+
+ await expect(page.locator('ion-modal')).not.toBeAttached();
+ });
+
+ test('should remove the overlay when pushing to a new route', async ({ page }) => {
+ await page.goto(withTestingMode('/overlays'));
+
+ await page.locator('#openModal').click();
+
+ await expect(page.locator('ion-modal')).toBeAttached();
+
+ await page.locator('#push').click();
+
+ await expect(page.locator('ion-modal')).not.toBeAttached();
+ });
+
+ test('should remove the overlay when replacing the route', async ({ page }) => {
+ await page.goto(withTestingMode('/overlays'));
+
+ await page.locator('#openModal').click();
+
+ await expect(page.locator('ion-modal')).toBeAttached();
+
+ await page.locator('#replace').click();
+
+ await expect(page.locator('ion-modal')).not.toBeAttached();
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/playwright/param-swipe-back.spec.ts b/packages/react-router/test/base/tests/e2e/playwright/param-swipe-back.spec.ts
new file mode 100644
index 00000000000..bf0c9cec7a8
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/playwright/param-swipe-back.spec.ts
@@ -0,0 +1,129 @@
+import { test, expect } from '@playwright/test';
+import { ionPageVisible, ionPageHidden, ionPageDoesNotExist, ionGoBack, withTestingMode } from './utils/test-utils';
+import { ionSwipeToGoBack } from './utils/drag-utils';
+
+const IOS_MODE = 'ionic:mode=ios';
+
+/**
+ * Tests for issue #27900:
+ * Swipe back gesture breaks with Ionic React Router on certain parameterized route changes.
+ *
+ * Two reproductions from the issue:
+ * A) /foo/alex -> /baz -> /foo/sean -> swipe back (ok) -> swipe back (broken)
+ * B) /one -> replace /two -> /two/details -> swipe back (broken)
+ */
+test.describe('Param Back Navigation (#27900)', () => {
+
+ // --- Reproduction A ---
+ // Routes: /user/:name and /middle
+ // Navigate /user/alex -> /middle -> /user/sean -> back -> back
+ // Second back should go to /user/alex
+
+ test('Repro A: browser back through param routes with non-param in between', async ({ page }) => {
+ await page.goto(withTestingMode('/param-swipe-back'));
+ await ionPageVisible(page, 'psb-start');
+
+ // 1. Navigate /user/alex
+ await page.locator('[data-pageid="psb-start"] #go-to-alex').click();
+ await page.waitForTimeout(250);
+ await ionPageVisible(page, 'psb-user-alex');
+
+ // 2. Navigate /middle
+ await page.locator('[data-pageid="psb-user-alex"] #go-to-middle').click();
+ await page.waitForTimeout(250);
+ await ionPageVisible(page, 'psb-middle');
+
+ // 3. Navigate /user/sean
+ await page.locator('[data-pageid="psb-middle"] #go-to-sean').click();
+ await page.waitForTimeout(250);
+ await ionPageVisible(page, 'psb-user-sean');
+
+ // 4. Back β /middle
+ await ionGoBack(page, '/param-swipe-back/middle');
+ await ionPageVisible(page, 'psb-middle');
+ await ionPageDoesNotExist(page, 'psb-user-sean');
+
+ // 5. Back β /user/alex (this is the step that broke in #27900)
+ await ionGoBack(page, '/param-swipe-back/user/alex');
+ await ionPageVisible(page, 'psb-user-alex');
+ await ionPageDoesNotExist(page, 'psb-middle');
+ });
+
+ test('Repro A: swipe back through param routes with non-param in between', async ({ page }) => {
+ await page.goto(`/param-swipe-back?${IOS_MODE}`);
+ await ionPageVisible(page, 'psb-start');
+
+ await page.locator('[data-pageid="psb-start"] #go-to-alex').click();
+ await page.waitForTimeout(250);
+ await ionPageVisible(page, 'psb-user-alex');
+ await ionPageHidden(page, 'psb-start');
+
+ await page.locator('[data-pageid="psb-user-alex"] #go-to-middle').click();
+ await page.waitForTimeout(250);
+ await ionPageVisible(page, 'psb-middle');
+ await ionPageHidden(page, 'psb-user-alex');
+
+ await page.locator('[data-pageid="psb-middle"] #go-to-sean').click();
+ await page.waitForTimeout(250);
+ await ionPageVisible(page, 'psb-user-sean');
+ await ionPageHidden(page, 'psb-middle');
+
+ // 4. Swipe back β /middle
+ await ionSwipeToGoBack(page, true, 'ion-router-outlet#param-swipe-back');
+ await ionPageVisible(page, 'psb-middle');
+ await ionPageDoesNotExist(page, 'psb-user-sean');
+
+ // 5. Swipe back β /user/alex
+ await ionSwipeToGoBack(page, true, 'ion-router-outlet#param-swipe-back');
+ await ionPageVisible(page, 'psb-user-alex');
+ await ionPageDoesNotExist(page, 'psb-middle');
+ });
+
+ // --- Reproduction B ---
+ // Routes: /item/:name and /item/:name/details
+ // Navigate /item/one -> replace with /item/two -> /item/two/details -> back
+ // Back should return to /item/two
+
+ test('Repro B: browser back after replace + parameterized details route', async ({ page }) => {
+ // 1. Go to /item/one
+ await page.goto(withTestingMode('/param-swipe-back-b/item/one'));
+ await ionPageVisible(page, 'psb-item-one');
+
+ // 2. Replace route with /item/two
+ await page.locator('#replace-with-two').click();
+ await page.waitForTimeout(250);
+ await ionPageVisible(page, 'psb-item-two');
+
+ // 3. Go to /item/two/details
+ await page.locator('[data-pageid="psb-item-two"] #go-to-details').click();
+ await page.waitForTimeout(250);
+ await ionPageVisible(page, 'psb-item-two-details');
+
+ // 4. Back β should return to /item/two
+ await ionGoBack(page, '/param-swipe-back-b/item/two');
+ await ionPageVisible(page, 'psb-item-two');
+ await ionPageDoesNotExist(page, 'psb-item-two-details');
+ });
+
+ test('Repro B: swipe back after replace + parameterized details route', async ({ page }) => {
+ // 1. Go to /item/one
+ await page.goto(`/param-swipe-back-b/item/one?${IOS_MODE}`);
+ await ionPageVisible(page, 'psb-item-one');
+
+ // 2. Replace route with /item/two
+ await page.locator('#replace-with-two').click();
+ await page.waitForTimeout(250);
+ await ionPageVisible(page, 'psb-item-two');
+
+ // 3. Go to /item/two/details
+ await page.locator('[data-pageid="psb-item-two"] #go-to-details').click();
+ await page.waitForTimeout(250);
+ await ionPageVisible(page, 'psb-item-two-details');
+ await ionPageHidden(page, 'psb-item-two');
+
+ // 4. Swipe back β should return to /item/two
+ await ionSwipeToGoBack(page, true, 'ion-router-outlet#param-swipe-back-b');
+ await ionPageVisible(page, 'psb-item-two');
+ await ionPageDoesNotExist(page, 'psb-item-two-details');
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/playwright/replace-params.spec.ts b/packages/react-router/test/base/tests/e2e/playwright/replace-params.spec.ts
new file mode 100644
index 00000000000..009efe21ae5
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/playwright/replace-params.spec.ts
@@ -0,0 +1,66 @@
+import { test, expect } from '@playwright/test';
+import { ionPageVisible, ionPageDoesNotExist, withTestingMode } from './utils/test-utils';
+
+test.describe('Replace Params', () => {
+ test('replaced views with params should be unmounted and fresh on revisit', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://github.com/ionic-team/ionic-framework/issues/25640',
+ });
+
+ await page.goto(withTestingMode('/replace-params/step1'));
+ await ionPageVisible(page, 'replace-params-step1');
+
+ await page.locator('#go-step2-first').click();
+ await ionPageVisible(page, 'replace-params-step2');
+ await expect(page.locator('[data-testid="step2-param"]')).toHaveText('first');
+
+ await page.locator('#go-step3').click();
+ await ionPageVisible(page, 'replace-params-step3');
+ await expect(page.locator('[data-testid="step3-param"]')).toHaveText('first');
+
+ // Record mount ID to verify we get a fresh component later
+ const firstMountId = await page.locator('[data-testid="step3-mount-id"]').textContent();
+
+ await page.locator('#go-step4').click();
+ await ionPageVisible(page, 'replace-params-step4');
+
+ await page.locator('#go-to-step1').click();
+ await ionPageVisible(page, 'replace-params-step1');
+
+ await page.locator('#go-step2-second').click();
+ await ionPageVisible(page, 'replace-params-step2');
+ await expect(page.locator('[data-testid="step2-param"]')).toHaveText('second');
+
+ // Wait for previously replaced views to be fully removed from the DOM
+ await ionPageDoesNotExist(page, 'replace-params-step4');
+ await ionPageDoesNotExist(page, 'replace-params-step3');
+
+ // Critical assertion: step3 should show 'second' params, not stale 'first'
+ await page.locator('#go-step3').click();
+ await ionPageVisible(page, 'replace-params-step3');
+ await expect(page.locator('[data-testid="step3-param"]')).toHaveText('second');
+
+ // Verify it's a fresh component instance, not the cached one
+ const secondMountId = await page.locator('[data-testid="step3-mount-id"]').textContent();
+ expect(secondMountId).not.toBe(firstMountId);
+ });
+
+ test('simple replace chain should clean up replaced views', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://github.com/ionic-team/ionic-framework/issues/25640',
+ });
+
+ await page.goto(withTestingMode('/replace-params/step1'));
+ await ionPageVisible(page, 'replace-params-step1');
+
+ await page.locator('#go-step2-first').click();
+ await ionPageVisible(page, 'replace-params-step2');
+ await ionPageDoesNotExist(page, 'replace-params-step1');
+
+ await page.locator('#go-step3').click();
+ await ionPageVisible(page, 'replace-params-step3');
+ await ionPageDoesNotExist(page, 'replace-params-step2');
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/playwright/routing.spec.ts b/packages/react-router/test/base/tests/e2e/playwright/routing.spec.ts
new file mode 100644
index 00000000000..c02431d32b7
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/playwright/routing.spec.ts
@@ -0,0 +1,391 @@
+import { test, expect } from '@playwright/test';
+import {
+ ionPageVisible, ionPageHidden, ionPageDoesNotExist,
+ ionNav, ionBackClick, ionTabClick, ionMenuClick, ionMenuNav,
+ ionGoBack, ionGoForward, withTestingMode
+} from './utils/test-utils';
+
+test.describe('Routing Tests', () => {
+
+ test('/ > Details 1, the details screen should appear', async ({ page }) => {
+ await page.goto(withTestingMode('/routing'));
+ await ionPageVisible(page, 'home-page');
+ await ionNav(page, 'ion-item', 'Details 1');
+ await expect(page.locator('[data-pageid="home-details-page-1"] [data-testid="details-label"]')).toContainText('Details 1');
+ });
+
+ test('/ > Details 1 > Back, should go back to home', async ({ page }) => {
+ await page.goto(withTestingMode('/routing'));
+ await ionNav(page, 'ion-item', 'Details 1');
+ await expect(page.locator('[data-pageid="home-details-page-1"] [data-testid="details-label"]')).toContainText('Details 1');
+ await ionBackClick(page, 'home-details-page-1');
+ await expect(page.locator('[data-pageid="home-page"] ion-title')).toContainText('Home');
+ });
+
+ test('/ > Details 1 > Settings Tab > Home Tab, should go back to details 1 on home tab', async ({ page }) => {
+ await page.goto(withTestingMode('/routing'));
+ await ionNav(page, 'ion-item', 'Details 1');
+ await ionPageVisible(page, 'home-details-page-1');
+ await ionTabClick(page, 'Settings');
+ await ionPageVisible(page, 'settings-page');
+ await ionTabClick(page, 'Home');
+ await ionPageVisible(page, 'home-details-page-1');
+ });
+
+ test('/ > Details 1 > Settings Tab > Home Tab > Home Tab, should go back to home', async ({ page }) => {
+ await page.goto(withTestingMode('/routing'));
+ await ionNav(page, 'ion-item', 'Details 1');
+ await expect(page.locator('[data-pageid="home-details-page-1"] [data-testid="details-label"]')).toContainText('Details 1');
+ await ionTabClick(page, 'Settings');
+ await ionPageVisible(page, 'settings-page');
+ await ionTabClick(page, 'Home');
+ await ionPageVisible(page, 'home-details-page-1');
+ await ionTabClick(page, 'Home');
+ await ionPageVisible(page, 'home-page');
+ });
+
+ test('When going directly to /tabs, it should load home page', async ({ page }) => {
+ await page.goto(withTestingMode('/routing/tabs'));
+ await ionPageVisible(page, 'home-page');
+ });
+
+ test('When going directly to /tabs/home, it should load home page', async ({ page }) => {
+ await page.goto(withTestingMode('/routing/tabs/home'));
+ await ionPageVisible(page, 'home-page');
+ });
+
+ test('When going directly to /tabs/settings, it should load settings page', async ({ page }) => {
+ await page.goto(withTestingMode('/routing/tabs/settings'));
+ await ionPageVisible(page, 'settings-page');
+ });
+
+ test('Home Details 1 > Settings Tab > Home Tab, details 1 page should still be active', async ({ page }) => {
+ await page.goto(withTestingMode('/routing/tabs/home/details/1'));
+ await ionTabClick(page, 'Settings');
+ await ionPageVisible(page, 'settings-page');
+ await ionTabClick(page, 'Home');
+ await ionPageVisible(page, 'home-details-page-1');
+ });
+
+ test('/ > Home Details 1+2+3 > Settings Tab > Setting Details 1+2+3 > Home Tab > Back * 3 > Settings Tab > Back * 3, should be at settings page', async ({ page }) => {
+ // This was a bug when loading the root of the app and not going directly to the home tab
+ await page.goto(withTestingMode('/routing/'));
+ await ionNav(page, 'ion-item', 'Details 1');
+ await ionNav(page, 'ion-button', 'Go to Details 2');
+ await ionNav(page, 'ion-button', 'Go to Details 3');
+ await expect(page.locator('[data-pageid="home-details-page-3"] [data-testid="details-label"]')).toContainText('Details 3');
+ await ionTabClick(page, 'Settings');
+ await ionNav(page, 'ion-item', 'Settings Details 1');
+ await ionNav(page, 'ion-button', 'Go to Settings Details 2');
+ await ionNav(page, 'ion-button', 'Go to Settings Details 3');
+ await expect(page.locator('[data-pageid="settings-details-page-3"] [data-testid="details-label"]')).toContainText('Details 3');
+ await ionTabClick(page, 'Home');
+ await expect(page.locator('[data-pageid="home-details-page-3"] [data-testid="details-label"]')).toContainText('Details 3');
+ await ionBackClick(page, 'home-details-page-3');
+ await expect(page.locator('[data-pageid="home-details-page-2"] [data-testid="details-label"]')).toContainText('Details 2');
+ await ionBackClick(page, 'home-details-page-2');
+ await expect(page.locator('[data-pageid="home-details-page-1"] [data-testid="details-label"]')).toContainText('Details 1');
+ await ionBackClick(page, 'home-details-page-1');
+ await ionPageVisible(page, 'home-page');
+ });
+
+ test('/ > Details 1 with Query Param > Details 2 > Back, Query param should show on screen', async ({ page }) => {
+ await page.goto(withTestingMode('/routing/'));
+ await ionNav(page, 'ion-item', 'Details 1 with Query Params');
+ await expect(page.locator('[data-pageid="home-details-page-1"] [data-testid="query-label"]')).toContainText('Query Params:');
+ await ionNav(page, 'ion-button', 'Go to Details 2');
+ await expect(page.locator('[data-pageid="home-details-page-2"] [data-testid="details-label"]')).toContainText('Details 2');
+ await ionBackClick(page, 'home-details-page-2');
+ await expect(page.locator('[data-pageid="home-details-page-1"] [data-testid="query-label"]')).toContainText('Query Params:');
+ });
+
+ test('/ > Details 1 with Query Param > Settings Tab > Home Tab > Query param should show on screen', async ({ page }) => {
+ await page.goto(withTestingMode('/routing/'));
+ await ionNav(page, 'ion-item', 'Details 1 with Query Params');
+ await expect(page.locator('[data-pageid="home-details-page-1"] [data-testid="query-label"]')).toContainText('Query Params:');
+ await ionTabClick(page, 'Settings');
+ await ionPageVisible(page, 'settings-page');
+ await ionTabClick(page, 'Home');
+ await expect(page.locator('[data-pageid="home-details-page-1"] [data-testid="query-label"]')).toContainText('Query Params:');
+ });
+
+ test('Home Details 1 > Home Tab > Details 1 > Home Tab, should be on home page', async ({ page }) => {
+ // Tests a bug when landing directly on a page thats not the originalHref and going back to home
+ await page.goto(withTestingMode('/routing/tabs/home/details/1'));
+ await ionTabClick(page, 'Home');
+ await ionPageVisible(page, 'home-page');
+ await ionNav(page, 'ion-item', 'Details 1');
+ await ionPageVisible(page, 'home-details-page-1');
+ await ionTabClick(page, 'Home');
+ await ionPageVisible(page, 'home-page');
+ });
+
+ test('Home > Session Details Link > Session Details 2 > Back > Back, should be at home page', async ({ page }) => {
+ // Tests a bug when landing directly on a page thats not the originalHref and going back to home
+ await page.goto(withTestingMode('/routing/tabs/home'));
+ await ionNav(page, 'a', 'Go to details 1 on settings');
+ await ionPageVisible(page, 'settings-details-page-1');
+ await ionNav(page, 'ion-button', 'Go to Settings Details 2');
+ await ionPageVisible(page, 'settings-details-page-2');
+ await ionBackClick(page, 'settings-details-page-2');
+ await ionPageVisible(page, 'settings-details-page-1');
+ await ionBackClick(page, 'settings-details-page-1');
+ await ionPageVisible(page, 'home-page');
+ });
+
+ test('Tab 3 > Other Page > Back, should be back on Tab 3', async ({ page }) => {
+ // Tests transferring from one outlet to another and back again with animation
+ await page.goto(withTestingMode('/routing/tabs/tab3'));
+ await ionNav(page, 'ion-button', 'Go to Other Page');
+ await ionPageVisible(page, 'other-page');
+ await ionBackClick(page, 'other-page');
+ await ionPageVisible(page, 'tab3-page');
+ });
+
+ // SKIPPED: Two MutationObserver tests for #25477 flash tests are in transition-flash.spec.ts
+
+ test('/ > Menu > Favorites > Menu > Tabs, should be back on Home', async ({ page }) => {
+ // Tests transferring from one outlet to another and back again via menu
+ await page.goto(withTestingMode('/routing'));
+ await ionPageVisible(page, 'home-page');
+ await ionMenuClick(page);
+ await ionMenuNav(page, 'Favorites');
+ await ionPageVisible(page, 'favorites-page');
+ await ionMenuClick(page);
+ await ionMenuNav(page, 'Tabs');
+ await ionPageVisible(page, 'home-page');
+ });
+
+ test('/ > Session Details > Details 2 > Details 3 > Browser Back * 3, should be back on home', async ({ page }) => {
+ // Tests browser back button
+ await page.goto(withTestingMode('/routing/'));
+ await ionNav(page, 'ion-item', 'Details 1');
+ await ionPageVisible(page, 'home-details-page-1');
+ await ionNav(page, 'ion-button', 'Go to Details 2');
+ await ionPageVisible(page, 'home-details-page-2');
+ await ionNav(page, 'ion-button', 'Go to Details 3');
+ await ionPageVisible(page, 'home-details-page-3');
+ await ionGoBack(page);
+ await ionPageVisible(page, 'home-details-page-2');
+ await ionGoBack(page);
+ await ionPageVisible(page, 'home-details-page-1');
+ await ionGoBack(page);
+ await ionPageVisible(page, 'home-page');
+ });
+
+ test('/ > Details 1 > Details 2 > Details 3 > Settings Tab > Home Tab > Browser Back, should be back on home', async ({ page }) => {
+ // Tests browser back button with a tab switch
+ await page.goto(withTestingMode('/routing/'));
+ await ionNav(page, 'ion-item', 'Details 1');
+ await ionPageVisible(page, 'home-details-page-1');
+ await ionNav(page, 'ion-button', 'Go to Details 2');
+ await ionPageVisible(page, 'home-details-page-2');
+ await ionNav(page, 'ion-button', 'Go to Details 3');
+ await ionPageVisible(page, 'home-details-page-3');
+ await ionTabClick(page, 'Settings');
+ await ionPageVisible(page, 'settings-page');
+ await ionTabClick(page, 'Home');
+ await ionPageVisible(page, 'home-details-page-3');
+ await ionGoBack(page);
+ await ionPageVisible(page, 'home-details-page-2');
+ await expect(page.locator('ion-tab-button.tab-selected')).toContainText('Home');
+ });
+
+ test('/ > Details 1 > Details 2 > Details 3 > Browser Back > Back > Back, should be back on home', async ({ page }) => {
+ // Tests browser back button with a tab switch
+ await page.goto(withTestingMode('/routing/'));
+ await ionNav(page, 'ion-item', 'Details 1');
+ await ionPageVisible(page, 'home-details-page-1');
+ await ionNav(page, 'ion-button', 'Go to Details 2');
+ await ionPageVisible(page, 'home-details-page-2');
+ await ionNav(page, 'ion-button', 'Go to Details 3');
+ await ionPageVisible(page, 'home-details-page-3');
+ await ionGoBack(page);
+ await ionPageVisible(page, 'home-details-page-2');
+ await ionBackClick(page, 'home-details-page-2');
+ await ionPageVisible(page, 'home-details-page-1');
+ await ionBackClick(page, 'home-details-page-1');
+ await ionPageVisible(page, 'home-page');
+ });
+
+ test('/ > Details 1 > Details 2 > Details 3 > Browser Back > Browser Forward, should be back on Details 3', async ({ page }) => {
+ // Tests browser forward button within a tab's own navigation stack
+ await page.goto(withTestingMode('/routing/'));
+ await ionNav(page, 'ion-item', 'Details 1');
+ await ionPageVisible(page, 'home-details-page-1');
+ await ionNav(page, 'ion-button', 'Go to Details 2');
+ await ionPageVisible(page, 'home-details-page-2');
+ await ionNav(page, 'ion-button', 'Go to Details 3');
+ await ionPageVisible(page, 'home-details-page-3');
+ await ionGoBack(page);
+ await ionPageVisible(page, 'home-details-page-2');
+ await ionGoForward(page);
+ await ionPageVisible(page, 'home-details-page-3');
+ });
+
+ test('/ > Details 1 > Settings Details 1 > Browser Back > Browser Forward, should show Settings Details 1', async ({ page }) => {
+ // Tests browser forward button across tabs (cross-tab forward)
+ await page.goto(withTestingMode('/routing/'));
+ await ionNav(page, 'ion-item', 'Details 1');
+ await ionPageVisible(page, 'home-details-page-1');
+ await ionNav(page, 'ion-button', 'Go to Settings Details 1');
+ await ionPageVisible(page, 'settings-details-page-1');
+ await ionGoBack(page);
+ await ionPageVisible(page, 'home-details-page-1');
+ await ionGoForward(page);
+ await ionPageVisible(page, 'settings-details-page-1');
+ });
+
+ test('when props get passed into a route render, the component should update', async ({ page }) => {
+ await page.goto(withTestingMode('/routing/propstest'));
+ await ionPageVisible(page, 'props-test');
+ await expect(page.locator('div[data-testid=count-label]')).toContainText('1');
+ await page.locator('text=Increment').click();
+ await expect(page.locator('div[data-testid=count-label]')).toContainText('2');
+ await page.locator('text=Increment').click();
+ await expect(page.locator('div[data-testid=count-label]')).toContainText('3');
+ });
+
+ test('/routing/asdf, when accessing a route not defined from root outlet, should show not found page', async ({ page }) => {
+ await page.goto(withTestingMode('/routing/asdf'));
+ await ionPageVisible(page, 'not-found');
+ await expect(page.getByText('Not found')).toBeVisible();
+ });
+
+ test('/tabs/home > Details 1 on settings > Home Tab, should be back on home page', async ({ page }) => {
+ await page.goto(withTestingMode('/routing/tabs/home'));
+ await ionNav(page, 'ion-item', 'Details 1 on settings');
+ await ionPageVisible(page, 'settings-details-page-1');
+ await ionTabClick(page, 'Home');
+ await ionPageVisible(page, 'home-page');
+ });
+
+ test('/ > Details 1 on settings > Back > Settings Tab, should be on setting home', async ({ page }) => {
+ // For bug https://github.com/ionic-team/ionic-framework/issues/21031
+ await page.goto(withTestingMode('/routing/'));
+ await ionNav(page, 'ion-item', 'Details 1 on settings');
+ await ionPageVisible(page, 'settings-details-page-1');
+ await ionBackClick(page, 'settings-details-page-1');
+ await ionPageVisible(page, 'home-page');
+ await ionTabClick(page, 'Settings');
+ await ionPageVisible(page, 'settings-page');
+ });
+
+ test('/routing/tabs/redirect > Should be on settings page > Home Tab > Should be on home page', async ({ page }) => {
+ // tests that a redirect going to a tab other than the first tab works
+ // fixes bug https://github.com/ionic-team/ionic-framework/issues/21830
+ await page.goto(withTestingMode('/routing/tabs/redirect'));
+ await ionPageVisible(page, 'settings-page');
+ await ionTabClick(page, 'Home');
+ await ionPageVisible(page, 'home-page');
+ });
+
+ test('/routing/ > Details 1 > Details 2 > Details 3 > Back > Settings Tab > Home Tab > Should be at details 2 page', async ({ page }) => {
+ // fixes an issue where route history was being lost after starting to go back, switching tabs
+ // and switching back to the same tab again
+ // for bug https://github.com/ionic-team/ionic-framework/issues/21834
+ await page.goto(withTestingMode('/routing'));
+ await ionPageVisible(page, 'home-page');
+ await ionNav(page, 'ion-item', 'Details 1');
+ await ionPageVisible(page, 'home-details-page-1');
+ await ionNav(page, 'ion-button', 'Go to Details 2');
+ await ionPageVisible(page, 'home-details-page-2');
+ await ionNav(page, 'ion-button', 'Go to Details 3');
+ await ionPageVisible(page, 'home-details-page-3');
+ await ionBackClick(page, 'home-details-page-3');
+ await ionPageVisible(page, 'home-details-page-2');
+ await ionTabClick(page, 'Settings');
+ await ionPageVisible(page, 'settings-page');
+ await ionTabClick(page, 'Home');
+ await ionPageVisible(page, 'home-details-page-2');
+ });
+
+ test('/routing/tabs/home Menu > Favorites > Menu > Home with redirect, Home page should be visible, and Favorites should be destroyed', async ({ page }) => {
+ await page.goto(withTestingMode('/routing/tabs/home'));
+ await ionMenuClick(page);
+ await ionMenuNav(page, 'Favorites');
+ await ionPageVisible(page, 'favorites-page');
+ await ionMenuClick(page);
+ await ionMenuNav(page, 'Home with redirect');
+ await ionPageVisible(page, 'home-page');
+ await ionPageDoesNotExist(page, 'favorites-page');
+ });
+
+ test('/routing/tabs/home Menu > Favorites > Menu > Home with router, Home page should be visible, and Favorites should be destroyed', async ({ page }) => {
+ await page.goto(withTestingMode('/routing/tabs/home'));
+ await ionMenuClick(page);
+ await ionMenuNav(page, 'Favorites');
+ await ionPageVisible(page, 'favorites-page');
+ await ionMenuClick(page);
+ await ionMenuNav(page, 'Home with router');
+ await ionPageVisible(page, 'home-page');
+ await ionPageDoesNotExist(page, 'favorites-page');
+ });
+
+ test('should show back button when going back to a pushed page', async ({ page }) => {
+ await page.goto(withTestingMode('/routing/tabs/home'));
+
+ await ionNav(page, 'ion-item', 'Details 1');
+ await ionPageHidden(page, 'home-page');
+ await ionPageVisible(page, 'home-details-page-1');
+
+ await page.locator('ion-tab-button#tab-button-settings').click();
+ await ionPageHidden(page, 'home-details-page-1');
+ await ionPageVisible(page, 'settings-page');
+
+ await page.locator('ion-tab-button#tab-button-home').click();
+ await ionPageHidden(page, 'settings-page');
+ await ionPageVisible(page, 'home-details-page-1');
+
+ await ionBackClick(page, 'home-details-page-1');
+
+ await ionPageDoesNotExist(page, 'home-details-page-1');
+ await ionPageVisible(page, 'home-page');
+ });
+
+ test('should mount new view item instances of parameterized routes', async ({ page }) => {
+ await page.goto(withTestingMode('/routing/tabs/home/details/1'));
+
+ const page1Input = page.locator('div.ion-page[data-pageid=home-details-page-1] [data-testid="details-input"]');
+ await expect(page1Input).toHaveValue('');
+
+ await page1Input.fill('1');
+
+ await ionNav(page, 'ion-button', 'Go to Details 2');
+ await ionPageVisible(page, 'home-details-page-2');
+
+ const page2Input = page.locator('div.ion-page[data-pageid=home-details-page-2] [data-testid="details-input"]');
+ await expect(page2Input).toHaveValue('');
+
+ await page2Input.fill('2');
+
+ await ionNav(page, 'ion-button', 'Go to Details 3');
+ await ionPageVisible(page, 'home-details-page-3');
+
+ const page3Input = page.locator('div.ion-page[data-pageid=home-details-page-3] [data-testid="details-input"]');
+ await expect(page3Input).toHaveValue('');
+
+ await page3Input.fill('3');
+
+ await ionBackClick(page, 'home-details-page-3');
+ await ionPageVisible(page, 'home-details-page-2');
+
+ await expect(page2Input).toHaveValue('2');
+
+ await ionBackClick(page, 'home-details-page-2');
+ await ionPageVisible(page, 'home-details-page-1');
+
+ await expect(page1Input).toHaveValue('1');
+ });
+
+ test('should complete chained Navigate redirects from root to /routing/tabs/home', async ({ page }) => {
+ // Tests that chained Navigate redirects work correctly:
+ // / > click Routing link > /routing (Navigate to tabs) > /routing/tabs (Navigate to home) > /routing/tabs/home
+ // This was a bug where the second Navigate would be unmounted before it could trigger
+ await page.goto(withTestingMode('/'));
+ await ionNav(page, 'ion-item', 'Routing');
+ await ionPageVisible(page, 'home-page');
+ await expect(page).toHaveURL(/\/routing\/tabs\/home/);
+ });
+
+});
diff --git a/packages/react-router/test/base/tests/e2e/playwright/swipe-to-go-back.spec.ts b/packages/react-router/test/base/tests/e2e/playwright/swipe-to-go-back.spec.ts
new file mode 100644
index 00000000000..cbd80062c2c
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/playwright/swipe-to-go-back.spec.ts
@@ -0,0 +1,206 @@
+import { test, expect } from '@playwright/test';
+import { ionPageVisible, ionPageHidden, ionPageDoesNotExist, ionNav } from './utils/test-utils';
+import { ionSwipeToGoBack } from './utils/drag-utils';
+
+const IOS_MODE = 'ionic:mode=ios';
+
+test.describe('Swipe To Go Back', () => {
+ /*
+ This spec tests that swipe to go back works.
+ Animations must be enabled (no ionic:_testing=true) for gesture tests.
+ */
+
+ test('should swipe and abort', async ({ page }) => {
+ await page.goto(`/swipe-to-go-back?${IOS_MODE}`);
+ await ionPageVisible(page, 'main');
+
+ await ionNav(page, 'ion-item', 'Details');
+ await ionPageVisible(page, 'details');
+ await ionPageHidden(page, 'main');
+
+ await ionSwipeToGoBack(page, false, 'ion-router-outlet#swipe-to-go-back');
+ await ionPageVisible(page, 'details');
+ await ionPageHidden(page, 'main');
+ });
+
+ test('should swipe and go back', async ({ page }) => {
+ await page.goto(`/swipe-to-go-back?${IOS_MODE}`);
+ await ionPageVisible(page, 'main');
+
+ await ionNav(page, 'ion-item', 'Details');
+ await ionPageVisible(page, 'details');
+ await ionPageHidden(page, 'main');
+
+ await ionSwipeToGoBack(page, true, 'ion-router-outlet#swipe-to-go-back');
+ await ionPageVisible(page, 'main');
+ });
+
+ test('should render details page when navigating directly to nested route', async ({ page }) => {
+ await page.goto(`/swipe-to-go-back/details?${IOS_MODE}`);
+ await ionPageVisible(page, 'details');
+
+ await expect(page.locator('[data-pageid="details"]')).toBeVisible();
+ await expect(page.locator('[data-pageid="details"] ion-content')).toContainText('Details');
+ });
+
+ test('should swipe and abort within a tab', async ({ page }) => {
+ await page.goto(`/tabs/tab1?${IOS_MODE}`);
+ await ionPageVisible(page, 'tab1');
+
+ await page.locator('#child-one').click();
+ await ionPageHidden(page, 'tab1');
+ await ionPageVisible(page, 'tab1child1');
+
+ await ionSwipeToGoBack(page, false, 'ion-tabs ion-router-outlet');
+
+ await ionPageHidden(page, 'tab1');
+ await ionPageVisible(page, 'tab1child1');
+ });
+
+ test('should swipe and go back within a tab', async ({ page }) => {
+ await page.goto(`/tabs/tab1?${IOS_MODE}`);
+ await ionPageVisible(page, 'tab1');
+
+ await page.locator('#child-one').click();
+ await ionPageHidden(page, 'tab1');
+ await ionPageVisible(page, 'tab1child1');
+
+ await ionSwipeToGoBack(page, true, 'ion-tabs ion-router-outlet');
+
+ await ionPageVisible(page, 'tab1');
+ await ionPageDoesNotExist(page, 'tab1child1');
+ });
+
+ test('should swipe and go back to correct tab after switching tabs', async ({ page }) => {
+ await page.goto(`/?${IOS_MODE}`);
+ await ionPageVisible(page, 'home');
+
+ await page.locator('#go-to-tabs').click();
+ await ionPageHidden(page, 'home');
+ await ionPageVisible(page, 'tab1');
+ await ionPageVisible(page, 'tabs');
+
+ await page.locator('#child-one').click();
+ await ionPageHidden(page, 'tab1');
+ await ionPageVisible(page, 'tab1child1');
+
+ await page.locator('ion-tab-button#tab-button-tab2').click();
+ await ionPageVisible(page, 'tab2');
+ await ionPageHidden(page, 'tab1child1');
+
+ await page.locator('ion-tab-button#tab-button-tab1').click();
+ await ionPageVisible(page, 'tab1child1');
+ await ionPageHidden(page, 'tab2');
+
+ await ionSwipeToGoBack(page, true, 'ion-tabs ion-router-outlet');
+
+ await ionPageVisible(page, 'tab1');
+ await ionPageDoesNotExist(page, 'tab1child1');
+
+ await ionSwipeToGoBack(page, true, 'ion-tabs ion-router-outlet');
+ await ionPageVisible(page, 'home');
+ await ionPageDoesNotExist(page, 'tabs');
+ });
+
+ test('should be able to swipe back from child tab page after visiting', async ({ page }) => {
+ await page.goto(`/tabs/tab1?${IOS_MODE}`);
+ await ionPageVisible(page, 'tab1');
+
+ await page.locator('#child-one').click();
+ await ionPageHidden(page, 'tab1');
+ await ionPageVisible(page, 'tab1child1');
+
+ await page.locator('#child-two').click();
+ await ionPageHidden(page, 'tab1child1');
+ await ionPageVisible(page, 'tab1child2');
+
+ await ionSwipeToGoBack(page, true, 'ion-tabs ion-router-outlet');
+
+ await ionPageDoesNotExist(page, 'tab1child2');
+ await ionPageVisible(page, 'tab1child1');
+
+ await ionSwipeToGoBack(page, true, 'ion-tabs ion-router-outlet');
+
+ await ionPageDoesNotExist(page, 'tab1child1');
+ await ionPageVisible(page, 'tab1');
+
+ await page.locator('#child-one').click();
+ await ionPageHidden(page, 'tab1');
+ await ionPageVisible(page, 'tab1child1');
+
+ await ionSwipeToGoBack(page, true, 'ion-tabs ion-router-outlet');
+
+ await ionPageDoesNotExist(page, 'tab1child1');
+ await ionPageVisible(page, 'tab1');
+ });
+
+ test('should not swipe to go back to the same view you are on', async ({ page }) => {
+ await page.goto(`/?${IOS_MODE}`);
+ await ionPageVisible(page, 'home');
+
+ await ionSwipeToGoBack(page, false);
+ await ionPageVisible(page, 'home');
+ });
+
+ test('should not hide a parameterized page when swiping and aborting', async ({ page }) => {
+ await page.goto(`/params/0?${IOS_MODE}`);
+ await ionPageVisible(page, 'params-0');
+
+ await page.locator('#next-page').click();
+ await ionPageVisible(page, 'params-1');
+
+ await ionSwipeToGoBack(page, false);
+
+ await ionPageVisible(page, 'params-1');
+ });
+
+ test('should keep correct view visible after swipe-back completes then abort on previous page', async ({ page }) => {
+ // Navigate three levels deep: main -> details -> details2
+ await page.goto(`/swipe-to-go-back?${IOS_MODE}`);
+ await ionPageVisible(page, 'main');
+
+ await ionNav(page, 'ion-item', 'Details');
+ await ionPageVisible(page, 'details');
+ await ionPageHidden(page, 'main');
+
+ await page.locator('#go-to-details2').click();
+ await page.waitForTimeout(250);
+ await ionPageVisible(page, 'details2');
+ await ionPageHidden(page, 'details');
+
+ // Complete swipe back from details2 -> details
+ await ionSwipeToGoBack(page, true, 'ion-router-outlet#swipe-to-go-back');
+ await ionPageVisible(page, 'details');
+ await ionPageDoesNotExist(page, 'details2');
+
+ // Now on details, abort a swipe back toward main
+ // This validates that the abort doesn't hide the currently-visible page
+ await ionSwipeToGoBack(page, false, 'ion-router-outlet#swipe-to-go-back');
+ await ionPageVisible(page, 'details');
+ await ionPageHidden(page, 'main');
+ });
+
+ test('should handle multiple consecutive swipe aborts without hiding current page', async ({ page }) => {
+ await page.goto(`/swipe-to-go-back?${IOS_MODE}`);
+ await ionPageVisible(page, 'main');
+
+ await ionNav(page, 'ion-item', 'Details');
+ await ionPageVisible(page, 'details');
+ await ionPageHidden(page, 'main');
+
+ // First abort
+ await ionSwipeToGoBack(page, false, 'ion-router-outlet#swipe-to-go-back');
+ await ionPageVisible(page, 'details');
+ await ionPageHidden(page, 'main');
+
+ // Second abort
+ await ionSwipeToGoBack(page, false, 'ion-router-outlet#swipe-to-go-back');
+ await ionPageVisible(page, 'details');
+ await ionPageHidden(page, 'main');
+
+ // Third abort
+ await ionSwipeToGoBack(page, false, 'ion-router-outlet#swipe-to-go-back');
+ await ionPageVisible(page, 'details');
+ await ionPageHidden(page, 'main');
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/playwright/tab-lifecycle.spec.ts b/packages/react-router/test/base/tests/e2e/playwright/tab-lifecycle.spec.ts
new file mode 100644
index 00000000000..f421357cac3
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/playwright/tab-lifecycle.spec.ts
@@ -0,0 +1,73 @@
+import { test, expect } from '@playwright/test';
+import { ionPageVisible, ionTabClick, withTestingMode } from './utils/test-utils';
+
+test.describe('Tab Lifecycle Events', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.addInitScript(() => {
+ (window as any).lifecycleEvents = [];
+ });
+ });
+
+ test('ionViewDidLeave should fire on active tab child page when navigating away from tabs', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'FW-6788',
+ });
+
+ await page.goto(withTestingMode('/tab-lifecycle/home'));
+ await ionPageVisible(page, 'tab-lifecycle-home');
+
+ await page.evaluate(() => { (window as any).lifecycleEvents = []; });
+
+ await page.locator('#go-outside').click();
+ await ionPageVisible(page, 'tab-lifecycle-outside');
+
+ const events = await page.evaluate(() => (window as any).lifecycleEvents as string[]);
+ expect(events).toContain('home:ionViewWillLeave');
+ expect(events).toContain('home:ionViewDidLeave');
+ });
+
+ test('ionViewDidLeave should fire on active tab child page when navigating from non-default tab', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'FW-6788',
+ });
+
+ await page.goto(withTestingMode('/tab-lifecycle/home'));
+ await ionPageVisible(page, 'tab-lifecycle-home');
+
+ await ionTabClick(page, 'Settings');
+ await ionPageVisible(page, 'tab-lifecycle-settings');
+
+ await page.evaluate(() => { (window as any).lifecycleEvents = []; });
+
+ await page.locator('#go-outside-settings').click();
+ await ionPageVisible(page, 'tab-lifecycle-outside');
+
+ const events = await page.evaluate(() => (window as any).lifecycleEvents as string[]);
+ expect(events).toContain('settings:ionViewWillLeave');
+ expect(events).toContain('settings:ionViewDidLeave');
+ });
+
+ test('ionViewDidEnter should fire on tab child page when navigating back to tabs', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'FW-6788',
+ });
+
+ await page.goto(withTestingMode('/tab-lifecycle/home'));
+ await ionPageVisible(page, 'tab-lifecycle-home');
+
+ await page.locator('#go-outside').click();
+ await ionPageVisible(page, 'tab-lifecycle-outside');
+
+ await page.evaluate(() => { (window as any).lifecycleEvents = []; });
+
+ await page.locator('#go-back-to-tabs').click();
+ await ionPageVisible(page, 'tab-lifecycle-home');
+
+ const events = await page.evaluate(() => (window as any).lifecycleEvents as string[]);
+ expect(events).toContain('home:ionViewWillEnter');
+ expect(events).toContain('home:ionViewDidEnter');
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/playwright/tabs.spec.ts b/packages/react-router/test/base/tests/e2e/playwright/tabs.spec.ts
new file mode 100644
index 00000000000..490836374ee
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/playwright/tabs.spec.ts
@@ -0,0 +1,89 @@
+import { test, expect } from '@playwright/test';
+import {
+ ionPageVisible, ionPageHidden, ionPageDoesNotExist,
+ ionNav, ionBackClick, ionTabClick, ionGoBack, withTestingMode
+} from './utils/test-utils';
+
+test.describe('Tabs', () => {
+ // Verifies fix for https://github.com/ionic-team/ionic-framework/issues/23101
+ test('should return to previous tab instance when using the ion-back-button', async ({ page }) => {
+ await page.goto(withTestingMode('/tabs/tab1'));
+
+ await page.locator('#tabs-secondary').click();
+ await ionPageVisible(page, 'tab1-secondary');
+
+ await page.locator('ion-tab-button#tab-button-tab2-secondary').click();
+ await ionPageHidden(page, 'tab1-secondary');
+ await ionPageVisible(page, 'tab2-secondary');
+
+ await page.locator('ion-tab-button#tab-button-tab1-secondary').click();
+ await ionPageHidden(page, 'tab2-secondary');
+ await ionPageVisible(page, 'tab1-secondary');
+
+ await ionBackClick(page, 'tab1-secondary');
+ await ionPageDoesNotExist(page, 'tabs-secondary');
+ await ionPageVisible(page, 'tab1');
+ });
+
+ // Verifies that /tabs outlet uses segment-aware scope checking
+ // so /tabs-secondary is not treated as a prefix match of /tabs
+ test('should hide /tabs views when navigating to /tabs-secondary (segment-aware scope)', async ({ page }) => {
+ await page.goto(withTestingMode('/tabs/tab1'));
+ await ionPageVisible(page, 'tab1');
+
+ await page.locator('#tabs-secondary').click();
+ await ionPageVisible(page, 'tab1-secondary');
+ await ionPageDoesNotExist(page, 'tab1');
+ });
+
+ // Verifies fix for https://github.com/ionic-team/ionic-framework/issues/25141
+ test('browser back from preserved child page should sync URL and display', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://github.com/ionic-team/ionic-framework/issues/25141',
+ });
+
+ await page.goto(withTestingMode('/tabs/tab1'));
+ await ionPageVisible(page, 'tab1');
+
+ await page.locator('#child-one').click();
+ await ionPageHidden(page, 'tab1');
+ await ionPageVisible(page, 'tab1child1');
+
+ await ionTabClick(page, 'Tab2');
+ await ionPageHidden(page, 'tab1child1');
+ await ionPageVisible(page, 'tab2');
+
+ await ionTabClick(page, 'Tab1');
+ await ionPageHidden(page, 'tab2');
+ await ionPageVisible(page, 'tab1child1');
+
+ await ionGoBack(page, '/tabs/tab1');
+ await ionPageDoesNotExist(page, 'tab1child1');
+ await ionPageVisible(page, 'tab1');
+ await expect(page.locator('ion-tab-button.tab-selected')).toContainText('Tab1');
+ });
+
+ // Verifies fix for https://github.com/ionic-team/ionic-framework/issues/23087
+ test('should return to correct view and url when going back from child page after switching tabs', async ({ page }) => {
+ await page.goto(withTestingMode('/tabs/tab1'));
+
+ await page.locator('#child-one').click();
+ await ionPageHidden(page, 'tab1');
+ await ionPageVisible(page, 'tab1child1');
+
+ await page.locator('ion-tab-button#tab-button-tab2').click();
+ await ionPageHidden(page, 'tab1child1');
+ await ionPageVisible(page, 'tab2');
+
+ await page.locator('ion-tab-button#tab-button-tab1').click();
+ await ionPageHidden(page, 'tab2');
+ await ionPageVisible(page, 'tab1child1');
+
+ await ionBackClick(page, 'tab1child1');
+ await ionPageDoesNotExist(page, 'tab1child1');
+ await ionPageVisible(page, 'tab1');
+
+ await expect(page).toHaveURL(/\/tabs\/tab1/);
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/playwright/transition-flash.spec.ts b/packages/react-router/test/base/tests/e2e/playwright/transition-flash.spec.ts
new file mode 100644
index 00000000000..45816964184
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/playwright/transition-flash.spec.ts
@@ -0,0 +1,75 @@
+import { test, expect } from '@playwright/test';
+import { ionPageVisible } from './utils/test-utils';
+import { captureClassChanges, assertClassNeverApplied, didChildReceiveInvisibleClass } from './utils/animation-utils';
+
+test.describe('Transition Flash', () => {
+ // Verifies fix for https://github.com/ionic-team/ionic-framework/issues/25477
+ // Tests that navigating from a tab page to a non-tab page does not cause
+ // the tab page content to vanish before the transition animation completes.
+ // Bug: handleOutOfScopeOutlet immediately applied ion-page-hidden (display: none)
+ // to tab views, causing the leaving page to flash blank during the forward transition.
+ test('Tab 3 > Other Page: tab page should not flash blank during transition', async ({ page }) => {
+ await page.goto('/routing/tabs/tab3?ionic:mode=ios');
+ await ionPageVisible(page, 'tab3-page');
+
+ const classHistory = await captureClassChanges(page, '[data-pageid="tab3-page"]', async () => {
+ await page.locator('ion-button').filter({ hasText: 'Go to Other Page' }).click();
+ await ionPageVisible(page, 'other-page');
+ });
+
+ assertClassNeverApplied(classHistory, 'ion-page-hidden');
+ });
+
+ // Verifies fix for https://github.com/ionic-team/ionic-framework/issues/25477
+ // Same test as above but from the Home tab using routerLink navigation.
+ // Scopes the click to the home page to avoid matching the menu's "Other Page" item.
+ test('Home > Other Page: tab page should not flash blank during transition', async ({ page }) => {
+ await page.goto('/routing/tabs/home?ionic:mode=ios');
+ await ionPageVisible(page, 'home-page');
+
+ const classHistory = await captureClassChanges(page, '[data-pageid="home-page"]', async () => {
+ await page.locator('[data-pageid="home-page"] ion-item').filter({ hasText: 'Other Page' }).click();
+ await ionPageVisible(page, 'other-page');
+ });
+
+ assertClassNeverApplied(classHistory, 'ion-page-hidden');
+ });
+
+ // Verifies that forward navigation actually triggers an animation.
+ // The entering page should receive ion-page-invisible (opacity: 0) during the
+ // transition, then have it removed when the animation completes.
+ // Observes the router-outlet container since the entering page doesn't exist
+ // in the DOM until React creates it during navigation.
+ test('Forward transition should animate (ion-page-invisible lifecycle)', async ({ page }) => {
+ await page.goto('/routing/tabs/home?ionic:mode=ios');
+ await ionPageVisible(page, 'home-page');
+
+ const sawInvisible = await didChildReceiveInvisibleClass(
+ page,
+ 'ion-router-outlet#tabs',
+ async () => {
+ await page.locator('[data-pageid="home-page"] ion-item').filter({ hasText: 'Details 1' }).first().click();
+ await ionPageVisible(page, 'home-details-page-1');
+ }
+ );
+
+ expect(sawInvisible, 'Entering page should have had ion-page-invisible during transition').toBe(true);
+ });
+
+ // Verifies that the forward push from the swipe-to-go-back test page also animates.
+ test('Swipe-to-go-back forward push should animate', async ({ page }) => {
+ await page.goto('/swipe-to-go-back?ionic:mode=ios');
+ await ionPageVisible(page, 'main');
+
+ const sawInvisible = await didChildReceiveInvisibleClass(
+ page,
+ 'ion-router-outlet#swipe-to-go-back',
+ async () => {
+ await page.locator('ion-item').filter({ hasText: 'Details' }).click();
+ await ionPageVisible(page, 'details');
+ }
+ );
+
+ expect(sawInvisible, 'Entering page should have had ion-page-invisible during transition').toBe(true);
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/playwright/utils/animation-utils.ts b/packages/react-router/test/base/tests/e2e/playwright/utils/animation-utils.ts
new file mode 100644
index 00000000000..064d72ce97e
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/playwright/utils/animation-utils.ts
@@ -0,0 +1,156 @@
+import { expect, type Page } from '@playwright/test';
+
+/**
+ * Captures all CSS class changes on an element during an action.
+ *
+ * Installs a MutationObserver before the action runs, executes the action,
+ * then disconnects the observer and returns the recorded class history.
+ *
+ * Usage:
+ * const history = await captureClassChanges(page, '[data-pageid="home"]', async () => {
+ * await page.locator('ion-item').click();
+ * await ionPageVisible(page, 'details');
+ * });
+ * assertClassNeverApplied(history, 'ion-page-hidden');
+ */
+export async function captureClassChanges(
+ page: Page,
+ selector: string,
+ action: () => Promise
+): Promise {
+ // Install observer before the action
+ await page.evaluate((sel) => {
+ const el = document.querySelector(sel);
+ (window as any).__ionClassCapture = [];
+ if (!el) return;
+ const obs = new MutationObserver(() => {
+ (window as any).__ionClassCapture.push((el as HTMLElement).className);
+ });
+ obs.observe(el, { attributes: true, attributeFilter: ['class'] });
+ (window as any).__ionClassCaptureObs = obs;
+ }, selector);
+
+ // Execute the action (e.g., click a link and wait for page transition)
+ await action();
+
+ // Read and disconnect
+ return page.evaluate(() => {
+ (window as any).__ionClassCaptureObs?.disconnect();
+ return ((window as any).__ionClassCapture ?? []) as string[];
+ });
+}
+
+/**
+ * Asserts that a CSS class was NEVER applied during the captured class history.
+ * Used to verify that ion-page-hidden (display:none) was not applied during
+ * a transition, which would cause a blank flash.
+ */
+export function assertClassNeverApplied(classHistory: string[], className: string): void {
+ const wasApplied = classHistory.some((c) => c.includes(className));
+ expect(wasApplied, `"${className}" was unexpectedly applied during transition`).toBe(false);
+}
+
+/**
+ * Asserts that a CSS class appeared at some point during the captured history
+ * but is NOT present in the final snapshot. This proves a transition animation
+ * actually ran (e.g., ion-page-invisible was added then removed).
+ */
+export function assertTransitionalClass(classHistory: string[], className: string): void {
+ expect(classHistory.length, `No class mutations were captured β observer may not have matched the element`).toBeGreaterThan(0);
+ const wasPresent = classHistory.some((c) => c.includes(className));
+ const isGoneAtEnd = !classHistory[classHistory.length - 1].includes(className);
+ expect(wasPresent, `Expected "${className}" to appear during transition`).toBe(true);
+ expect(isGoneAtEnd, `Expected "${className}" to be removed after transition`).toBe(true);
+}
+
+/**
+ * Observes a router-outlet for any child element receiving ion-page-invisible
+ * during an action. Used to verify that entering pages go through the
+ * invisible β visible animation lifecycle.
+ *
+ * Unlike captureClassChanges (which observes a specific existing element),
+ * this observes the outlet container with subtree: true to catch class changes
+ * on elements that are created during the action (entering pages that don't
+ * exist in the DOM before navigation starts).
+ */
+export async function didChildReceiveInvisibleClass(
+ page: Page,
+ outletSelector: string,
+ action: () => Promise
+): Promise {
+ await page.evaluate((sel) => {
+ (window as any).__ionSawInvisible = false;
+ const outlet = document.querySelector(sel);
+ if (!outlet) return;
+ const obs = new MutationObserver((mutations) => {
+ for (const m of mutations) {
+ if (m.type === 'attributes' && (m.target as HTMLElement).classList?.contains('ion-page-invisible')) {
+ (window as any).__ionSawInvisible = true;
+ }
+ if (m.type === 'childList') {
+ m.addedNodes.forEach((n) => {
+ if ((n as HTMLElement).classList?.contains('ion-page-invisible')) {
+ (window as any).__ionSawInvisible = true;
+ }
+ });
+ }
+ }
+ });
+ obs.observe(outlet, { attributes: true, attributeFilter: ['class'], childList: true, subtree: true });
+ (window as any).__ionInvisibleObs = obs;
+ }, outletSelector);
+
+ await action();
+
+ return page.evaluate(() => {
+ (window as any).__ionInvisibleObs?.disconnect();
+ return (window as any).__ionSawInvisible as boolean;
+ });
+}
+
+/**
+ * Checks whether any CSS animations or transitions ran on an element during an action.
+ * Uses the Web Animations API (document.getAnimations()).
+ *
+ * Note: This has a race condition if the animation completes before evaluate fires.
+ * Best used for animations longer than ~300ms. For shorter animations, prefer
+ * captureClassChanges + assertTransitionalClass.
+ */
+export async function didElementAnimate(
+ page: Page,
+ selector: string,
+ action: () => Promise
+): Promise {
+ // Record animation count before
+ const beforeCount = await page.evaluate((sel) => {
+ const el = document.querySelector(sel);
+ if (!el) return 0;
+ return document.getAnimations().filter((a) => (a.effect as KeyframeEffect)?.target === el).length;
+ }, selector);
+
+ await action();
+
+ // Check for new or finished animations
+ return page.evaluate(
+ ({ sel, before }) => {
+ const el = document.querySelector(sel);
+ if (!el) return false;
+ const anims = document.getAnimations().filter((a) => (a.effect as KeyframeEffect)?.target === el);
+ return anims.length > before || anims.some((a) => a.playState === 'finished');
+ },
+ { sel: selector, before: beforeCount }
+ );
+}
+
+/**
+ * Waits for all running animations on an element to complete.
+ * Use after triggering navigation when animations are enabled.
+ */
+export async function waitForAnimationsComplete(page: Page, selector: string): Promise {
+ await page.evaluate(async (sel) => {
+ const el = document.querySelector(sel);
+ if (!el) return;
+ const anims = document.getAnimations().filter((a) => (a.effect as KeyframeEffect)?.target === el);
+ await Promise.all(anims.map((a) => a.finished));
+ }, selector);
+}
diff --git a/packages/react-router/test/base/tests/e2e/playwright/utils/drag-utils.ts b/packages/react-router/test/base/tests/e2e/playwright/utils/drag-utils.ts
new file mode 100644
index 00000000000..dd4ce1d65d7
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/playwright/utils/drag-utils.ts
@@ -0,0 +1,69 @@
+import type { Page } from '@playwright/test';
+
+/**
+ * Drag an element by a given number of pixels.
+ * Uses page.mouse with intermediate steps for gesture recognition.
+ */
+export const dragElementBy = async (
+ page: Page,
+ selector: string,
+ dragByX = 0,
+ dragByY = 0
+): Promise => {
+ const el = page.locator(selector).first();
+ const box = await el.boundingBox();
+ if (!box) throw new Error(`Element not found or not visible: ${selector}`);
+
+ const startX = box.x + box.width / 2;
+ const startY = box.y + box.height / 2;
+
+ await page.mouse.move(startX, startY);
+ await page.mouse.down();
+ await page.mouse.move(startX + dragByX, startY + dragByY, { steps: 10 });
+ await page.mouse.up();
+};
+
+/**
+ * Simulate iOS swipe-to-go-back gesture on a router outlet.
+ *
+ * Ionic's gesture recognizer requires:
+ * 1. mousedown near the left edge (within ~50px)
+ * 2. Multiple intermediate mousemove events to establish velocity
+ * 3. mouseup to complete or abort
+ *
+ * @param page Playwright page
+ * @param complete If true, swipe far enough to trigger back navigation.
+ * If false, swipe a small amount then release (abort).
+ * @param outletSelector CSS selector for the ion-router-outlet to swipe on.
+ */
+export const ionSwipeToGoBack = async (
+ page: Page,
+ complete = false,
+ outletSelector = 'ion-router-outlet'
+): Promise => {
+ const outlet = page.locator(outletSelector).first();
+ const box = await outlet.boundingBox();
+ if (!box) throw new Error(`Router outlet not found or not visible: ${outletSelector}`);
+
+ const startX = box.x;
+ const endX = complete ? box.x + box.width - 50 : box.x + 25;
+ const y = box.y + box.height / 2;
+
+ const urlBefore = page.url();
+
+ await page.mouse.move(startX, y);
+ await page.mouse.down();
+ // steps: 10 produces intermediate mousemove events essential for gesture recognition
+ await page.mouse.move(endX, y, { steps: 10 });
+ await page.mouse.up();
+
+ if (complete) {
+ // Wait for goBack() to trigger the URL change, confirming navigation completed.
+ // Falls back to a fixed timeout if the URL doesn't change (abort edge cases).
+ await page.waitForURL((url) => url.toString() !== urlBefore, { timeout: 3000 })
+ .catch((e: Error) => { if (e.name !== 'TimeoutError') throw e; });
+ }
+
+ // Small settle time for the transition animation to finish
+ await page.waitForTimeout(150);
+};
diff --git a/packages/react-router/test/base/tests/e2e/playwright/utils/test-utils.ts b/packages/react-router/test/base/tests/e2e/playwright/utils/test-utils.ts
new file mode 100644
index 00000000000..364047db60f
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/playwright/utils/test-utils.ts
@@ -0,0 +1,123 @@
+import { expect, type Page } from '@playwright/test';
+
+/**
+ * Selector for React Router test pages using data-pageid attribute.
+ * React test pages use `` convention.
+ */
+function pageSelector(pageId: string): string {
+ return `div.ion-page[data-pageid="${pageId}"]`;
+}
+
+/**
+ * Appends ionic:_testing=true to a URL to disable Ionic animations.
+ * Use for behavioral tests that don't need to verify animations.
+ */
+export function withTestingMode(path: string): string {
+ const separator = path.includes('?') ? '&' : '?';
+ return `${path}${separator}ionic:_testing=true`;
+}
+
+/**
+ * Assert that a page is visible and not hidden or invisible.
+ * Equivalent to Cypress `cy.ionPageVisible(pageId)`.
+ */
+export async function ionPageVisible(page: Page, pageId: string): Promise {
+ const locator = page.locator(pageSelector(pageId));
+ await expect(locator).toHaveCount(1);
+ await expect(locator).toBeVisible();
+ await expect(locator).not.toHaveClass(/ion-page-hidden/);
+ await expect(locator).not.toHaveClass(/ion-page-invisible/);
+ await expect(locator).not.toHaveAttribute('aria-hidden', 'true');
+}
+
+/**
+ * Assert that a page is hidden with ion-page-hidden class.
+ * Equivalent to Cypress `cy.ionPageHidden(pageId)`.
+ */
+export async function ionPageHidden(page: Page, pageId: string): Promise {
+ const locator = page.locator(pageSelector(pageId));
+ await expect(locator).toHaveClass(/ion-page-hidden/);
+ await expect(locator).toHaveAttribute('aria-hidden', 'true');
+ await expect(locator).toHaveCount(1);
+}
+
+/**
+ * Assert that a page does not exist in the DOM.
+ * Equivalent to Cypress `cy.ionPageDoesNotExist(pageId)`.
+ */
+export async function ionPageDoesNotExist(page: Page, pageId: string): Promise {
+ await expect(page.locator(pageSelector(pageId))).toHaveCount(0);
+}
+
+/**
+ * Click an element matching tag + text content, then wait for transition.
+ * Equivalent to Cypress `cy.ionNav(element, contains)`.
+ */
+export async function ionNav(page: Page, element: string, contains: string): Promise {
+ await page.locator(element).filter({ hasText: contains }).first().click();
+ await page.waitForTimeout(250);
+}
+
+/**
+ * Click the back button inside a specific page.
+ * Equivalent to Cypress `cy.ionBackClick(pageId)`.
+ */
+export async function ionBackClick(page: Page, pageId: string): Promise {
+ const pageLocator = page.locator(pageSelector(pageId));
+ await expect(pageLocator).toBeVisible();
+ await pageLocator.locator('ion-back-button').click();
+}
+
+/**
+ * Click a tab button by text. Includes a 500ms wait before clicking
+ * to handle the timing issue with tab switches after forward navigation (FW-2800).
+ * Equivalent to Cypress `cy.ionTabClick(tabText)`.
+ */
+export async function ionTabClick(page: Page, tabText: string): Promise {
+ await page.waitForTimeout(500);
+ await page.locator('ion-tab-button').filter({ hasText: tabText }).click({ force: true });
+}
+
+/**
+ * Click the menu button and wait for the menu animation.
+ * Equivalent to Cypress `cy.ionMenuClick()`.
+ */
+export async function ionMenuClick(page: Page): Promise {
+ await page.locator('ion-menu-button').first().click({ force: true });
+ await page.waitForTimeout(500);
+}
+
+/**
+ * Click a menu navigation item and wait for transition.
+ * Equivalent to Cypress `cy.ionMenuNav(contains)`.
+ */
+export async function ionMenuNav(page: Page, contains: string): Promise {
+ await page.locator('ion-item').filter({ hasText: contains }).first().click({ force: true });
+ await page.waitForTimeout(250);
+}
+
+/**
+ * Navigate browser back and optionally assert URL contains a fragment.
+ * Equivalent to Cypress `cy.ionGoBack(expectedUrlPart)`.
+ */
+export async function ionGoBack(page: Page, expectedUrlPart?: string): Promise {
+ await page.goBack();
+ if (expectedUrlPart) {
+ const escaped = expectedUrlPart.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ await expect(page).toHaveURL(new RegExp(escaped));
+ }
+ await page.waitForTimeout(500);
+}
+
+/**
+ * Navigate browser forward and optionally assert URL contains a fragment.
+ * Equivalent to Cypress `cy.ionGoForward(expectedUrlPart)`.
+ */
+export async function ionGoForward(page: Page, expectedUrlPart?: string): Promise {
+ await page.goForward();
+ if (expectedUrlPart) {
+ const escaped = expectedUrlPart.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ await expect(page).toHaveURL(new RegExp(escaped));
+ }
+ await page.waitForTimeout(500);
+}
diff --git a/packages/react-router/test/base/tests/e2e/plugins/index.js b/packages/react-router/test/base/tests/e2e/plugins/index.js
index 8dd144a6c1a..a9fbe480369 100644
--- a/packages/react-router/test/base/tests/e2e/plugins/index.js
+++ b/packages/react-router/test/base/tests/e2e/plugins/index.js
@@ -18,4 +18,5 @@
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
+ require('cypress-terminal-report/src/installLogsPrinter')(on);
};
diff --git a/packages/react-router/test/base/tests/e2e/specs/content-change-navigation.cy.js b/packages/react-router/test/base/tests/e2e/specs/content-change-navigation.cy.js
new file mode 100644
index 00000000000..a812b042b43
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/specs/content-change-navigation.cy.js
@@ -0,0 +1,54 @@
+/**
+ * Verifies that when view content changes (causing IonPage to remount)
+ * while navigation is happening, the correct view is displayed.
+ *
+ * @see https://github.com/ionic-team/ionic-framework/issues/28878
+ */
+
+const port = 3000;
+
+describe('Content Change Navigation Tests', () => {
+ it('should navigate to list page correctly', () => {
+ cy.visit(`http://localhost:${port}/content-change-navigation`);
+ cy.ionPageVisible('content-nav-home');
+
+ cy.get('[data-testid="go-to-list"]').click();
+ cy.wait(300);
+
+ cy.ionPageVisible('list-page');
+ cy.url().should('include', '/content-change-navigation/list');
+ });
+
+ it('when clearing items and navigating, should show home page, not empty view', () => {
+ cy.visit(`http://localhost:${port}/content-change-navigation`);
+ cy.ionPageVisible('content-nav-home');
+
+ cy.get('[data-testid="go-to-list"]').click();
+ cy.wait(300);
+ cy.ionPageVisible('list-page');
+
+ // Bug scenario: clearing items renders a different IonPage while navigating away
+ cy.get('[data-testid="clear-and-navigate"]').click();
+ cy.wait(500);
+
+ cy.url().should('include', '/content-change-navigation/home');
+ cy.url().should('not.include', '/content-change-navigation/list');
+ cy.ionPageVisible('content-nav-home');
+ cy.get('[data-testid="home-content"]').should('be.visible');
+
+ // The empty view should NOT be visible (the fix ensures it's hidden)
+ cy.get('[data-testid="empty-view"]').should('not.be.visible');
+ });
+
+ it('direct navigation to home should work correctly', () => {
+ cy.visit(`http://localhost:${port}/content-change-navigation/home`);
+ cy.ionPageVisible('content-nav-home');
+ cy.get('[data-testid="home-content"]').should('be.visible');
+ });
+
+ it('direct navigation to list should work correctly', () => {
+ cy.visit(`http://localhost:${port}/content-change-navigation/list`);
+ cy.ionPageVisible('list-page');
+ cy.contains('Item 1').should('be.visible');
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/specs/cross-route-navigation.cy.js b/packages/react-router/test/base/tests/e2e/specs/cross-route-navigation.cy.js
new file mode 100644
index 00000000000..e3b57ac7009
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/specs/cross-route-navigation.cy.js
@@ -0,0 +1,168 @@
+const port = 3000;
+
+describe('Cross-Route Navigation', () => {
+ /**
+ * This test verifies that navigation between different top-level routes works correctly.
+ *
+ * Routing uses and MultipleTabs uses
+ * , ensuring view isolation between outlets.
+ */
+ it('should navigate from home to routing and back correctly', () => {
+ // Start at home
+ cy.visit(`http://localhost:${port}/`);
+ cy.ionPageVisible('home');
+
+ // Navigate to routing by clicking the link
+ cy.contains('ion-item', 'Routing').click();
+
+ // Routing should redirect to /routing/tabs/home and show the home-page
+ cy.ionPageVisible('home-page');
+
+ // Go back to the main home page using browser back
+ cy.go('back');
+
+ // Home page should be visible again
+ cy.ionPageVisible('home');
+ });
+
+ it('should navigate from home to multiple-tabs correctly', () => {
+ // Start at home
+ cy.visit(`http://localhost:${port}/`);
+ cy.ionPageVisible('home');
+
+ // Navigate to multiple-tabs
+ cy.contains('ion-item', 'Multiple Tabs').click();
+
+ // Multiple tabs should redirect to /multiple-tabs/tab1/pagea and show PageA
+ cy.ionPageVisible('PageA');
+ });
+
+ it('should navigate home -> routing -> back -> routing again', () => {
+ // Start at home
+ cy.visit(`http://localhost:${port}/`);
+ cy.ionPageVisible('home');
+
+ // Navigate to routing
+ cy.contains('ion-item', 'Routing').click();
+ cy.ionPageVisible('home-page');
+
+ // Go back to home
+ cy.go('back');
+ cy.ionPageVisible('home');
+
+ // Navigate to routing again - Navigate should fire again
+ cy.contains('ion-item', 'Routing').click();
+ cy.ionPageVisible('home-page');
+ });
+
+ it('should navigate home -> multiple-tabs -> back -> multiple-tabs again', () => {
+ // Start at home
+ cy.visit(`http://localhost:${port}/`);
+ cy.ionPageVisible('home');
+
+ // Navigate to multiple-tabs
+ cy.contains('ion-item', 'Multiple Tabs').click();
+ cy.ionPageVisible('PageA');
+
+ // Go back to home
+ cy.go('back');
+ cy.ionPageVisible('home');
+
+ // Navigate to multiple-tabs again - Navigate should fire again
+ cy.contains('ion-item', 'Multiple Tabs').click();
+ cy.ionPageVisible('PageA');
+ });
+
+ /**
+ * This test verifies behavior when navigating between different top-level routes
+ * that use separate outlet IDs. With unique outlet IDs, view items are completely
+ * isolated between outlets, so "stale views" from one outlet don't interfere with
+ * another outlet.
+ *
+ * This test uses ionPageDoesNotExist instead of ionPageHidden because views from
+ * one route hierarchy (like /routing/*) are completely unmounted (not just hidden)
+ * when navigating to a different route hierarchy (like /multiple-tabs/*).
+ */
+ it('should navigate home -> routing -> home -> multiple-tabs without stale views', () => {
+ // Start at home
+ cy.visit(`http://localhost:${port}/`);
+ cy.ionPageVisible('home');
+
+ // Navigate to routing
+ cy.contains('ion-item', 'Routing').click();
+ cy.ionPageVisible('home-page');
+ cy.url().should('include', '/routing/tabs/home');
+
+ // Go back to home
+ cy.go('back');
+ cy.ionPageVisible('home');
+
+ // Navigate to multiple-tabs - this is where stale views could interfere
+ cy.contains('ion-item', 'Multiple Tabs').click();
+ cy.ionPageVisible('PageA');
+ cy.url().should('include', '/multiple-tabs/tab1/pagea');
+
+ // The routing home-page should NOT exist in the DOM (views are unmounted when leaving route)
+ cy.ionPageDoesNotExist('home-page');
+ });
+
+ /**
+ * Test the reverse: multiple-tabs -> home -> routing
+ * With unique outlet IDs, views are isolated and properly unmounted.
+ */
+ it('should navigate home -> multiple-tabs -> home -> routing without stale views', () => {
+ // Start at home
+ cy.visit(`http://localhost:${port}/`);
+ cy.ionPageVisible('home');
+
+ // Navigate to multiple-tabs
+ cy.contains('ion-item', 'Multiple Tabs').click();
+ cy.ionPageVisible('PageA');
+ cy.url().should('include', '/multiple-tabs/tab1/pagea');
+
+ // Go back to home
+ cy.go('back');
+ cy.ionPageVisible('home');
+
+ // Navigate to routing - stale views from multiple-tabs should be cleaned up
+ cy.contains('ion-item', 'Routing').click();
+ cy.ionPageVisible('home-page');
+ cy.url().should('include', '/routing/tabs/home');
+
+ // PageA from multiple-tabs should NOT exist in the DOM (views are unmounted when leaving route)
+ cy.ionPageDoesNotExist('PageA');
+ });
+
+ /**
+ * Test navigating to another page and back to routing
+ * With unique outlet IDs, views are isolated and properly unmounted.
+ */
+ it('should not have overlay issues when navigating between different routes', () => {
+ // Start at home
+ cy.visit(`http://localhost:${port}/`);
+ cy.ionPageVisible('home');
+
+ // Navigate to routing
+ cy.contains('ion-item', 'Routing').click();
+ cy.ionPageVisible('home-page');
+
+ // Go back to home
+ cy.go('back');
+ cy.ionPageVisible('home');
+
+ // Navigate to multiple-tabs
+ cy.contains('ion-item', 'Multiple Tabs').click();
+ cy.ionPageVisible('PageA');
+
+ // Go back to home again
+ cy.go('back');
+ cy.ionPageVisible('home');
+
+ // Navigate back to routing - no stale views should overlay
+ cy.contains('ion-item', 'Routing').click();
+ cy.ionPageVisible('home-page');
+
+ // Verify PageA does not exist in the DOM (views are unmounted when leaving route)
+ cy.ionPageDoesNotExist('PageA');
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/specs/direction-none-back.cy.js b/packages/react-router/test/base/tests/e2e/specs/direction-none-back.cy.js
new file mode 100644
index 00000000000..c1730c8b373
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/specs/direction-none-back.cy.js
@@ -0,0 +1,42 @@
+const port = 3000;
+
+/**
+ * Tests that IonBackButton works correctly after navigating with
+ * routerDirection="none". The back button should use history to
+ * determine the previous page, not fall back to defaultHref.
+ *
+ * @see https://github.com/ionic-team/ionic-framework/issues/24074
+ */
+describe('routerDirection="none" Back Button', () => {
+
+ it('back button should return to Page A after navigating with direction "forward"', () => {
+ cy.visit(`http://localhost:${port}/direction-none-back/a`);
+ cy.ionPageVisible('direction-none-page-a');
+
+ // Navigate A -> B with default forward direction
+ cy.ionNav('ion-button#go-forward', 'Go to B (forward)');
+ cy.ionPageVisible('direction-none-page-b');
+
+ // Back button should go back to Page A (not to defaultHref fallback)
+ cy.ionBackClick('direction-none-page-b');
+ cy.ionPageDoesNotExist('direction-none-fallback');
+ cy.ionPageVisible('direction-none-page-a');
+ cy.url().should('include', '/direction-none-back/a');
+ });
+
+ it('back button should return to Page A after navigating with direction "none"', () => {
+ cy.visit(`http://localhost:${port}/direction-none-back/a`);
+ cy.ionPageVisible('direction-none-page-a');
+
+ // Navigate A -> B with routerDirection="none"
+ cy.ionNav('ion-button#go-none', 'Go to B (none)');
+ cy.ionPageVisible('direction-none-page-b');
+
+ // Back button should go back to Page A (not to defaultHref fallback)
+ cy.ionBackClick('direction-none-page-b');
+ cy.ionPageDoesNotExist('direction-none-fallback');
+ cy.ionPageVisible('direction-none-page-a');
+ cy.url().should('include', '/direction-none-back/a');
+ });
+
+});
diff --git a/packages/react-router/test/base/tests/e2e/specs/dynamic-ionpage-classnames.cy.js b/packages/react-router/test/base/tests/e2e/specs/dynamic-ionpage-classnames.cy.js
index d41781fe21e..44c9f2834ba 100644
--- a/packages/react-router/test/base/tests/e2e/specs/dynamic-ionpage-classnames.cy.js
+++ b/packages/react-router/test/base/tests/e2e/specs/dynamic-ionpage-classnames.cy.js
@@ -15,4 +15,24 @@ describe('Dynamic IonPage Classnames', () => {
cy.get('.other-class');
cy.ionPageVisible('dynamic-ionpage-classnames');
});
+
+ it('should preserve framework-added classes like can-go-back when className prop changes', () => {
+ const page = '[data-pageid="dynamic-ionpage-classnames"]';
+
+ // Navigate from home to create history (triggers can-go-back class)
+ cy.visit(`http://localhost:${port}/`);
+ cy.ionPageVisible('home');
+ cy.contains('Dynamic IonPage Classnames').click();
+ cy.ionPageVisible('dynamic-ionpage-classnames');
+
+ cy.get(page).should('have.class', 'initial-class');
+ cy.get(page).should('have.class', 'can-go-back');
+
+ cy.contains('Add Class').click();
+
+ cy.get(page).should('have.class', 'other-class');
+ cy.get(page).should('not.have.class', 'initial-class');
+ cy.get(page).should('have.class', 'can-go-back');
+ cy.ionPageVisible('dynamic-ionpage-classnames');
+ });
});
diff --git a/packages/react-router/test/base/tests/e2e/specs/dynamic-routes.cy.js b/packages/react-router/test/base/tests/e2e/specs/dynamic-routes.cy.js
index 76cc14ad7b4..9e2210f47a3 100644
--- a/packages/react-router/test/base/tests/e2e/specs/dynamic-routes.cy.js
+++ b/packages/react-router/test/base/tests/e2e/specs/dynamic-routes.cy.js
@@ -5,6 +5,11 @@ Fixes bug reported in https://github.com/ionic-team/ionic-framework/issues/21329
*/
describe('Dynamic Routes', () => {
+ it('/dynamic-routes/home loads directly', () => {
+ cy.visit(`http://localhost:${port}/dynamic-routes/home`);
+ cy.ionPageVisible('dynamic-routes-home');
+ });
+
it('/dynamic-routes, when adding a dynamic route, we should be able to navigate to it', () => {
cy.visit(`http://localhost:${port}/dynamic-routes`);
cy.ionPageVisible('dynamic-routes-home');
diff --git a/packages/react-router/test/base/tests/e2e/specs/index-param-priority.cy.js b/packages/react-router/test/base/tests/e2e/specs/index-param-priority.cy.js
new file mode 100644
index 00000000000..ccb07376b5e
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/specs/index-param-priority.cy.js
@@ -0,0 +1,149 @@
+const port = 3000;
+
+describe('Index Param Priority', () => {
+ /*
+ Tests route specificity and priority in an outlet with:
+ (index route)
+ (single-param route)
+ (catch-all wildcard)
+
+ Validates:
+ 1. Index routes are prioritized over parameterized routes in both
+ route creation and view lookup (sort consistency fix).
+ 2. Multi-segment paths match the wildcard, not the single-param route
+ (computeParentPath fix to respect outlet mount path).
+ */
+
+ it('should show the index page on initial visit', () => {
+ cy.visit(`http://localhost:${port}/index-param-priority`);
+ cy.ionPageVisible('index-param-priority-index');
+ cy.get('[data-testid="index-page-label"]').should('contain', 'This is the index page');
+ });
+
+ it('should navigate to slug and back to index via routerLink', () => {
+ cy.visit(`http://localhost:${port}/index-param-priority`);
+ cy.ionPageVisible('index-param-priority-index');
+
+ cy.get('#go-to-slug').click();
+ cy.get('[data-testid="slug-page-label"]').should('contain', 'Slug page: hello');
+
+ cy.get('#back-to-index').click();
+ cy.ionPageVisible('index-param-priority-index');
+ cy.get('[data-testid="index-page-label"]').should('contain', 'This is the index page');
+ });
+
+ it('should navigate to slug and back to index via browser back', () => {
+ cy.visit(`http://localhost:${port}/index-param-priority`);
+ cy.ionPageVisible('index-param-priority-index');
+
+ cy.get('#go-to-slug').click();
+ cy.get('[data-testid="slug-page-label"]').should('contain', 'Slug page: hello');
+
+ cy.go('back');
+ cy.ionPageVisible('index-param-priority-index');
+ cy.get('[data-testid="index-page-label"]').should('contain', 'This is the index page');
+ });
+
+ it('should handle round-trip: index -> slug -> index -> slug -> index', () => {
+ cy.visit(`http://localhost:${port}/index-param-priority`);
+ cy.ionPageVisible('index-param-priority-index');
+
+ cy.get('#go-to-slug').click();
+ cy.get('[data-testid="slug-page-label"]').should('contain', 'Slug page: hello');
+
+ cy.go('back');
+ cy.ionPageVisible('index-param-priority-index');
+
+ cy.get('#go-to-slug-world').click();
+ cy.get('[data-testid="slug-page-label"]').should('contain', 'Slug page: world');
+
+ cy.go('back');
+ cy.ionPageVisible('index-param-priority-index');
+ cy.get('[data-testid="index-page-label"]').should('contain', 'This is the index page');
+ });
+
+ it('should handle browser back through multiple navigations', () => {
+ cy.visit(`http://localhost:${port}/index-param-priority`);
+ cy.ionPageVisible('index-param-priority-index');
+
+ cy.get('#go-to-slug').click();
+ cy.get('[data-testid="slug-page-label"]').should('contain', 'Slug page: hello');
+
+ cy.get('#back-to-index').click();
+ cy.ionPageVisible('index-param-priority-index');
+
+ cy.get('#go-to-slug-world').click();
+ cy.get('[data-testid="slug-page-label"]').should('contain', 'Slug page: world');
+
+ cy.go('back');
+ cy.ionPageVisible('index-param-priority-index');
+ cy.get('[data-testid="index-page-label"]').should('contain', 'This is the index page');
+ });
+
+ it('should handle direct deep link to slug then navigate to index', () => {
+ cy.visit(`http://localhost:${port}/index-param-priority/hello`);
+ cy.get('[data-testid="slug-page-label"]').should('contain', 'Slug page: hello');
+
+ cy.get('#back-to-index').click();
+ cy.ionPageVisible('index-param-priority-index');
+ cy.get('[data-testid="index-page-label"]').should('contain', 'This is the index page');
+ });
+
+ // Wildcard matching tests - multi-segment paths should match * not :slug
+ it('should navigate to wildcard page for multi-segment path and back to index', () => {
+ cy.visit(`http://localhost:${port}/index-param-priority`);
+ cy.ionPageVisible('index-param-priority-index');
+
+ cy.get('#go-to-wildcard').click();
+ cy.get('[data-testid="notfound-page-label"]').should('contain', 'Page not found');
+
+ cy.get('#back-to-index-from-notfound').click();
+ cy.ionPageVisible('index-param-priority-index');
+ cy.get('[data-testid="index-page-label"]').should('contain', 'This is the index page');
+ });
+
+ it('should navigate slug -> index -> wildcard -> index round-trip', () => {
+ cy.visit(`http://localhost:${port}/index-param-priority`);
+ cy.ionPageVisible('index-param-priority-index');
+
+ // Go to slug
+ cy.get('#go-to-slug').click();
+ cy.get('[data-testid="slug-page-label"]').should('contain', 'Slug page: hello');
+
+ // Back to index
+ cy.go('back');
+ cy.ionPageVisible('index-param-priority-index');
+
+ // Go to wildcard path
+ cy.get('#go-to-wildcard').click();
+ cy.get('[data-testid="notfound-page-label"]').should('contain', 'Page not found');
+
+ // Back to index
+ cy.get('#back-to-index-from-notfound').click();
+ cy.ionPageVisible('index-param-priority-index');
+ cy.get('[data-testid="index-page-label"]').should('contain', 'This is the index page');
+ });
+
+ it('should handle browser back from wildcard to index', () => {
+ cy.visit(`http://localhost:${port}/index-param-priority`);
+ cy.ionPageVisible('index-param-priority-index');
+
+ cy.get('#go-to-wildcard').click();
+ cy.get('[data-testid="notfound-page-label"]').should('contain', 'Page not found');
+
+ cy.go('back');
+ cy.ionPageVisible('index-param-priority-index');
+ cy.get('[data-testid="index-page-label"]').should('contain', 'This is the index page');
+ });
+
+ it('should show wildcard page on direct deep-link to multi-segment path', () => {
+ cy.visit(`http://localhost:${port}/index-param-priority/deep/nested/path`);
+ cy.get('[data-testid="notfound-page-label"]').should('contain', 'Page not found');
+
+ cy.get('#back-to-index-from-notfound').click();
+ cy.url().should('include', '/index-param-priority');
+ cy.wait(300);
+ cy.ionPageVisible('index-param-priority-index');
+ cy.get('[data-testid="index-page-label"]').should('contain', 'This is the index page');
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/specs/index-route-reuse.cy.js b/packages/react-router/test/base/tests/e2e/specs/index-route-reuse.cy.js
new file mode 100644
index 00000000000..7ebfa89f98b
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/specs/index-route-reuse.cy.js
@@ -0,0 +1,118 @@
+const port = 3000;
+
+/**
+ * Tests for index route reuse across tabs with nested outlets.
+ *
+ * Validates that switching between tabs where each has its own nested
+ * IonRouterOutlet with an index route correctly shows the right content.
+ * This tests whether createViewItem's index route reuse check
+ * (existingIsIndexRoute && newIsIndexRoute) causes issues when multiple
+ * outlets each have their own index routes.
+ */
+describe('Index Route Reuse - Nested Outlet Index Routes', () => {
+ it('should show tab1 index content by default', () => {
+ cy.visit(`http://localhost:${port}/index-route-reuse`);
+ cy.ionPageVisible('irr-tab1-home');
+ cy.get('[data-testid="irr-tab1-home-content"]').should('be.visible');
+ cy.get('[data-testid="irr-tab1-home-content"]').should('contain', 'Tab 1 Index Route Content');
+ });
+
+ it('should show tab2 index content when switching to tab2', () => {
+ cy.visit(`http://localhost:${port}/index-route-reuse/tab1`);
+ cy.ionPageVisible('irr-tab1-home');
+
+ // Switch to Tab 2
+ cy.ionTabClick('Tab 2');
+ cy.url().should('include', '/index-route-reuse/tab2');
+ cy.ionPageVisible('irr-tab2-home');
+ cy.get('[data-testid="irr-tab2-home-content"]').should('be.visible');
+ cy.get('[data-testid="irr-tab2-home-content"]').should('contain', 'Tab 2 Index Route Content');
+ });
+
+ it('should show tab3 index content when switching to tab3', () => {
+ cy.visit(`http://localhost:${port}/index-route-reuse/tab1`);
+ cy.ionPageVisible('irr-tab1-home');
+
+ // Switch to Tab 3
+ cy.ionTabClick('Tab 3');
+ cy.url().should('include', '/index-route-reuse/tab3');
+ cy.ionPageVisible('irr-tab3-home');
+ cy.get('[data-testid="irr-tab3-home-content"]').should('be.visible');
+ cy.get('[data-testid="irr-tab3-home-content"]').should('contain', 'Tab 3 Index Route Content');
+ });
+
+ it('should correctly show each tab index when cycling through all tabs', () => {
+ cy.visit(`http://localhost:${port}/index-route-reuse/tab1`);
+ cy.ionPageVisible('irr-tab1-home');
+ cy.get('[data-testid="irr-tab1-home-content"]').should('be.visible');
+
+ // Tab 1 -> Tab 2
+ cy.ionTabClick('Tab 2');
+ cy.url().should('include', '/index-route-reuse/tab2');
+ cy.ionPageVisible('irr-tab2-home');
+ cy.get('[data-testid="irr-tab2-home-content"]').should('be.visible');
+ cy.get('[data-testid="irr-tab2-home-content"]').should('contain', 'Tab 2 Index Route Content');
+
+ // Tab 2 -> Tab 3
+ cy.ionTabClick('Tab 3');
+ cy.url().should('include', '/index-route-reuse/tab3');
+ cy.ionPageVisible('irr-tab3-home');
+ cy.get('[data-testid="irr-tab3-home-content"]').should('be.visible');
+ cy.get('[data-testid="irr-tab3-home-content"]').should('contain', 'Tab 3 Index Route Content');
+
+ // Tab 3 -> Tab 1 (back to start)
+ cy.ionTabClick('Tab 1');
+ cy.url().should('include', '/index-route-reuse/tab1');
+ cy.ionPageVisible('irr-tab1-home');
+ cy.get('[data-testid="irr-tab1-home-content"]').should('be.visible');
+ cy.get('[data-testid="irr-tab1-home-content"]').should('contain', 'Tab 1 Index Route Content');
+ });
+
+ it('should preserve tab1 detail navigation and return correctly', () => {
+ cy.visit(`http://localhost:${port}/index-route-reuse/tab1`);
+ cy.ionPageVisible('irr-tab1-home');
+
+ // Navigate to detail page
+ cy.get('#irr-tab1-detail-btn').click();
+ cy.ionPageVisible('irr-tab1-detail');
+ cy.get('[data-testid="irr-tab1-detail-content"]').should('be.visible');
+
+ // Switch to Tab 2
+ cy.ionTabClick('Tab 2');
+ cy.url().should('include', '/index-route-reuse/tab2');
+ cy.ionPageVisible('irr-tab2-home');
+ cy.get('[data-testid="irr-tab2-home-content"]').should('be.visible');
+
+ // Switch back to Tab 1 - should show detail (preserved history)
+ cy.ionTabClick('Tab 1');
+ cy.url().should('include', '/index-route-reuse/tab1');
+ cy.ionPageVisible('irr-tab1-detail');
+ cy.get('[data-testid="irr-tab1-detail-content"]').should('be.visible');
+ });
+
+ it('should show correct content after rapid tab switching', () => {
+ cy.visit(`http://localhost:${port}/index-route-reuse/tab1`);
+ cy.ionPageVisible('irr-tab1-home');
+
+ // Rapid switching: Tab1 -> Tab2 -> Tab3 -> Tab2 -> Tab1
+ cy.ionTabClick('Tab 2');
+ cy.url().should('include', '/index-route-reuse/tab2');
+ cy.ionPageVisible('irr-tab2-home');
+
+ cy.ionTabClick('Tab 3');
+ cy.url().should('include', '/index-route-reuse/tab3');
+ cy.ionPageVisible('irr-tab3-home');
+
+ cy.ionTabClick('Tab 2');
+ cy.url().should('include', '/index-route-reuse/tab2');
+ cy.ionPageVisible('irr-tab2-home');
+ cy.get('[data-testid="irr-tab2-home-content"]').should('be.visible');
+ cy.get('[data-testid="irr-tab2-home-content"]').should('contain', 'Tab 2 Index Route Content');
+
+ cy.ionTabClick('Tab 1');
+ cy.url().should('include', '/index-route-reuse/tab1');
+ cy.ionPageVisible('irr-tab1-home');
+ cy.get('[data-testid="irr-tab1-home-content"]').should('be.visible');
+ cy.get('[data-testid="irr-tab1-home-content"]').should('contain', 'Tab 1 Index Route Content');
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/specs/ion-route-props.cy.js b/packages/react-router/test/base/tests/e2e/specs/ion-route-props.cy.js
new file mode 100644
index 00000000000..bfd0409eea3
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/specs/ion-route-props.cy.js
@@ -0,0 +1,42 @@
+const port = 3000;
+
+describe('IonRoute Props', () => {
+ /**
+ * Tests that IonRoute correctly forwards index and caseSensitive props.
+ *
+ * Previously, IonRouteInner only destructured path and element,
+ * silently dropping index and caseSensitive props.
+ */
+
+ describe('index prop', () => {
+ it('should render the index route when navigating to parent path', () => {
+ cy.visit(`http://localhost:${port}/ion-route-props`);
+ cy.ionPageVisible('index-home');
+ cy.get('#index-home-text').should('be.visible').and('contain', 'This is the index route');
+ });
+
+ it('should navigate from index route to details and back', () => {
+ cy.visit(`http://localhost:${port}/ion-route-props`);
+ cy.ionPageVisible('index-home');
+ cy.ionNav('ion-button', 'Go to Details');
+ cy.ionPageVisible('ion-route-details');
+ cy.get('#details-text').should('be.visible');
+ cy.ionNav('ion-button', 'Back to Home');
+ cy.ionPageVisible('index-home');
+ });
+ });
+
+ describe('caseSensitive prop', () => {
+ it('should match the exact case path', () => {
+ cy.visit(`http://localhost:${port}/ion-route-props/CaseSensitive`);
+ cy.ionPageVisible('case-sensitive-page');
+ cy.get('#case-sensitive-text').should('be.visible');
+ });
+
+ it('should not match a different case path when caseSensitive is true', () => {
+ cy.visit(`http://localhost:${port}/ion-route-props/casesensitive`);
+ // The case-sensitive page should NOT be visible since the case doesn't match
+ cy.get('[data-pageid="case-sensitive-page"]').should('not.exist');
+ });
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/specs/modal-aria-hidden.cy.js b/packages/react-router/test/base/tests/e2e/specs/modal-aria-hidden.cy.js
new file mode 100644
index 00000000000..b5983b984ae
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/specs/modal-aria-hidden.cy.js
@@ -0,0 +1,39 @@
+const port = 3000;
+
+/**
+ * Tests that aria-hidden is properly cleaned up on the root
+ * ion-router-outlet when a modal is auto-removed during navigation
+ * (i.e., removed without being explicitly dismissed).
+ *
+ * When a modal is presented, core sets aria-hidden="true" on the
+ * root ion-router-outlet to hide background content from screen readers.
+ * If the modal is dismissed via dismiss(), core cleans up aria-hidden.
+ * But if the modal is removed from the DOM by a framework (e.g., React
+ * unmounting during a route change), dismiss() is never called, so
+ * aria-hidden must be cleaned up via the modal's disconnectedCallback.
+ */
+describe('Modal Aria Hidden Cleanup', () => {
+ it('should not leave aria-hidden on root outlet when modal is removed via navigation', () => {
+ cy.visit(`http://localhost:${port}/modal-aria-hidden`);
+ cy.ionPageVisible('modal-page-a');
+
+ // Open the modal and wait for it to be visible
+ cy.get('#openModal').click();
+ cy.get('ion-modal').should('be.visible');
+
+ // The root outlet should have aria-hidden while modal is open
+ cy.get('ion-router-outlet').first().should('have.attr', 'aria-hidden', 'true');
+
+ // Navigate to section B without dismissing the modal
+ cy.get('#navigateToB').should('be.visible').click();
+
+ // Wait for navigation to complete
+ cy.ionPageVisible('modal-page-b');
+
+ // The modal should be removed from the DOM
+ cy.get('ion-modal').should('not.exist');
+
+ // The root outlet should NOT have aria-hidden anymore
+ cy.get('ion-router-outlet').first().should('not.have.attr', 'aria-hidden');
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/specs/multi-step-back.cy.js b/packages/react-router/test/base/tests/e2e/specs/multi-step-back.cy.js
new file mode 100644
index 00000000000..f4301be7bda
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/specs/multi-step-back.cy.js
@@ -0,0 +1,94 @@
+const port = 3000;
+
+/**
+ * Tests for navigate(-n) where n > 1 (multi-step back navigation).
+ * Verifies that the correct view is shown when skipping multiple
+ * entries in the history stack.
+ *
+ * @see https://github.com/ionic-team/ionic-framework/issues/23775
+ */
+describe('Multi-Step Back Navigation', () => {
+
+ it('A > B > C > navigate(-2) should show Page A', () => {
+ cy.visit(`http://localhost:${port}/multi-step-back/a`);
+ cy.ionPageVisible('multi-step-page-a');
+
+ // Navigate A -> B -> C
+ cy.ionNav('ion-button#go-to-b', 'Go to Page B');
+ cy.ionPageVisible('multi-step-page-b');
+
+ cy.ionNav('ion-button#go-to-c', 'Go to Page C');
+ cy.ionPageVisible('multi-step-page-c');
+
+ // navigate(-2) should go back to Page A, skipping Page B
+ cy.ionNav('ion-button#page-c-go-back-2', 'Go Back 2 (to A)');
+ cy.ionPageVisible('multi-step-page-a');
+ cy.url().should('include', '/multi-step-back/a');
+ });
+
+ it('A > B > C > D > navigate(-2) should show Page B', () => {
+ cy.visit(`http://localhost:${port}/multi-step-back/a`);
+ cy.ionPageVisible('multi-step-page-a');
+
+ // Navigate A -> B -> C -> D
+ cy.ionNav('ion-button#go-to-b', 'Go to Page B');
+ cy.ionPageVisible('multi-step-page-b');
+
+ cy.ionNav('ion-button#go-to-c', 'Go to Page C');
+ cy.ionPageVisible('multi-step-page-c');
+
+ cy.ionNav('ion-button#go-to-d', 'Go to Page D');
+ cy.ionPageVisible('multi-step-page-d');
+
+ // navigate(-2) from D should show Page B
+ cy.ionNav('ion-button#page-d-go-back-2', 'Go Back 2 (to B)');
+ cy.ionPageVisible('multi-step-page-b');
+ cy.url().should('include', '/multi-step-back/b');
+ });
+
+ it('A > B > C > D > navigate(-3) should show Page A', () => {
+ cy.visit(`http://localhost:${port}/multi-step-back/a`);
+ cy.ionPageVisible('multi-step-page-a');
+
+ // Navigate A -> B -> C -> D
+ cy.ionNav('ion-button#go-to-b', 'Go to Page B');
+ cy.ionPageVisible('multi-step-page-b');
+
+ cy.ionNav('ion-button#go-to-c', 'Go to Page C');
+ cy.ionPageVisible('multi-step-page-c');
+
+ cy.ionNav('ion-button#go-to-d', 'Go to Page D');
+ cy.ionPageVisible('multi-step-page-d');
+
+ // navigate(-3) from D should show Page A
+ cy.ionNav('ion-button#page-d-go-back-3', 'Go Back 3 (to A)');
+ cy.ionPageVisible('multi-step-page-a');
+ cy.url().should('include', '/multi-step-back/a');
+ });
+
+ it('A > B > C > navigate(-2) > Browser Forward should show Page B', () => {
+ cy.visit(`http://localhost:${port}/multi-step-back/a`);
+ cy.ionPageVisible('multi-step-page-a');
+
+ // Navigate A -> B -> C
+ cy.ionNav('ion-button#go-to-b', 'Go to Page B');
+ cy.ionPageVisible('multi-step-page-b');
+
+ cy.ionNav('ion-button#go-to-c', 'Go to Page C');
+ cy.ionPageVisible('multi-step-page-c');
+
+ // navigate(-2) back to Page A
+ cy.ionNav('ion-button#page-c-go-back-2', 'Go Back 2 (to A)');
+ cy.ionPageVisible('multi-step-page-a');
+ cy.url().should('include', '/multi-step-back/a');
+
+ // Browser forward should go to Page B (one step forward from A)
+ cy.ionGoForward('/multi-step-back/b');
+ cy.ionPageVisible('multi-step-page-b');
+
+ // A second forward step from B should reach Page C
+ cy.ionGoForward('/multi-step-back/c');
+ cy.ionPageVisible('multi-step-page-c');
+ });
+
+});
diff --git a/packages/react-router/test/base/tests/e2e/specs/nested-outlets.cy.js b/packages/react-router/test/base/tests/e2e/specs/nested-outlets.cy.js
index d43d2cc64d5..2b4c06d14ce 100644
--- a/packages/react-router/test/base/tests/e2e/specs/nested-outlets.cy.js
+++ b/packages/react-router/test/base/tests/e2e/specs/nested-outlets.cy.js
@@ -83,12 +83,7 @@ describe('Nested Outlets 2', () => {
cy.ionPageVisible('list');
});
- it(`/nested-outlet2 >
- Go to Welcome IonItem click >
- Go to list from Welcome IonItem click >
- Item#1 IonItem Click >
- Item page should be visible
-`, () => {
+ it('/nested-outlet2 > Go to Welcome IonItem click > Go to list from Welcome IonItem click > Item#1 IonItem Click > Item page should be visible', () => {
cy.visit(`http://localhost:${port}/nested-outlet2`);
cy.ionPageVisible('home');
cy.ionNav('ion-item', 'Go to Welcome');
@@ -99,13 +94,7 @@ describe('Nested Outlets 2', () => {
cy.ionPageVisible('item');
});
- it(`/nested-outlet2 >
- Go to list from Home IonItem click >
- Item#1 IonItem Click >
- Item page should be visible >
- Back >
- List page should be visible
-`, () => {
+ it('/nested-outlet2 > Go to list from Home IonItem click > Item#1 IonItem Click > Item page should be visible > Back > List page should be visible', () => {
cy.visit(`http://localhost:${port}/nested-outlet2`);
cy.ionPageVisible('home');
cy.ionNav('ion-item', 'Go to list from Home');
@@ -116,15 +105,7 @@ describe('Nested Outlets 2', () => {
cy.ionPageVisible('list');
});
- it(`/nested-outlet2 >
- Go to list from Home IonItem click >
- Item#1 IonItem Click >
- Item page should be visible >
- Back >
- List page should be visible
- Back >
- Home page should be visible
-`, () => {
+ it('/nested-outlet2 > Go to list from Home IonItem click > Item#1 IonItem Click > Item page should be visible > Back > List page should be visible > Back > Home page should be visible', () => {
cy.visit(`http://localhost:${port}/nested-outlet2`);
cy.ionPageVisible('home');
cy.ionNav('ion-item', 'Go to list from Home');
@@ -136,4 +117,30 @@ describe('Nested Outlets 2', () => {
cy.ionBackClick('list');
cy.ionPageVisible('home');
});
+
+ it('/nested-outlet2 > Browser back/forward across nested outlets should show correct page', () => {
+ cy.visit(`http://localhost:${port}/nested-outlet2`);
+ cy.ionPageVisible('home');
+
+ // Navigate: Home β Welcome β List β Item #1
+ cy.ionNav('ion-item', 'Go to Welcome');
+ cy.ionPageVisible('welcome');
+ cy.ionNav('ion-item', 'Go to list from Welcome');
+ cy.ionPageVisible('list');
+ cy.ionNav('ion-item', 'Item #1');
+ cy.ionPageVisible('item');
+ cy.location('pathname').should('eq', '/nested-outlet2/list/1');
+
+ // Browser back: Item β List
+ cy.go('back');
+ cy.ionPageVisible('list');
+ cy.location('pathname').should('eq', '/nested-outlet2/list');
+
+ // Browser forward: List β Item (this was showing Welcome page before the fix)
+ cy.go('forward');
+ cy.wait(500);
+ cy.ionPageVisible('item');
+ cy.location('pathname').should('eq', '/nested-outlet2/list/1');
+ cy.ionPageDoesNotExist('welcome');
+ });
});
diff --git a/packages/react-router/test/base/tests/e2e/specs/nested-params.cy.js b/packages/react-router/test/base/tests/e2e/specs/nested-params.cy.js
new file mode 100644
index 00000000000..1b3f84189b3
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/specs/nested-params.cy.js
@@ -0,0 +1,392 @@
+const port = 3000;
+
+describe('Nested Params', () => {
+ /*
+ Tests that route params are correctly passed to nested routes
+ when using parameterized wildcard routes (e.g., user/:userId/*).
+ */
+
+ it('/nested-params > Landing page should be visible', () => {
+ cy.visit(`http://localhost:${port}/nested-params`);
+ cy.ionPageVisible('nested-params-landing');
+ });
+
+ it('/nested-params > Navigate to user details > Params should be available', () => {
+ cy.visit(`http://localhost:${port}/nested-params`);
+ cy.ionPageVisible('nested-params-landing');
+
+ cy.get('#go-to-user-42').click();
+
+ cy.get('[data-testid="user-layout-param"]').should('contain', 'Layout sees user: 42');
+ cy.get('[data-testid="user-details-param"]').should('contain', 'Details view user: 42');
+ });
+
+ it('/nested-params > Navigate between sibling routes > Params should be maintained', () => {
+ cy.visit(`http://localhost:${port}/nested-params`);
+ cy.ionPageVisible('nested-params-landing');
+
+ // Navigate to user 42 details
+ cy.get('#go-to-user-42').click();
+ cy.get('[data-testid="user-layout-param"]').should('contain', 'Layout sees user: 42');
+ cy.get('[data-testid="user-details-param"]').should('contain', 'Details view user: 42');
+
+ // Navigate to settings (sibling route)
+ cy.get('#go-to-settings').click();
+ cy.get('[data-testid="user-layout-param"]').should('contain', 'Layout sees user: 42');
+ cy.get('[data-testid="user-settings-param"]').should('contain', 'Settings view user: 42');
+
+ // Navigate back to details
+ cy.contains('ion-button', 'Back to Details').click();
+ cy.get('[data-testid="user-layout-param"]').should('contain', 'Layout sees user: 42');
+ cy.get('[data-testid="user-details-param"]').should('contain', 'Details view user: 42');
+ });
+
+ it('/nested-params > Direct navigation to nested route > Params should be available', () => {
+ // Navigate directly to a nested route with params
+ cy.visit(`http://localhost:${port}/nested-params/user/123/settings`);
+
+ cy.get('[data-testid="user-layout-param"]').should('contain', 'Layout sees user: 123');
+ cy.get('[data-testid="user-settings-param"]').should('contain', 'Settings view user: 123');
+ });
+
+ it('/nested-params > Deep link to child then navigate to landing > Landing should show correctly', () => {
+ // Deep-link directly to a nested child route
+ cy.visit(`http://localhost:${port}/nested-params/user/42/details`);
+ cy.get('[data-testid="user-details-param"]').should('contain', 'Details view user: 42');
+
+ // Navigate back to the landing (index route)
+ cy.get('#back-to-landing').click();
+ cy.ionPageVisible('nested-params-landing');
+
+ // The user layout page should be hidden (Ionic preserves it in DOM for back navigation)
+ cy.get('[data-pageid^="nested-params-user-"]').should('have.class', 'ion-page-hidden');
+ });
+
+ it('/nested-params > Deep link then navigate to landing and back > Round-trip should work', () => {
+ // Deep-link directly to a nested child route
+ cy.visit(`http://localhost:${port}/nested-params/user/42/details`);
+ cy.get('[data-testid="user-details-param"]').should('contain', 'Details view user: 42');
+
+ // Navigate to the landing page
+ cy.get('#back-to-landing').click();
+ cy.ionPageVisible('nested-params-landing');
+
+ // Navigate back to user details
+ cy.get('#go-to-user-42').click();
+ cy.get('[data-testid="user-details-param"]').should('contain', 'Details view user: 42');
+ cy.get('[data-testid="user-layout-param"]').should('contain', 'Layout sees user: 42');
+ });
+
+ it('/nested-params > Navigate to user then back > No visual overlap during transition', () => {
+ cy.visit(`http://localhost:${port}/nested-params`);
+ cy.ionPageVisible('nested-params-landing');
+
+ // Navigate to user 99
+ cy.get('#go-to-user-99').click();
+ cy.get('[data-testid="user-layout-param"]').should('contain', 'Layout sees user: 99');
+
+ // Install an overlap detector that runs every animation frame.
+ // It checks whether the landing page and user page IonPage elements are
+ // simultaneously visible β which is the root cause of the overlap bug.
+ // We check the IonPage elements directly (not descendants) because CSS
+ // display is not inherited β a child's computed display can be 'block'
+ // even when a parent has display:none.
+ cy.window().then((win) => {
+ win.__overlapDetected = false;
+ const check = () => {
+ const landing = win.document.querySelector('[data-pageid="nested-params-landing"]');
+ const userPage = win.document.querySelector('[data-pageid^="nested-params-user-"]');
+ if (landing && userPage) {
+ const landingStyle = win.getComputedStyle(landing);
+ const userStyle = win.getComputedStyle(userPage);
+ const landingVisible = landingStyle.display !== 'none' && landingStyle.visibility !== 'hidden' && landingStyle.opacity !== '0';
+ const userVisible = userStyle.display !== 'none' && userStyle.visibility !== 'hidden' && userStyle.opacity !== '0';
+ if (landingVisible && userVisible) {
+ win.__overlapDetected = true;
+ }
+ }
+ if (!win.__overlapCheckDone) {
+ requestAnimationFrame(check);
+ }
+ };
+ requestAnimationFrame(check);
+ });
+
+ // Go back to landing
+ cy.go('back');
+ cy.ionPageVisible('nested-params-landing');
+ cy.get('[data-testid="user-layout-param"]').should('not.exist');
+
+ // Stop the observer and verify no overlap was detected
+ cy.window().then((win) => {
+ win.__overlapCheckDone = true;
+ expect(win.__overlapDetected).to.be.false;
+ });
+ });
+
+ it('/nested-params > Navigate to user, back, forward, back > Pages should not persist', () => {
+ cy.visit(`http://localhost:${port}/nested-params`);
+ cy.ionPageVisible('nested-params-landing');
+
+ // Navigate to user 99
+ cy.get('#go-to-user-99').click();
+ cy.get('[data-testid="user-layout-param"]').should('contain', 'Layout sees user: 99');
+
+ // Go back
+ cy.go('back');
+ cy.ionPageVisible('nested-params-landing');
+
+ // Go forward β wait for async POP event processing and transition to settle
+ cy.go('forward');
+ cy.wait(500);
+ cy.get('[data-testid="user-layout-param"]').should('contain', 'Layout sees user: 99');
+
+ // Go back again
+ cy.go('back');
+ cy.ionPageVisible('nested-params-landing');
+ cy.get('[data-testid="user-layout-param"]').should('not.exist');
+ });
+
+ it('/nested-params > Different users should have different params', () => {
+ cy.visit(`http://localhost:${port}/nested-params`);
+ cy.ionPageVisible('nested-params-landing');
+
+ // Navigate to user 42
+ cy.get('#go-to-user-42').click();
+ cy.get('[data-testid="user-layout-param"]').should('contain', 'Layout sees user: 42');
+ cy.get('[data-testid="user-details-param"]').should('contain', 'Details view user: 42');
+
+ // Go back to landing
+ cy.go('back');
+ cy.ionPageVisible('nested-params-landing');
+
+ // Navigate to user 99
+ cy.get('#go-to-user-99').click();
+ cy.get('[data-testid="user-layout-param"]').should('contain', 'Layout sees user: 99');
+ cy.get('[data-testid="user-details-param"]').should('contain', 'Details view user: 99');
+ });
+
+ it('/nested-params > Full back navigation from sibling routes to root > Root page should show', () => {
+ // Start at root
+ cy.visit(`http://localhost:${port}/`);
+ cy.ionPageVisible('home');
+
+ // Navigate to nested-params
+ cy.contains('ion-item', 'Nested Params').click();
+ cy.ionPageVisible('nested-params-landing');
+
+ // Navigate to user 99 details
+ cy.get('#go-to-user-99').click();
+ cy.get('[data-testid="user-layout-param"]').should('contain', 'Layout sees user: 99');
+ cy.get('[data-testid="user-details-param"]').should('contain', 'Details view user: 99');
+
+ // Go to settings
+ cy.get('#go-to-settings').click();
+ cy.get('[data-testid="user-settings-param"]').should('contain', 'Settings view user: 99');
+
+ // Hit "Back to Details" (this is a forward push via routerLink)
+ cy.contains('ion-button', 'Back to Details').click();
+ cy.get('[data-testid="user-details-param"]').should('contain', 'Details view user: 99');
+
+ // Browser back repeatedly to root
+ // Back 1: details -> settings
+ cy.go('back');
+ cy.get('[data-testid="user-settings-param"]').should('contain', 'Settings view user: 99');
+
+ // Back 2: settings -> details
+ cy.go('back');
+ cy.get('[data-testid="user-details-param"]').should('contain', 'Details view user: 99');
+
+ // Back 3: details -> nested-params landing
+ cy.go('back');
+ cy.ionPageVisible('nested-params-landing');
+
+ // Back 4: nested-params -> root
+ cy.go('back');
+ cy.ionPageVisible('home');
+ cy.get('[data-testid="user-layout-param"]').should('not.exist');
+ });
+
+ it('/nested-params > Sibling route transitions (details <-> settings) > No visual overlap', () => {
+ // Start directly on the details page
+ cy.visit(`http://localhost:${port}/nested-params/user/99/details`);
+ cy.get('[data-testid="user-details-param"]').should('contain', 'Details view user: 99');
+
+ // Install overlap detector that checks whether details and settings IonPage
+ // elements are simultaneously visible inside the nested outlet.
+ cy.window().then((win) => {
+ win.__siblingOverlapDetected = false;
+ const check = () => {
+ const details = win.document.querySelector('[data-pageid="nested-params-details"]');
+ const settings = win.document.querySelector('[data-pageid="nested-params-settings"]');
+ if (details && settings) {
+ const detailsStyle = win.getComputedStyle(details);
+ const settingsStyle = win.getComputedStyle(settings);
+ const detailsVisible = detailsStyle.display !== 'none' && detailsStyle.visibility !== 'hidden' && detailsStyle.opacity !== '0';
+ const settingsVisible = settingsStyle.display !== 'none' && settingsStyle.visibility !== 'hidden' && settingsStyle.opacity !== '0';
+ if (detailsVisible && settingsVisible) {
+ win.__siblingOverlapDetected = true;
+ }
+ }
+ if (!win.__siblingOverlapCheckDone) {
+ requestAnimationFrame(check);
+ }
+ };
+ requestAnimationFrame(check);
+ });
+
+ // Navigate details -> settings
+ cy.get('#go-to-settings').click();
+ cy.get('[data-testid="user-settings-param"]').should('contain', 'Settings view user: 99');
+
+ // Navigate settings -> details
+ cy.contains('ion-button', 'Back to Details').click();
+ cy.get('[data-testid="user-details-param"]').should('contain', 'Details view user: 99');
+
+ // Stop the observer and verify no overlap was detected in either direction
+ cy.window().then((win) => {
+ win.__siblingOverlapCheckDone = true;
+ expect(win.__siblingOverlapDetected).to.be.false;
+ });
+ });
+
+ /**
+ * Deep nesting tests (4 levels) to validate derivePathnameToMatch
+ * handles relative paths correctly at depth > 2 parent segments.
+ *
+ * Route structure:
+ * /nested-params/* (App level)
+ * user/:userId/* (NestedParamsRoot outlet)
+ * details (UserLayout outlet)
+ * settings (UserLayout outlet)
+ * profile/* (UserLayout outlet)
+ * edit (ProfileLayout outlet - 4th level)
+ * view (ProfileLayout outlet - 4th level)
+ */
+
+ it('/nested-params > Deep nesting > Navigate to profile edit (4 levels deep)', () => {
+ cy.visit(`http://localhost:${port}/nested-params`);
+ cy.ionPageVisible('nested-params-landing');
+
+ // Navigate to user 42 details
+ cy.get('#go-to-user-42').click();
+ cy.get('[data-testid="user-details-param"]').should('contain', 'Details view user: 42');
+
+ // Navigate to profile edit (4th level)
+ cy.get('#go-to-profile-edit').click();
+ cy.get('[data-testid="profile-layout-param"]').should('contain', 'Profile layout user: 42');
+ cy.get('[data-testid="profile-edit-param"]').should('contain', 'Profile edit user: 42');
+ cy.location('pathname').should('eq', '/nested-params/user/42/profile/edit');
+ });
+
+ it('/nested-params > Deep nesting > Direct navigation to 4th level route', () => {
+ // Navigate directly to a deeply nested route
+ cy.visit(`http://localhost:${port}/nested-params/user/42/profile/edit`);
+
+ cy.get('[data-testid="profile-layout-param"]').should('contain', 'Profile layout user: 42');
+ cy.get('[data-testid="profile-edit-param"]').should('contain', 'Profile edit user: 42');
+ });
+
+ it('/nested-params > Deep nesting > Navigate between 4th level siblings (edit <-> view)', () => {
+ cy.visit(`http://localhost:${port}/nested-params/user/42/profile/edit`);
+ cy.get('[data-testid="profile-edit-param"]').should('contain', 'Profile edit user: 42');
+
+ // Navigate to profile view (sibling at 4th level)
+ cy.get('#go-to-profile-view').click();
+ cy.get('[data-testid="profile-view-param"]').should('contain', 'Profile view user: 42');
+ cy.location('pathname').should('eq', '/nested-params/user/42/profile/view');
+
+ // Navigate back to profile edit
+ cy.get('#back-to-profile-edit').click();
+ cy.get('[data-testid="profile-edit-param"]').should('contain', 'Profile edit user: 42');
+ cy.location('pathname').should('eq', '/nested-params/user/42/profile/edit');
+ });
+
+ it('/nested-params > Deep nesting > Back navigation from 4th level to root', () => {
+ cy.visit(`http://localhost:${port}/`);
+ cy.ionPageVisible('home');
+
+ // Navigate to nested-params
+ cy.contains('ion-item', 'Nested Params').click();
+ cy.ionPageVisible('nested-params-landing');
+
+ // Navigate to user 42 details
+ cy.get('#go-to-user-42').click();
+ cy.get('[data-testid="user-details-param"]').should('contain', 'Details view user: 42');
+
+ // Navigate to profile edit (4th level)
+ cy.get('#go-to-profile-edit').click();
+ cy.get('[data-testid="profile-edit-param"]').should('contain', 'Profile edit user: 42');
+
+ // Navigate to profile view (sibling at 4th level)
+ cy.get('#go-to-profile-view').click();
+ cy.get('[data-testid="profile-view-param"]').should('contain', 'Profile view user: 42');
+
+ // Browser back: view -> edit
+ cy.go('back');
+ cy.get('[data-testid="profile-edit-param"]').should('contain', 'Profile edit user: 42');
+
+ // Browser back: edit -> details
+ cy.go('back');
+ cy.get('[data-testid="user-details-param"]').should('contain', 'Details view user: 42');
+
+ // Browser back: details -> landing
+ cy.go('back');
+ cy.ionPageVisible('nested-params-landing');
+
+ // Browser back: landing -> home
+ cy.go('back');
+ cy.ionPageVisible('home');
+ });
+
+ it('/nested-params > Sibling route transitions > No nested scrollbars during transition', () => {
+ // Start directly on the details page
+ cy.visit(`http://localhost:${port}/nested-params/user/99/details`);
+ cy.get('[data-testid="user-details-param"]').should('contain', 'Details view user: 99');
+
+ // Install a detector that checks for multiple visible ion-content elements
+ // inside the nested outlet. Multiple visible ion-content elements cause
+ // nested scrollbars during transitions.
+ cy.window().then((win) => {
+ win.__nestedScrollbarsDetected = false;
+ const check = () => {
+ const outlet = win.document.querySelector('#nested-params-user-outlet');
+ if (outlet) {
+ const contents = outlet.querySelectorAll('ion-content');
+ let visibleCount = 0;
+ contents.forEach((content) => {
+ // Check the parent IonPage element's visibility
+ const page = content.closest('.ion-page');
+ if (page) {
+ const style = win.getComputedStyle(page);
+ if (style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0') {
+ visibleCount++;
+ }
+ }
+ });
+ if (visibleCount > 1) {
+ win.__nestedScrollbarsDetected = true;
+ }
+ }
+ if (!win.__nestedScrollbarsCheckDone) {
+ requestAnimationFrame(check);
+ }
+ };
+ requestAnimationFrame(check);
+ });
+
+ // Navigate details -> settings
+ cy.get('#go-to-settings').click();
+ cy.get('[data-testid="user-settings-param"]').should('contain', 'Settings view user: 99');
+
+ // Navigate settings -> details
+ cy.contains('ion-button', 'Back to Details').click();
+ cy.get('[data-testid="user-details-param"]').should('contain', 'Details view user: 99');
+
+ // Stop the observer and verify no nested scrollbars were detected
+ cy.window().then((win) => {
+ win.__nestedScrollbarsCheckDone = true;
+ expect(win.__nestedScrollbarsDetected).to.be.false;
+ });
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/specs/nested-tabs-relative-links.cy.js b/packages/react-router/test/base/tests/e2e/specs/nested-tabs-relative-links.cy.js
new file mode 100644
index 00000000000..563b45a5ca4
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/specs/nested-tabs-relative-links.cy.js
@@ -0,0 +1,156 @@
+const port = 3000;
+
+/**
+ * Tests for relative links within nested IonRouterOutlet components.
+ *
+ * This specifically tests the scenario where:
+ * 1. IonRouterOutlet has a catch-all route (*) containing IonTabs
+ * 2. Inside tabs, there's another outlet with nested routes using index routes
+ * 3. React Router's is used for navigation
+ *
+ * The expected behavior is:
+ * - at /nested-tabs-relative-links/tab1 should produce
+ * href="/nested-tabs-relative-links/tab1/page-a" (not /tab1/tab1/page-a)
+ * - should work and not 404
+ */
+describe('Nested Tabs with Relative Links', () => {
+ it('should navigate to tab1 by default', () => {
+ cy.visit(`http://localhost:${port}/nested-tabs-relative-links`);
+ cy.ionPageVisible('nested-tabs-relative-tab1');
+ cy.get('[data-testid="tab1-content"]').should('exist');
+ });
+
+ it('should have correct href for relative link', () => {
+ cy.visit(`http://localhost:${port}/nested-tabs-relative-links/tab1`);
+ cy.ionPageVisible('nested-tabs-relative-tab1');
+
+ // Check that the relative link has the correct href
+ // It should be /nested-tabs-relative-links/tab1/page-a, NOT /tab1/tab1/page-a
+ cy.get('[data-testid="link-relative-page-a"]')
+ .should('have.attr', 'href', '/nested-tabs-relative-links/tab1/page-a');
+ });
+
+ it('should navigate to Page A via relative link', () => {
+ cy.visit(`http://localhost:${port}/nested-tabs-relative-links/tab1`);
+ cy.ionPageVisible('nested-tabs-relative-tab1');
+
+ // Click the relative link
+ cy.get('[data-testid="link-relative-page-a"]').click();
+
+ // Should be at Page A
+ cy.ionPageVisible('nested-tabs-relative-page-a');
+ cy.get('[data-testid="page-a-content"]').should('exist');
+
+ // URL should be correct
+ cy.url().should('include', '/nested-tabs-relative-links/tab1/page-a');
+ // URL should NOT have duplicate path segments
+ cy.url().should('not.include', '/tab1/tab1/');
+ });
+
+ it('should navigate to Page A via absolute link', () => {
+ cy.visit(`http://localhost:${port}/nested-tabs-relative-links/tab1`);
+ cy.ionPageVisible('nested-tabs-relative-tab1');
+
+ // Click the absolute link
+ cy.get('[data-testid="link-absolute-page-a"]').click();
+
+ // Should be at Page A (not 404)
+ cy.ionPageVisible('nested-tabs-relative-page-a');
+ cy.get('[data-testid="page-a-content"]').should('exist');
+
+ // Should NOT show 404
+ cy.get('[data-testid="not-found"]').should('not.exist');
+ });
+
+ it('should navigate to Page B via relative link', () => {
+ cy.visit(`http://localhost:${port}/nested-tabs-relative-links/tab1`);
+ cy.ionPageVisible('nested-tabs-relative-tab1');
+
+ // Click the relative link to page B
+ cy.get('[data-testid="link-relative-page-b"]').click();
+
+ // Should be at Page B
+ cy.ionPageVisible('nested-tabs-relative-page-b');
+ cy.get('[data-testid="page-b-content"]').should('exist');
+
+ // URL should be correct
+ cy.url().should('include', '/nested-tabs-relative-links/tab1/page-b');
+ });
+
+ it('should navigate to Page A and back', () => {
+ cy.visit(`http://localhost:${port}/nested-tabs-relative-links/tab1`);
+ cy.ionPageVisible('nested-tabs-relative-tab1');
+
+ // Navigate to Page A
+ cy.get('[data-testid="link-relative-page-a"]').click();
+ cy.ionPageVisible('nested-tabs-relative-page-a');
+
+ // Go back
+ cy.ionBackClick('nested-tabs-relative-page-a');
+
+ // Should be back at Tab 1
+ cy.ionPageVisible('nested-tabs-relative-tab1');
+ });
+
+ it('should directly visit Page A via URL', () => {
+ cy.visit(`http://localhost:${port}/nested-tabs-relative-links/tab1/page-a`);
+
+ // Should be at Page A (not 404)
+ cy.ionPageVisible('nested-tabs-relative-page-a');
+ cy.get('[data-testid="page-a-content"]').should('exist');
+ });
+
+ /**
+ * This test navigates from the home page (/) to nested-tabs-relative-links
+ * via routerLink, rather than visiting the URL directly.
+ *
+ * This is a distinct scenario because NestedTabsRelativeLinks is a container
+ * route that does NOT wrap its content in IonPage β it renders IonRouterOutlet
+ * directly. When the root outlet transitions to a container without IonPage,
+ * the nested redirect fires before the root
+ * outlet's timeout, causing the leaving view reference to shift. Without
+ * proper handling, the home page is never hidden.
+ */
+ it('should navigate from home page to nested tabs', () => {
+ cy.visit(`http://localhost:${port}/`);
+ cy.ionPageVisible('home');
+
+ // Click the Nested Tabs Relative Links item
+ cy.get('ion-item[router-link="/nested-tabs-relative-links"]').click();
+
+ // Should navigate to tab1 (via index redirect)
+ cy.ionPageVisible('nested-tabs-relative-tab1');
+ cy.get('[data-testid="tab1-content"]').should('exist');
+
+ // Home page should be hidden
+ cy.ionPageHidden('home');
+
+ // URL should be correct
+ cy.url().should('include', '/nested-tabs-relative-links/tab1');
+
+ // Verify the container route does NOT have an IonPage wrapper.
+ // This is the key invariant: NestedTabsRelativeLinks renders IonRouterOutlet
+ // directly without IonPage. If someone adds IonPage to the container, this
+ // test would pass trivially and no longer cover the container-without-IonPage
+ // transition path. The ion-tabs element should have no [data-pageid] ancestors,
+ // confirming no IonPage wraps the container.
+ cy.get('ion-tabs').parents('[data-pageid]').should('have.length', 0);
+ });
+
+ it('should switch tabs and maintain correct relative link resolution', () => {
+ cy.visit(`http://localhost:${port}/nested-tabs-relative-links/tab1`);
+ cy.ionPageVisible('nested-tabs-relative-tab1');
+
+ // Switch to Tab 2
+ cy.ionTabClick('Tab 2');
+ cy.ionPageVisible('nested-tabs-relative-tab2');
+
+ // Switch back to Tab 1
+ cy.ionTabClick('Tab 1');
+ cy.ionPageVisible('nested-tabs-relative-tab1');
+
+ // The relative link should still have correct href
+ cy.get('[data-testid="link-relative-page-a"]')
+ .should('have.attr', 'href', '/nested-tabs-relative-links/tab1/page-a');
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/specs/nonlinear-forward.cy.js b/packages/react-router/test/base/tests/e2e/specs/nonlinear-forward.cy.js
new file mode 100644
index 00000000000..6a01a957858
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/specs/nonlinear-forward.cy.js
@@ -0,0 +1,100 @@
+const port = 3000;
+
+describe('Non-linear POP Forward Navigation', () => {
+ // Tests browser forward after a non-linear POP (back from a route without pushedByRoute).
+
+ it('Home > D1 > Settings Tab > Browser Back > Browser Forward, should correctly detect forward after non-linear POP back', () => {
+ cy.visit(`http://localhost:${port}/routing/tabs/home`);
+ cy.ionPageVisible('home-page');
+
+ // Navigate to Details 1 (PUSH - creates route with pushedByRoute)
+ cy.ionNav('ion-item', 'Details 1');
+ cy.ionPageVisible('home-details-page-1');
+
+ // Switch to Settings tab (PUSH with direction 'none' - creates route WITHOUT pushedByRoute)
+ cy.ionTabClick('Settings');
+ cy.ionPageVisible('settings-page');
+
+ // Browser back - this is a non-linear POP because settings route has no pushedByRoute.
+ // The else branch should push the current location key onto forwardStack.
+ cy.ionGoBack('/routing/tabs/home/details/1');
+ cy.ionPageVisible('home-details-page-1');
+ cy.get('ion-tab-button.tab-selected').contains('Home');
+
+ // Browser forward - should be detected as forward navigation via forwardStack.
+ // Without the fix, forwardStack is empty so this falls into the wrong branch
+ // (else-if, treating it as back navigation with wrong animation).
+ cy.ionGoForward('/routing/tabs/settings');
+ cy.ionPageVisible('settings-page');
+ cy.get('ion-tab-button.tab-selected').contains('Settings');
+ });
+
+ it('Home > D1 > Settings Tab > Browser Back > Browser Forward > Browser Back, forward stack should not be corrupted', () => {
+ cy.visit(`http://localhost:${port}/routing/tabs/home`);
+ cy.ionPageVisible('home-page');
+
+ // Build navigation stack
+ cy.ionNav('ion-item', 'Details 1');
+ cy.ionPageVisible('home-details-page-1');
+
+ // Tab switch creates a route without pushedByRoute
+ cy.ionTabClick('Settings');
+ cy.ionPageVisible('settings-page');
+
+ // Browser back (non-linear POP)
+ cy.ionGoBack('/routing/tabs/home/details/1');
+ cy.ionPageVisible('home-details-page-1');
+
+ // Browser forward
+ cy.ionGoForward('/routing/tabs/settings');
+ cy.ionPageVisible('settings-page');
+
+ // Browser back again - without the fix, the forward stack is corrupted from
+ // the previous misclassification, causing this back to be treated as forward.
+ cy.ionGoBack('/routing/tabs/home/details/1');
+ cy.ionPageVisible('home-details-page-1');
+ cy.get('ion-tab-button.tab-selected').contains('Home');
+
+ // One more forward/back cycle to verify stack integrity
+ cy.ionGoForward('/routing/tabs/settings');
+ cy.ionPageVisible('settings-page');
+ cy.get('ion-tab-button.tab-selected').contains('Settings');
+
+ cy.ionGoBack('/routing/tabs/home/details/1');
+ cy.ionPageVisible('home-details-page-1');
+ cy.get('ion-tab-button.tab-selected').contains('Home');
+ });
+
+ it('Home > D1 > Settings Tab > Browser Back > Browser Forward, forward navigation should have correct routeDirection', () => {
+ cy.visit(`http://localhost:${port}/routing/tabs/home`);
+ cy.ionPageVisible('home-page');
+
+ // Navigate to Details 1
+ cy.ionNav('ion-item', 'Details 1');
+ cy.ionPageVisible('home-details-page-1');
+
+ // Switch to Settings tab (route has no pushedByRoute)
+ cy.ionTabClick('Settings');
+ cy.ionPageVisible('settings-page');
+
+ // Browser back (non-linear POP)
+ cy.ionGoBack('/routing/tabs/home/details/1');
+ cy.ionPageVisible('home-details-page-1');
+
+ // Browser forward to Settings
+ cy.ionGoForward('/routing/tabs/settings');
+ cy.ionPageVisible('settings-page');
+
+ // Browser back to D1
+ cy.ionGoBack('/routing/tabs/home/details/1');
+ cy.ionPageVisible('home-details-page-1');
+
+ // Browser back to Home. The D1 route lost its pushedByRoute when it was
+ // recreated via a non-linear POP, so this back also goes through the
+ // non-linear else branch.
+ cy.ionGoBack('/routing/tabs/home');
+ cy.ionPageVisible('home-page');
+ cy.contains('[data-pageid=home-page]', '"routeAction":"pop"');
+ cy.contains('[data-pageid=home-page]', '"pathname":"/routing/tabs/home"');
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/specs/prefix-match-wildcard.cy.js b/packages/react-router/test/base/tests/e2e/specs/prefix-match-wildcard.cy.js
new file mode 100644
index 00000000000..32e2f2bb796
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/specs/prefix-match-wildcard.cy.js
@@ -0,0 +1,72 @@
+const port = 3000;
+
+/**
+ * Tests that wildcard routes work correctly when specific routes share
+ * a common prefix with the navigation target.
+ *
+ * Bug: couldSpecificRouteMatch used a 3-char prefix heuristic that
+ * falsely blocked wildcard matches (e.g., "settings" vs "setup" both
+ * start with "set", causing the wildcard to not match "setup").
+ */
+describe('Prefix Match Wildcard', () => {
+ it('should navigate to settings (specific route match)', () => {
+ cy.visit(`http://localhost:${port}/prefix-match-wildcard`);
+ cy.ionPageVisible('prefix-home');
+
+ cy.get('#go-to-settings').click();
+ cy.ionPageVisible('prefix-settings');
+ cy.get('[data-testid="settings-content"]').should('exist');
+ });
+
+ it('should navigate to setup via wildcard (shares "set" prefix with settings)', () => {
+ cy.visit(`http://localhost:${port}/prefix-match-wildcard`);
+ cy.ionPageVisible('prefix-home');
+
+ cy.get('#go-to-setup').click();
+ cy.ionPageVisible('prefix-catchall');
+ cy.get('[data-testid="catchall-content"]').should('exist');
+ });
+
+ it('should navigate to unknown path via wildcard', () => {
+ cy.visit(`http://localhost:${port}/prefix-match-wildcard`);
+ cy.ionPageVisible('prefix-home');
+
+ cy.get('#go-to-unknown').click();
+ cy.ionPageVisible('prefix-catchall');
+ cy.get('[data-testid="catchall-content"]').should('exist');
+ });
+
+ it('should load settings directly when visiting URL', () => {
+ cy.visit(`http://localhost:${port}/prefix-match-wildcard/settings`);
+ cy.ionPageVisible('prefix-settings');
+ cy.get('[data-testid="settings-content"]').should('exist');
+ });
+
+ it('should load setup directly via wildcard when visiting URL (no prior mount path)', () => {
+ // Direct visit β no prior navigation, so outletMountPath is undefined.
+ // Bug: the 3-char prefix heuristic in couldSpecificRouteMatch sees that
+ // "settings" and "setup" both start with "set", blocks the wildcard,
+ // and the index route incorrectly claims the match at the wrong depth.
+ cy.visit(`http://localhost:${port}/prefix-match-wildcard/setup`);
+ cy.ionPageVisible('prefix-catchall');
+ cy.get('[data-testid="catchall-content"]').should('exist');
+ });
+
+ it('should navigate to wildcard, go back, then navigate to settings', () => {
+ cy.visit(`http://localhost:${port}/prefix-match-wildcard`);
+ cy.ionPageVisible('prefix-home');
+
+ // Navigate to a wildcard route
+ cy.get('#go-to-setup').click();
+ cy.ionPageVisible('prefix-catchall');
+
+ // Go back to home
+ cy.go('back');
+ cy.ionPageVisible('prefix-home');
+
+ // Navigate to settings β this should work after returning from wildcard
+ cy.get('#go-to-settings').click();
+ cy.ionPageVisible('prefix-settings');
+ cy.get('[data-testid="settings-content"]').should('exist');
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/specs/redirect-params.cy.js b/packages/react-router/test/base/tests/e2e/specs/redirect-params.cy.js
new file mode 100644
index 00000000000..285e062921d
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/specs/redirect-params.cy.js
@@ -0,0 +1,72 @@
+/**
+ * Verifies that useParams() returns correct values after a Navigate
+ * (catch-all redirect) fires inside IonRouterOutlet.
+ *
+ * @see https://github.com/ionic-team/ionic-framework/issues/23743
+ */
+
+const port = 3000;
+
+describe('Redirect Params', () => {
+
+ describe('Tabs with catch-all Navigate redirect', () => {
+ // Verifies fix for https://github.com/ionic-team/ionic-framework/issues/23743
+ it('should have correct params after redirect then tab navigation', () => {
+ cy.visit(`http://localhost:${port}/redirect-params/tabs`);
+
+ // Wait for the redirect to complete and tab2 to load
+ cy.get('[data-testid="tab2-loaded"]').should('be.visible');
+ cy.url().should('include', '/redirect-params/tabs/tab2');
+
+ // Click "Tab 1" tab button which navigates to /redirect-params/tabs/tab1/TESTING
+ cy.get('ion-tab-button[tab="tab1"]').click();
+
+ // Verify the page loaded and params are correct
+ cy.ionPageVisible('tab1-with-param');
+ cy.get('[data-testid="param-value"]').should('have.text', 'TESTING');
+ cy.get('#param-display').should('contain.text', 'Tab 1 with param: TESTING');
+ });
+
+ it('should have correct params after redirect then button navigation', () => {
+ cy.visit(`http://localhost:${port}/redirect-params/tabs`);
+
+ cy.get('[data-testid="tab2-loaded"]').should('be.visible');
+
+ // Navigate to Tab 1 via the routerLink button
+ cy.get('#go-to-tab1').click();
+
+ cy.ionPageVisible('tab1-with-param');
+ cy.get('[data-testid="param-value"]').should('have.text', 'TESTING');
+ });
+ });
+
+ describe('Flat outlet with catch-all Navigate redirect', () => {
+ it('should have correct params after redirect then navigation to parameterized route', () => {
+ cy.visit(`http://localhost:${port}/redirect-params/flat`);
+
+ cy.get('[data-testid="home-loaded"]').should('be.visible');
+ cy.url().should('include', '/redirect-params/flat/home');
+
+ cy.get('#go-to-details-42').click();
+
+ cy.ionPageVisible('details-page');
+ cy.get('[data-testid="detail-param-value"]').should('have.text', '42');
+ cy.get('#detail-display').should('contain.text', 'Detail ID: 42');
+ });
+
+ it('should have correct params for multiple different parameterized navigations after redirect', () => {
+ cy.visit(`http://localhost:${port}/redirect-params/flat`);
+
+ cy.get('[data-testid="home-loaded"]').should('be.visible');
+
+ cy.get('#go-to-details-42').click();
+ cy.get('[data-testid="detail-param-value"]').should('have.text', '42');
+
+ cy.go('back');
+ cy.get('[data-testid="home-loaded"]').should('be.visible');
+
+ cy.get('#go-to-details-99').click();
+ cy.get('[data-testid="detail-param-value"]').should('have.text', '99');
+ });
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/specs/relative-paths.cy.js b/packages/react-router/test/base/tests/e2e/specs/relative-paths.cy.js
new file mode 100644
index 00000000000..4491636fbc8
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/specs/relative-paths.cy.js
@@ -0,0 +1,68 @@
+const port = 3000;
+
+/**
+ * Tests for relative path handling in IonRouterOutlet.
+ * Verifies that routes with relative paths (no leading slash) work
+ * the same as absolute paths, matching React Router 6 behavior.
+ */
+describe('Relative Paths Tests', () => {
+ it('should navigate to the relative paths home page', () => {
+ cy.visit(`http://localhost:${port}/relative-paths`);
+ cy.ionPageVisible('relative-paths-home');
+ });
+
+ it('should navigate to Page A (defined with absolute path)', () => {
+ cy.visit(`http://localhost:${port}/relative-paths`);
+ cy.ionPageVisible('relative-paths-home');
+ cy.ionNav('ion-item', 'Go to Page A');
+ cy.ionPageVisible('relative-paths-page-a');
+ cy.get('[data-testid="page-a-content"]').should('contain', 'Page A');
+ });
+
+ it('should navigate to Page B (defined with relative path - no leading slash)', () => {
+ cy.visit(`http://localhost:${port}/relative-paths`);
+ cy.ionPageVisible('relative-paths-home');
+ cy.ionNav('ion-item', 'Go to Page B');
+ cy.ionPageVisible('relative-paths-page-b');
+ cy.get('[data-testid="page-b-content"]').should('contain', 'Page B');
+ });
+
+ it('should navigate directly to Page B via URL', () => {
+ cy.visit(`http://localhost:${port}/relative-paths/page-b`);
+ cy.ionPageVisible('relative-paths-page-b');
+ cy.get('[data-testid="page-b-content"]').should('contain', 'Page B');
+ });
+
+ it('should navigate to Page B and back', () => {
+ cy.visit(`http://localhost:${port}/relative-paths`);
+ cy.ionPageVisible('relative-paths-home');
+ cy.ionNav('ion-item', 'Go to Page B');
+ cy.ionPageVisible('relative-paths-page-b');
+ cy.ionBackClick('relative-paths-page-b');
+ cy.ionPageVisible('relative-paths-home');
+ });
+
+ it('should render catch-all * route for unknown paths via navigation', () => {
+ cy.visit(`http://localhost:${port}/relative-paths`);
+ cy.ionPageVisible('relative-paths-home');
+ cy.ionNav('ion-item', 'Go to Unknown Page');
+ cy.ionPageVisible('relative-paths-catch-all');
+ cy.ionPageHidden('relative-paths-home');
+ cy.get('[data-testid="catch-all-content"]').should('contain', 'not found');
+ });
+
+ it('should render catch-all * route for unknown paths via direct URL', () => {
+ cy.visit(`http://localhost:${port}/relative-paths/some-nonexistent-page`);
+ cy.ionPageVisible('relative-paths-catch-all');
+ cy.get('[data-testid="catch-all-content"]').should('contain', 'not found');
+ });
+
+ it('should navigate to catch-all and back to home', () => {
+ cy.visit(`http://localhost:${port}/relative-paths`);
+ cy.ionPageVisible('relative-paths-home');
+ cy.ionNav('ion-item', 'Go to Unknown Page');
+ cy.ionPageVisible('relative-paths-catch-all');
+ cy.ionBackClick('relative-paths-catch-all');
+ cy.ionPageVisible('relative-paths-home');
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/specs/replace-actions.cy.js b/packages/react-router/test/base/tests/e2e/specs/replace-actions.cy.js
index 918888dd908..945f65cb30b 100644
--- a/packages/react-router/test/base/tests/e2e/specs/replace-actions.cy.js
+++ b/packages/react-router/test/base/tests/e2e/specs/replace-actions.cy.js
@@ -17,4 +17,23 @@ describe('Replace Action', () => {
cy.ionPageVisible('page1');
cy.ionPageDoesNotExist('page2');
});
+
+ /**
+ * Tests that the browser forward button works after going back from a
+ * replace-navigated page. When going back uses navigate(-1) (native browser
+ * back), the forward entry is preserved in the browser history stack.
+ * If it falls through to handleNavigate (replace-based), forward is broken.
+ */
+ it('/replace-action > Goto Page2 > Goto Page3 > Browser Back > Browser Forward > Page3 should be visible', () => {
+ cy.visit(`http://localhost:${port}/replace-action`);
+ cy.ionPageVisible('page1');
+ cy.ionNav('ion-button', 'Goto Page2');
+ cy.ionPageVisible('page2');
+ cy.ionNav('ion-button', 'Goto Page3');
+ cy.ionPageVisible('page3');
+ cy.go('back');
+ cy.ionPageVisible('page1');
+ cy.go('forward');
+ cy.ionPageVisible('page3');
+ });
});
diff --git a/packages/react-router/test/base/tests/e2e/specs/root-splat-tabs.cy.js b/packages/react-router/test/base/tests/e2e/specs/root-splat-tabs.cy.js
new file mode 100644
index 00000000000..e9f626da4b6
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/specs/root-splat-tabs.cy.js
@@ -0,0 +1,95 @@
+const port = 3000;
+
+/**
+ * Tests for relative paths (e.g., "tab1/*") inside root-level splat routes (*).
+ * Verifies the fix for routes not matching when parent is a splat-only route.
+ */
+describe('Root Splat Tabs - Customer Reproduction', () => {
+ it('should navigate to tab1 by default when visiting /root-splat-tabs', () => {
+ cy.visit(`http://localhost:${port}/root-splat-tabs`);
+ // Should redirect to tab1 and show tab1 content
+ cy.ionPageVisible('root-splat-tab1');
+ cy.get('[data-testid="root-splat-tab1-content"]').should('exist');
+ });
+
+ it('should load tab1 when directly visiting /root-splat-tabs/tab1', () => {
+ cy.visit(`http://localhost:${port}/root-splat-tabs/tab1`);
+ // CRITICAL: This should show tab1 content, NOT 404
+ cy.ionPageVisible('root-splat-tab1');
+ cy.get('[data-testid="root-splat-tab1-content"]').should('exist');
+ cy.get('[data-testid="root-splat-not-found"]').should('not.exist');
+ });
+
+ it('should load Page A when directly visiting /root-splat-tabs/tab1/page-a', () => {
+ cy.visit(`http://localhost:${port}/root-splat-tabs/tab1/page-a`);
+ // CRITICAL: This should show Page A, NOT 404
+ // This is the exact issue the customer reported
+ cy.ionPageVisible('root-splat-page-a');
+ cy.get('[data-testid="root-splat-page-a-content"]').should('exist');
+ cy.get('[data-testid="root-splat-not-found"]').should('not.exist');
+ });
+
+ it('should navigate to Page A via relative link', () => {
+ cy.visit(`http://localhost:${port}/root-splat-tabs/tab1`);
+ cy.ionPageVisible('root-splat-tab1');
+
+ // Click the relative link
+ cy.get('[data-testid="link-relative-page-a"]').click();
+
+ // Should be at Page A (not 404)
+ cy.ionPageVisible('root-splat-page-a');
+ cy.get('[data-testid="root-splat-page-a-content"]').should('exist');
+ cy.get('[data-testid="root-splat-not-found"]').should('not.exist');
+
+ // URL should be correct
+ cy.url().should('include', '/root-splat-tabs/tab1/page-a');
+ });
+
+ it('should navigate to Page A via absolute link', () => {
+ cy.visit(`http://localhost:${port}/root-splat-tabs/tab1`);
+ cy.ionPageVisible('root-splat-tab1');
+
+ // Click the absolute link
+ cy.get('[data-testid="link-absolute-page-a"]').click();
+
+ // Should be at Page A (not 404)
+ cy.ionPageVisible('root-splat-page-a');
+ cy.get('[data-testid="root-splat-page-a-content"]').should('exist');
+ cy.get('[data-testid="root-splat-not-found"]').should('not.exist');
+ });
+
+ it('should have correct href for relative link', () => {
+ cy.visit(`http://localhost:${port}/root-splat-tabs/tab1`);
+ cy.ionPageVisible('root-splat-tab1');
+
+ // The relative link should resolve to the correct absolute href
+ cy.get('[data-testid="link-relative-page-a"]')
+ .should('have.attr', 'href', '/root-splat-tabs/tab1/page-a');
+ });
+
+ it('should navigate between tabs correctly', () => {
+ cy.visit(`http://localhost:${port}/root-splat-tabs/tab1`);
+ cy.ionPageVisible('root-splat-tab1');
+
+ // Switch to Tab 2
+ cy.ionTabClick('Tab 2');
+ cy.ionPageVisible('root-splat-tab2');
+
+ // Switch back to Tab 1
+ cy.ionTabClick('Tab 1');
+ cy.ionPageVisible('root-splat-tab1');
+ });
+
+ it('should navigate to Page A and back to Tab 1', () => {
+ cy.visit(`http://localhost:${port}/root-splat-tabs/tab1`);
+ cy.ionPageVisible('root-splat-tab1');
+
+ // Navigate to Page A
+ cy.get('[data-testid="link-relative-page-a"]').click();
+ cy.ionPageVisible('root-splat-page-a');
+
+ // Go back
+ cy.ionBackClick('root-splat-page-a');
+ cy.ionPageVisible('root-splat-tab1');
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/specs/route-context-shape.cy.js b/packages/react-router/test/base/tests/e2e/specs/route-context-shape.cy.js
new file mode 100644
index 00000000000..55b8bfc7cb1
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/specs/route-context-shape.cy.js
@@ -0,0 +1,56 @@
+const port = 3000;
+
+/**
+ * Validates that React Router's UNSAFE_RouteContext shape is compatible with
+ * @ionic/react-router. This is a canary test β if React Router changes the
+ * internal context shape, these tests will fail and signal that the
+ * RouteContextMatch type and buildContextMatches need updating.
+ *
+ * The validators read Ionic's constructed context (built by buildContextMatches
+ * in ReactRouterViewStack), which mirrors the native RR6 shape. If the shape
+ * Ionic produces drifts, components like useParams() will break.
+ */
+describe('UNSAFE_RouteContext shape validation', () => {
+ it('should produce valid constructed context shape at root outlet level', () => {
+ cy.visit(`http://localhost:${port}/route-context-shape`);
+ cy.ionPageVisible('route-context-root');
+
+ // The root validator reads Ionic's constructed context and verifies
+ // that buildContextMatches produces entries with the expected shape
+ cy.get('#validator-root')
+ .should('have.attr', 'data-has-context', 'true')
+ .should('have.attr', 'data-all-valid', 'true');
+
+ // Should have at least 1 match (the route-context-shape/* route)
+ cy.get('#validator-root')
+ .invoke('attr', 'data-match-count')
+ .then((count) => {
+ expect(parseInt(count, 10)).to.be.greaterThan(0);
+ });
+ });
+
+ it('should produce valid constructed context shape at nested level with params', () => {
+ cy.visit(`http://localhost:${port}/route-context-shape`);
+ cy.ionPageVisible('route-context-root');
+
+ // Navigate to nested route with params
+ cy.get('#go-nested').click();
+ cy.ionPageVisible('route-context-nested');
+
+ // The nested validator reads Ionic's constructed context at a deeper
+ // level β verifies parent + child matches are both correctly shaped
+ cy.get('#validator-nested')
+ .should('have.attr', 'data-has-context', 'true')
+ .should('have.attr', 'data-all-valid', 'true');
+
+ // Nested route should have more matches than root (parent + child)
+ cy.get('#validator-nested')
+ .invoke('attr', 'data-match-count')
+ .then((count) => {
+ expect(parseInt(count, 10)).to.be.greaterThan(1);
+ });
+
+ // Params should be correctly propagated through context
+ cy.get('#nested-params').should('contain', '"id":"42"');
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/specs/routing.cy.js b/packages/react-router/test/base/tests/e2e/specs/routing.cy.js
index fd28ee573c0..6a90080af9f 100644
--- a/packages/react-router/test/base/tests/e2e/specs/routing.cy.js
+++ b/packages/react-router/test/base/tests/e2e/specs/routing.cy.js
@@ -140,9 +140,75 @@ describe('Routing Tests', () => {
cy.ionPageVisible('tab3-page');
});
+ it('Tab 3 > Other Page, tab page should not flash blank during transition', () => {
+ // Verifies fix for https://github.com/ionic-team/ionic-framework/issues/25477
+ // Tests that navigating from a tab page to a non-tab page does not cause
+ // the tab page content to vanish before the transition animation completes.
+ // Bug: handleOutOfScopeOutlet immediately applied ion-page-hidden (display: none)
+ // to tab views, causing the leaving page to flash blank during the forward transition.
+ cy.visit(`http://localhost:${port}/routing/tabs/tab3?ionic:mode=ios`);
+ cy.ionPageVisible('tab3-page');
+
+ // Set up a MutationObserver BEFORE navigating to detect if ion-page-hidden
+ // (display: none) is ever applied to the tab page during the transition.
+ cy.window().then((win) => {
+ const tabPage = win.document.querySelector('[data-pageid="tab3-page"]');
+ win.__ionPageHiddenApplied = false;
+ const observer = new MutationObserver((mutations) => {
+ for (const mutation of mutations) {
+ if (mutation.target.classList && mutation.target.classList.contains('ion-page-hidden')) {
+ win.__ionPageHiddenApplied = true;
+ }
+ }
+ });
+ observer.observe(tabPage, { attributes: true, attributeFilter: ['class'] });
+ win.__tabPageObserver = observer;
+ });
+
+ // Navigate to non-tab page
+ cy.contains('ion-button', 'Go to Other Page').click();
+ cy.ionPageVisible('other-page');
+
+ // Verify ion-page-hidden was never applied during the transition
+ cy.window().then((win) => {
+ win.__tabPageObserver.disconnect();
+ expect(win.__ionPageHiddenApplied).to.be.false;
+ });
+ });
+
+ it('Home > Other Page, tab page should not flash blank during transition', () => {
+ // Verifies fix for https://github.com/ionic-team/ionic-framework/issues/25477
+ // Same test as above but from the Home tab using routerLink navigation
+ cy.visit(`http://localhost:${port}/routing/tabs/home?ionic:mode=ios`);
+ cy.ionPageVisible('home-page');
+
+ cy.window().then((win) => {
+ const tabPage = win.document.querySelector('[data-pageid="home-page"]');
+ win.__ionPageHiddenApplied = false;
+ const observer = new MutationObserver((mutations) => {
+ for (const mutation of mutations) {
+ if (mutation.target.classList && mutation.target.classList.contains('ion-page-hidden')) {
+ win.__ionPageHiddenApplied = true;
+ }
+ }
+ });
+ observer.observe(tabPage, { attributes: true, attributeFilter: ['class'] });
+ win.__tabPageObserver = observer;
+ });
+
+ cy.contains('ion-item', 'Other Page').click();
+ cy.ionPageVisible('other-page');
+
+ cy.window().then((win) => {
+ win.__tabPageObserver.disconnect();
+ expect(win.__ionPageHiddenApplied).to.be.false;
+ });
+ });
+
it('/ > Menu > Favorites > Menu > Tabs, should be back on Home', () => {
// Tests transferring from one outlet to another and back again via menu
cy.visit(`http://localhost:${port}/routing`);
+ cy.ionPageVisible('home-page');
cy.ionMenuClick();
cy.ionMenuNav('Favorites');
cy.ionPageVisible('favorites-page');
@@ -203,6 +269,36 @@ describe('Routing Tests', () => {
cy.ionPageVisible('home-page');
});
+ it('/ > Details 1 > Details 2 > Details 3 > Browser Back > Browser Forward, should be back on Details 3', () => {
+ // Tests browser forward button within a tab's own navigation stack
+ cy.visit(`http://localhost:${port}/routing/`);
+ cy.ionNav('ion-item', 'Details 1');
+ cy.ionPageVisible('home-details-page-1');
+ cy.ionNav('ion-button', 'Go to Details 2');
+ cy.ionPageVisible('home-details-page-2');
+ cy.ionNav('ion-button', 'Go to Details 3');
+ cy.ionPageVisible('home-details-page-3');
+ cy.go('back');
+ cy.ionPageVisible('home-details-page-2');
+ cy.go('forward');
+ cy.wait(500);
+ cy.ionPageVisible('home-details-page-3');
+ });
+
+ it('/ > Details 1 > Settings Details 1 > Browser Back > Browser Forward, should show Settings Details 1', () => {
+ // Tests browser forward button across tabs (cross-tab forward)
+ cy.visit(`http://localhost:${port}/routing/`);
+ cy.ionNav('ion-item', 'Details 1');
+ cy.ionPageVisible('home-details-page-1');
+ cy.ionNav('ion-button', 'Go to Settings Details 1');
+ cy.ionPageVisible('settings-details-page-1');
+ cy.go('back');
+ cy.ionPageVisible('home-details-page-1');
+ cy.go('forward');
+ cy.wait(500);
+ cy.ionPageVisible('settings-details-page-1');
+ });
+
it('when props get passed into a route render, the component should update', () => {
cy.visit(`http://localhost:${port}/routing/propstest`);
cy.ionPageVisible('props-test');
@@ -267,7 +363,7 @@ describe('Routing Tests', () => {
cy.ionPageVisible('home-details-page-2');
});
- it('/routing/tabs/home Menu > Favorites > Menu > Home with redirect, Home page should be visible, and Favorites should be hidden', () => {
+ it('/routing/tabs/home Menu > Favorites > Menu > Home with redirect, Home page should be visible, and Favorites should be destroyed', () => {
cy.visit(`http://localhost:${port}/routing/tabs/home`);
cy.ionMenuClick();
cy.ionMenuNav('Favorites');
@@ -278,7 +374,7 @@ describe('Routing Tests', () => {
cy.ionPageDoesNotExist('favorites-page');
});
- it('/routing/tabs/home Menu > Favorites > Menu > Home with router, Home page should be visible, and Favorites should be hidden', () => {
+ it('/routing/tabs/home Menu > Favorites > Menu > Home with router, Home page should be visible, and Favorites should be destroyed', () => {
cy.visit(`http://localhost:${port}/routing/tabs/home`);
cy.ionMenuClick();
cy.ionMenuNav('Favorites');
@@ -286,7 +382,7 @@ describe('Routing Tests', () => {
cy.ionMenuClick();
cy.ionMenuNav('Home with router');
cy.ionPageVisible('home-page');
- cy.ionPageHidden('favorites-page');
+ cy.ionPageDoesNotExist('favorites-page');
});
it('should show back button when going back to a pushed page', () => {
@@ -344,6 +440,16 @@ describe('Routing Tests', () => {
cy.get('div.ion-page[data-pageid=home-details-page-1] [data-testid="details-input"]').should('have.value', '1');
});
+ it('should complete chained Navigate redirects from root to /routing/tabs/home', () => {
+ // Tests that chained Navigate redirects work correctly:
+ // / > click Routing link > /routing (Navigate to tabs) > /routing/tabs (Navigate to home) > /routing/tabs/home
+ // This was a bug where the second Navigate would be unmounted before it could trigger
+ cy.visit(`http://localhost:${port}/`);
+ cy.ionNav('ion-item', 'Routing');
+ cy.ionPageVisible('home-page');
+ cy.url().should('include', '/routing/tabs/home');
+ });
+
/*
Tests to add:
Test that lifecycle events fire
diff --git a/packages/react-router/test/base/tests/e2e/specs/search-params.cy.js b/packages/react-router/test/base/tests/e2e/specs/search-params.cy.js
new file mode 100644
index 00000000000..83252c871c4
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/specs/search-params.cy.js
@@ -0,0 +1,88 @@
+describe('Search Params Navigation', () => {
+ beforeEach(() => {
+ cy.visit('/search-params');
+ // Wait for the page to be visible
+ cy.get('[data-pageid="search-params"]').should('be.visible');
+ });
+
+ it('should navigate when adding search params to the same path', () => {
+ // Verify we start without search params
+ cy.get('#current-path').should('have.text', '/search-params');
+ cy.get('#query-value').should('have.text', '');
+
+ // Click to add search param - this navigates from /search-params to /search-params?q=test
+ cy.get('#add-search').click();
+
+ // The URL should update with the search param
+ cy.location('pathname').should('eq', '/search-params');
+ cy.location('search').should('eq', '?q=test');
+
+ // The component should re-render with the new search param value
+ cy.get('#current-path').should('have.text', '/search-params?q=test');
+ cy.get('#query-value').should('have.text', 'test');
+ });
+
+ it('should navigate when changing search params on the same path', () => {
+ // First add a search param
+ cy.get('#add-search').click();
+ cy.get('#query-value').should('have.text', 'test');
+
+ // Now change the search param
+ cy.get('#change-search').click();
+
+ cy.location('search').should('eq', '?q=changed');
+ cy.get('#current-path').should('have.text', '/search-params?q=changed');
+ cy.get('#query-value').should('have.text', 'changed');
+ });
+
+ it('should navigate when removing search params from the same path', () => {
+ // First add a search param
+ cy.get('#add-search').click();
+ cy.get('#query-value').should('have.text', 'test');
+
+ // Now remove the search param
+ cy.get('#remove-search').click();
+
+ cy.location('search').should('eq', '');
+ cy.get('#current-path').should('have.text', '/search-params');
+ cy.get('#query-value').should('have.text', '');
+ });
+
+ it('should restore search params when navigating back from another page', () => {
+ // Add search param
+ cy.get('#add-search').click();
+ cy.get('#query-value').should('have.text', 'test');
+ cy.location('search').should('eq', '?q=test');
+
+ // Navigate to home page
+ cy.get('#go-home').click();
+ cy.get('[data-pageid="home"]').should('be.visible');
+ cy.location('pathname').should('eq', '/');
+
+ // Go back - should restore /search-params?q=test
+ cy.go('back');
+ cy.get('[data-pageid="search-params"]').should('be.visible');
+ cy.location('pathname').should('eq', '/search-params');
+ cy.location('search').should('eq', '?q=test');
+ cy.get('#query-value').should('have.text', 'test');
+ });
+
+ it('should support back through multiple search param changes', () => {
+ // Navigate: /search-params -> /search-params?q=test -> /search-params?q=changed
+ cy.get('#add-search').click();
+ cy.get('#query-value').should('have.text', 'test');
+
+ cy.get('#change-search').click();
+ cy.get('#query-value').should('have.text', 'changed');
+
+ // Go back to ?q=test
+ cy.go('back');
+ cy.location('search').should('eq', '?q=test');
+ cy.get('#query-value').should('have.text', 'test');
+
+ // Go back to no search params
+ cy.go('back');
+ cy.location('search').should('eq', '');
+ cy.get('#query-value').should('have.text', '');
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/specs/shadow-matchroutes.cy.js b/packages/react-router/test/base/tests/e2e/specs/shadow-matchroutes.cy.js
new file mode 100644
index 00000000000..49c93ae9c3c
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/specs/shadow-matchroutes.cy.js
@@ -0,0 +1,36 @@
+const port = 3000;
+
+/**
+ * Verifies that the matchRoutes()-based findRouteByRouteInfo works correctly
+ * for various route patterns: absolute, relative, nested, tabs, index.
+ */
+describe('matchRoutes integration', () => {
+ it('should match absolute routes at root outlet', () => {
+ cy.visit(`http://localhost:${port}/`);
+ cy.ionPageVisible('home');
+ });
+
+ it('should match relative routes in nested outlet', () => {
+ cy.visit(`http://localhost:${port}/routing/tabs/home`);
+ cy.ionPageVisible('home-page');
+ });
+
+ it('should match routes after tab switch', () => {
+ cy.visit(`http://localhost:${port}/routing/tabs/home`);
+ cy.ionPageVisible('home-page');
+
+ cy.ionTabClick('Settings');
+ cy.ionPageVisible('settings-page');
+ });
+
+ it('should match routes after switching tabs back', () => {
+ cy.visit(`http://localhost:${port}/routing/tabs/home`);
+ cy.ionPageVisible('home-page');
+
+ cy.ionTabClick('Settings');
+ cy.ionPageVisible('settings-page');
+
+ cy.ionTabClick('Home');
+ cy.ionPageVisible('home-page');
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/specs/stale-view-cleanup.cy.js b/packages/react-router/test/base/tests/e2e/specs/stale-view-cleanup.cy.js
new file mode 100644
index 00000000000..65d6bd06b02
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/specs/stale-view-cleanup.cy.js
@@ -0,0 +1,24 @@
+const port = 3000;
+
+describe('Stale View Cleanup', () => {
+ /**
+ * Tests that non-IonPage components are properly cleaned up from the DOM
+ * after navigating away. Both source and target lack IonPage wrappers.
+ * Guards against regressions in the view stack cleanup logic.
+ */
+ it('should clean up stale non-IonPage view after navigating to another non-IonPage view', () => {
+ cy.visit(`http://localhost:${port}/stale-view-cleanup/non-ionpage`);
+
+ // Verify the non-IonPage source component is visible
+ cy.get('[data-testid="non-ionpage-source"]').should('exist');
+
+ // Navigate to the target (also non-IonPage)
+ cy.get('#go-to-target').click();
+
+ // Verify the target loaded
+ cy.get('[data-testid="target-loaded"]').should('exist');
+
+ // The non-IonPage source component should be cleaned up from the DOM
+ cy.get('[data-testid="non-ionpage-source"]').should('not.exist');
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/specs/swipe-to-go-back.cy.js b/packages/react-router/test/base/tests/e2e/specs/swipe-to-go-back.cy.js
index 94fb3367ccc..283845e757a 100644
--- a/packages/react-router/test/base/tests/e2e/specs/swipe-to-go-back.cy.js
+++ b/packages/react-router/test/base/tests/e2e/specs/swipe-to-go-back.cy.js
@@ -31,6 +31,14 @@ describe('Swipe To Go Back', () => {
cy.ionPageVisible('main');
});
+ it('should render details page when navigating directly to nested route', () => {
+ cy.visit(`http://localhost:${port}/swipe-to-go-back/details?${IOS_MODE}`);
+ cy.ionPageVisible('details');
+
+ cy.get('[data-pageid="details"]').should('be.visible');
+ cy.get('[data-pageid="details"] ion-content').should('contain', 'Details');
+ });
+
it('should swipe and abort within a tab', () => {
cy.visit(`http://localhost:${port}/tabs/tab1?${IOS_MODE}`);
cy.ionPageVisible('tab1');
@@ -141,4 +149,54 @@ describe('Swipe To Go Back', () => {
cy.ionPageVisible('params-1');
})
+
+ it('should keep correct view visible after swipe-back completes then abort on previous page', () => {
+ // Navigate three levels deep: main β details β details2
+ cy.visit(`http://localhost:${port}/swipe-to-go-back?${IOS_MODE}`);
+ cy.ionPageVisible('main');
+
+ cy.ionNav('ion-item', 'Details');
+ cy.ionPageVisible('details');
+ cy.ionPageHidden('main');
+
+ cy.get('#go-to-details2').click();
+ cy.wait(250);
+ cy.ionPageVisible('details2');
+ cy.ionPageHidden('details');
+
+ // Complete swipe back from details2 β details
+ cy.ionSwipeToGoBack(true, 'ion-router-outlet#swipe-to-go-back');
+ cy.ionPageVisible('details');
+ cy.ionPageDoesNotExist('details2');
+
+ // Now on details, abort a swipe back toward main
+ // This validates that the abort doesn't hide the currently-visible page
+ cy.ionSwipeToGoBack(false, 'ion-router-outlet#swipe-to-go-back');
+ cy.ionPageVisible('details');
+ cy.ionPageHidden('main');
+ })
+
+ it('should handle multiple consecutive swipe aborts without hiding current page', () => {
+ cy.visit(`http://localhost:${port}/swipe-to-go-back?${IOS_MODE}`);
+ cy.ionPageVisible('main');
+
+ cy.ionNav('ion-item', 'Details');
+ cy.ionPageVisible('details');
+ cy.ionPageHidden('main');
+
+ // First abort
+ cy.ionSwipeToGoBack(false, 'ion-router-outlet#swipe-to-go-back');
+ cy.ionPageVisible('details');
+ cy.ionPageHidden('main');
+
+ // Second abort
+ cy.ionSwipeToGoBack(false, 'ion-router-outlet#swipe-to-go-back');
+ cy.ionPageVisible('details');
+ cy.ionPageHidden('main');
+
+ // Third abort
+ cy.ionSwipeToGoBack(false, 'ion-router-outlet#swipe-to-go-back');
+ cy.ionPageVisible('details');
+ cy.ionPageHidden('main');
+ })
});
diff --git a/packages/react-router/test/base/tests/e2e/specs/tab-history-isolation.cy.js b/packages/react-router/test/base/tests/e2e/specs/tab-history-isolation.cy.js
new file mode 100644
index 00000000000..eb8c1a1feb0
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/specs/tab-history-isolation.cy.js
@@ -0,0 +1,204 @@
+const port = 3000;
+
+describe('Tab History Isolation', () => {
+ it('should NOT navigate back to previous tab when using back button after tab bar switch', () => {
+ cy.visit(`http://localhost:${port}/tab-history-isolation/a`);
+ cy.ionPageVisible('tab-a');
+
+ cy.ionTabClick('Tab B');
+ cy.ionPageHidden('tab-a');
+ cy.ionPageVisible('tab-b');
+
+ cy.get(`div.ion-page[data-pageid=tab-b]`)
+ .find('ion-back-button')
+ .click({ force: true });
+
+ cy.wait(500);
+
+ cy.ionPageVisible('tab-b');
+ cy.ionPageHidden('tab-a');
+ cy.url().should('include', '/tab-history-isolation/b');
+ });
+
+ it('should NOT allow back navigation through multiple tab switches', () => {
+ cy.visit(`http://localhost:${port}/tab-history-isolation/a`);
+ cy.ionPageVisible('tab-a');
+
+ cy.ionTabClick('Tab B');
+ cy.ionPageHidden('tab-a');
+ cy.ionPageVisible('tab-b');
+
+ cy.ionTabClick('Tab C');
+ cy.ionPageHidden('tab-b');
+ cy.ionPageVisible('tab-c');
+
+ cy.get(`div.ion-page[data-pageid=tab-c]`)
+ .find('ion-back-button')
+ .click({ force: true });
+
+ cy.wait(500);
+
+ cy.ionPageVisible('tab-c');
+ cy.url().should('include', '/tab-history-isolation/c');
+ });
+
+ it('should navigate back within the same tab when using back button', () => {
+ cy.visit(`http://localhost:${port}/tab-history-isolation/a`);
+ cy.ionPageVisible('tab-a');
+
+ cy.get('#go-to-a-details').click();
+ cy.ionPageHidden('tab-a');
+ cy.ionPageVisible('tab-a-details');
+
+ cy.ionBackClick('tab-a-details');
+ cy.ionPageDoesNotExist('tab-a-details');
+ cy.ionPageVisible('tab-a');
+
+ cy.url().should('include', '/tab-history-isolation/a');
+ cy.url().should('not.include', '/details');
+ });
+
+ it('should only navigate back within current tab after switching tabs and navigating', () => {
+ cy.visit(`http://localhost:${port}/tab-history-isolation/a`);
+ cy.ionPageVisible('tab-a');
+
+ cy.ionTabClick('Tab B');
+ cy.ionPageHidden('tab-a');
+ cy.ionPageVisible('tab-b');
+
+ cy.get('#go-to-b-details').click();
+ cy.ionPageHidden('tab-b');
+ cy.ionPageVisible('tab-b-details');
+
+ cy.ionBackClick('tab-b-details');
+ cy.ionPageDoesNotExist('tab-b-details');
+ cy.ionPageVisible('tab-b');
+
+ cy.url().should('include', '/tab-history-isolation/b');
+ cy.url().should('not.include', '/details');
+
+ cy.get(`div.ion-page[data-pageid=tab-b]`)
+ .find('ion-back-button')
+ .click({ force: true });
+
+ cy.wait(500);
+
+ cy.ionPageVisible('tab-b');
+ cy.url().should('include', '/tab-history-isolation/b');
+ });
+
+ it('should preserve tab history when switching away and back', () => {
+ cy.visit(`http://localhost:${port}/tab-history-isolation/a`);
+ cy.ionPageVisible('tab-a');
+
+ cy.get('#go-to-a-details').click();
+ cy.ionPageHidden('tab-a');
+ cy.ionPageVisible('tab-a-details');
+
+ cy.ionTabClick('Tab B');
+ cy.ionPageHidden('tab-a-details');
+ cy.ionPageVisible('tab-b');
+
+ cy.ionTabClick('Tab A');
+ cy.ionPageHidden('tab-b');
+ cy.ionPageVisible('tab-a-details');
+
+ cy.ionBackClick('tab-a-details');
+ cy.ionPageDoesNotExist('tab-a-details');
+ cy.ionPageVisible('tab-a');
+ });
+
+ it('should have no back navigation when first visiting a tab', () => {
+ cy.visit(`http://localhost:${port}/tab-history-isolation/a`);
+ cy.ionPageVisible('tab-a');
+
+ cy.get(`div.ion-page[data-pageid=tab-a]`)
+ .find('ion-back-button')
+ .click({ force: true });
+
+ cy.wait(500);
+
+ cy.ionPageVisible('tab-a');
+ cy.url().should('include', '/tab-history-isolation/a');
+ });
+
+ /**
+ * Browser back/forward tests for non-"/tabs/" URL paths.
+ *
+ * These tests verify that per-tab history isolation works correctly
+ * when using browser back/forward buttons (POP events) with tab routes
+ * that do NOT contain "/tabs/" in their URL path.
+ *
+ * The tab-history-isolation routes use paths like /tab-history-isolation/a,
+ * /tab-history-isolation/b, etc. β no "/tabs/" segment. This exercises
+ * the context-driven tab detection (via location history) rather than
+ * URL-pattern-based detection.
+ */
+ it('should preserve tab context through browser back from detail page within a tab', () => {
+ cy.visit(`http://localhost:${port}/tab-history-isolation/a`);
+ cy.ionPageVisible('tab-a');
+
+ // Navigate to details within Tab A
+ cy.get('#go-to-a-details').click();
+ cy.ionPageHidden('tab-a');
+ cy.ionPageVisible('tab-a-details');
+
+ // Use browser back - should go back within the same tab
+ cy.go('back');
+ cy.ionPageVisible('tab-a');
+ cy.url().should('include', '/tab-history-isolation/a');
+ cy.url().should('not.include', '/details');
+ });
+
+ it('should handle browser forward after browser back within a tab', () => {
+ cy.visit(`http://localhost:${port}/tab-history-isolation/a`);
+ cy.ionPageVisible('tab-a');
+
+ // Navigate to details within Tab A
+ cy.get('#go-to-a-details').click();
+ cy.ionPageHidden('tab-a');
+ cy.ionPageVisible('tab-a-details');
+
+ // Browser back
+ cy.go('back');
+ cy.ionPageVisible('tab-a');
+ cy.url().should('not.include', '/details');
+
+ // Browser forward - should return to details
+ cy.go('forward');
+ cy.ionPageVisible('tab-a-details');
+ cy.url().should('include', '/tab-history-isolation/a/details');
+ });
+
+ it('should preserve per-tab history when using browser back after navigating within a tab and switching tabs', () => {
+ cy.visit(`http://localhost:${port}/tab-history-isolation/a`);
+ cy.ionPageVisible('tab-a');
+
+ // Navigate to details within Tab A
+ cy.get('#go-to-a-details').click();
+ cy.ionPageHidden('tab-a');
+ cy.ionPageVisible('tab-a-details');
+
+ // Switch to Tab B via tab bar
+ cy.ionTabClick('Tab B');
+ cy.ionPageHidden('tab-a-details');
+ cy.ionPageVisible('tab-b');
+
+ // Navigate to details within Tab B
+ cy.get('#go-to-b-details').click();
+ cy.ionPageHidden('tab-b');
+ cy.ionPageVisible('tab-b-details');
+
+ // Use browser back from Tab B details - should go back to Tab B root
+ cy.go('back');
+ cy.ionPageVisible('tab-b');
+ cy.url().should('include', '/tab-history-isolation/b');
+ cy.url().should('not.include', '/details');
+
+ // Switch back to Tab A - should still show Tab A details (preserved)
+ cy.ionTabClick('Tab A');
+ cy.ionPageHidden('tab-b');
+ cy.ionPageVisible('tab-a-details');
+ cy.url().should('include', '/tab-history-isolation/a/details');
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/specs/tab-search-params.cy.js b/packages/react-router/test/base/tests/e2e/specs/tab-search-params.cy.js
new file mode 100644
index 00000000000..5d5c0f40de0
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/specs/tab-search-params.cy.js
@@ -0,0 +1,54 @@
+const port = 3000;
+
+describe('Tab Button Search Params (Issue #25470)', () => {
+ it('should preserve query params when navigating to a tab for the first time', () => {
+ cy.visit(`http://localhost:${port}/tab-search-params`);
+
+ // The index route redirects to tab1?foo=bar
+ cy.ionPageVisible('tab-search-tab1');
+ cy.location('search').should('eq', '?foo=bar');
+ cy.get('#tab1-foo-value').should('have.text', 'bar');
+ });
+
+ it('should preserve query params when switching to another tab', () => {
+ cy.visit(`http://localhost:${port}/tab-search-params/tab1?foo=bar`);
+ cy.ionPageVisible('tab-search-tab1');
+
+ // Click tab2 which has href="/tab-search-params/tab2?baz=qux"
+ cy.ionTabClick('Tab2');
+ cy.ionPageVisible('tab-search-tab2');
+
+ cy.location('pathname').should('eq', '/tab-search-params/tab2');
+ cy.location('search').should('eq', '?baz=qux');
+ cy.get('#tab2-baz-value').should('have.text', 'qux');
+ });
+
+ it('should preserve query params when switching back to a previously visited tab', () => {
+ cy.visit(`http://localhost:${port}/tab-search-params/tab1?foo=bar`);
+ cy.ionPageVisible('tab-search-tab1');
+
+ // Switch to tab2
+ cy.ionTabClick('Tab2');
+ cy.ionPageVisible('tab-search-tab2');
+
+ // Switch back to tab1
+ cy.ionTabClick('Tab1');
+ cy.ionPageVisible('tab-search-tab1');
+
+ cy.location('pathname').should('eq', '/tab-search-params/tab1');
+ cy.location('search').should('eq', '?foo=bar');
+ cy.get('#tab1-foo-value').should('have.text', 'bar');
+ });
+
+ it('should preserve query params when clicking the already active tab', () => {
+ cy.visit(`http://localhost:${port}/tab-search-params/tab1?foo=bar`);
+ cy.ionPageVisible('tab-search-tab1');
+
+ // Click tab1 again (already active)
+ cy.ionTabClick('Tab1');
+
+ cy.location('pathname').should('eq', '/tab-search-params/tab1');
+ cy.location('search').should('eq', '?foo=bar');
+ cy.get('#tab1-foo-value').should('have.text', 'bar');
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/specs/tabs.cy.js b/packages/react-router/test/base/tests/e2e/specs/tabs.cy.js
index f0719090ae7..d04d9fcf08e 100644
--- a/packages/react-router/test/base/tests/e2e/specs/tabs.cy.js
+++ b/packages/react-router/test/base/tests/e2e/specs/tabs.cy.js
@@ -21,6 +21,17 @@ describe('Tabs', () => {
cy.ionPageVisible('tab1');
});
+ // Verifies that /tabs outlet uses segment-aware scope checking
+ // so /tabs-secondary is not treated as a prefix match of /tabs
+ it('should hide /tabs views when navigating to /tabs-secondary (segment-aware scope)', () => {
+ cy.visit(`http://localhost:${port}/tabs/tab1`);
+ cy.ionPageVisible('tab1');
+
+ cy.get('#tabs-secondary').click();
+ cy.ionPageVisible('tab1-secondary');
+ cy.ionPageDoesNotExist('tab1');
+ });
+
// Verifies fix for https://github.com/ionic-team/ionic-framework/issues/23087
it('should return to correct view and url when going back from child page after switching tabs', () => {
cy.visit(`http://localhost:${port}/tabs/tab1`);
diff --git a/packages/react-router/test/base/tests/e2e/specs/tail-slice-ambiguity.cy.js b/packages/react-router/test/base/tests/e2e/specs/tail-slice-ambiguity.cy.js
new file mode 100644
index 00000000000..8fbb4289211
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/specs/tail-slice-ambiguity.cy.js
@@ -0,0 +1,74 @@
+const port = 3000;
+
+/**
+ * Tests that derivePathnameToMatch's tail-slice heuristic does not produce
+ * false positive matches that incorrectly deactivate catch-all routes.
+ *
+ * Route structure:
+ * /tail-slice-ambiguity/*
+ * βββ index β ListPage
+ * βββ details/:id β DetailsPage
+ * βββ * β CatchAllPage
+ *
+ * Bug: navigating to /tail-slice-ambiguity/extra/details/99 after visiting
+ * /tail-slice-ambiguity/details/42 causes the tail-slice to extract
+ * ["details", "99"] which falsely matches details/:id, deactivating the
+ * catch-all page.
+ */
+describe('Tail-Slice Ambiguity', () => {
+ it('should show details page for /details/:id', () => {
+ cy.visit(`http://localhost:${port}/tail-slice-ambiguity`);
+ cy.ionPageVisible('tail-slice-list');
+
+ cy.get('#go-to-details').click();
+ cy.ionPageVisible('tail-slice-details');
+ cy.get('[data-testid="details-id"]').should('contain', 'Details ID: 42');
+ });
+
+ it('should show catch-all when path has extra segments before details', () => {
+ cy.visit(`http://localhost:${port}/tail-slice-ambiguity`);
+ cy.ionPageVisible('tail-slice-list');
+
+ // First create the details/:id view
+ cy.get('#go-to-details').click();
+ cy.ionPageVisible('tail-slice-details');
+ cy.get('[data-testid="details-id"]').should('contain', 'Details ID: 42');
+
+ // Go back to list
+ cy.get('#back-to-list').click();
+ cy.ionPageVisible('tail-slice-list');
+
+ // Navigate to ambiguous path - should show catch-all, NOT details
+ cy.get('#go-to-ambiguous').click();
+ cy.ionPageVisible('tail-slice-catchall');
+ cy.get('[data-testid="catchall-path"]').should('contain', '/tail-slice-ambiguity/extra/details/99');
+ });
+
+ it('should show catch-all for ambiguous path on direct navigation', () => {
+ cy.visit(`http://localhost:${port}/tail-slice-ambiguity/extra/details/99`);
+ cy.ionPageVisible('tail-slice-catchall');
+ cy.get('[data-testid="catchall-path"]').should('contain', '/tail-slice-ambiguity/extra/details/99');
+ });
+
+ it('should correctly navigate: details β list β ambiguous β back to list', () => {
+ cy.visit(`http://localhost:${port}/tail-slice-ambiguity`);
+ cy.ionPageVisible('tail-slice-list');
+
+ // Visit details
+ cy.get('#go-to-details').click();
+ cy.ionPageVisible('tail-slice-details');
+ cy.get('[data-testid="details-id"]').should('contain', 'Details ID: 42');
+
+ // Back to list
+ cy.get('#back-to-list').click();
+ cy.ionPageVisible('tail-slice-list');
+
+ // Visit ambiguous path (catch-all)
+ cy.get('#go-to-ambiguous').click();
+ cy.ionPageVisible('tail-slice-catchall');
+
+ // Back to list via browser back
+ cy.go('back');
+ cy.ionPageVisible('tail-slice-list');
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/specs/transition-unmount-guard.cy.js b/packages/react-router/test/base/tests/e2e/specs/transition-unmount-guard.cy.js
new file mode 100644
index 00000000000..98cc1e88277
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/specs/transition-unmount-guard.cy.js
@@ -0,0 +1,77 @@
+const port = 3000;
+
+describe('Transition Unmount Guard', () => {
+ /**
+ * Tests that rapid navigation away from a tabs view during a non-animated
+ * tab switch transition does not cause errors or stale state.
+ *
+ * The non-animated transition path in StackManager.transitionPage uses
+ * waitForComponentsReady() (MutationObserver + 100ms timeout) followed by
+ * nested requestAnimationFrame calls. If the component unmounts during this
+ * async window, the unmount guard should cancel in-flight rAFs and disconnect
+ * the MutationObserver to prevent DOM manipulation on detached elements.
+ */
+
+ it('should handle unmount during non-animated tab switch without errors', () => {
+ // Navigate to tabs from home (pushes /tabs/tab1 onto history)
+ cy.visit(`http://localhost:${port}/`);
+ cy.ionPageVisible('home');
+ cy.get('#go-to-tabs').click();
+ cy.ionPageVisible('tab1');
+
+ // Start a tab switch (non-animated transition) and immediately go back to home.
+ // History: /, /tabs/tab1, /tabs/tab2 β go(-2) jumps back to /
+ // This unmounts the tabs StackManager while the tab switch transition is in flight.
+ cy.get('ion-tab-button#tab-button-tab2').click();
+ cy.window().then((win) => win.history.go(-2));
+
+ // Home page should be visible and functional after the rapid unmount
+ cy.ionPageVisible('home');
+ });
+
+ it('should recover cleanly after unmount during transition and re-navigate to tabs', () => {
+ cy.visit(`http://localhost:${port}/`);
+ cy.ionPageVisible('home');
+
+ // First trip: navigate to tabs, switch tabs, immediately go back to home
+ cy.get('#go-to-tabs').click();
+ cy.ionPageVisible('tab1');
+ cy.get('ion-tab-button#tab-button-tab2').click();
+ cy.window().then((win) => win.history.go(-2));
+ cy.ionPageVisible('home');
+
+ // Second trip: navigate to tabs again to verify no stale state
+ cy.get('#go-to-tabs').click();
+ cy.ionPageVisible('tab1');
+
+ // Tab switching should still work correctly
+ cy.get('ion-tab-button#tab-button-tab2').click();
+ cy.ionPageVisible('tab2');
+ cy.ionPageHidden('tab1');
+
+ cy.get('ion-tab-button#tab-button-tab1').click();
+ cy.ionPageVisible('tab1');
+ cy.ionPageHidden('tab2');
+ });
+
+ it('should handle rapid repeated tab switches followed by unmount', () => {
+ cy.visit(`http://localhost:${port}/`);
+ cy.ionPageVisible('home');
+ cy.get('#go-to-tabs').click();
+ cy.ionPageVisible('tab1');
+
+ // Rapid tab switches to stack up multiple non-animated transitions
+ cy.get('ion-tab-button#tab-button-tab2').click();
+ cy.get('ion-tab-button#tab-button-tab1').click();
+ cy.get('ion-tab-button#tab-button-tab2').click();
+
+ // Navigate back to home while transitions may still be in flight.
+ // Multiple tab switches pushed extra entries, so go back enough to reach /.
+ cy.window().then((win) => win.history.go(-4));
+ cy.ionPageVisible('home');
+
+ // App should still be functional
+ cy.get('#go-to-tabs').click();
+ cy.ionPageVisible('tab1');
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/specs/wildcard-no-heuristic.cy.js b/packages/react-router/test/base/tests/e2e/specs/wildcard-no-heuristic.cy.js
new file mode 100644
index 00000000000..495029ab22f
--- /dev/null
+++ b/packages/react-router/test/base/tests/e2e/specs/wildcard-no-heuristic.cy.js
@@ -0,0 +1,105 @@
+const port = 3000;
+
+/**
+ * Tests that wildcard routing works with exact segment matching only,
+ * without relying on string-similarity heuristics.
+ *
+ * Route configuration: "page", "ab", and "*"
+ *
+ * These tests would fail with any prefix/similarity heuristic because
+ * the navigation targets intentionally share prefixes with specific routes:
+ * - "page2" shares prefix "page" with route "page"
+ * - "abc" shares prefix "ab" with route "ab"
+ * - "pager" shares prefix "page" with route "page"
+ */
+describe('Wildcard No Heuristic', () => {
+ it('should navigate to "page" specific route', () => {
+ cy.visit(`http://localhost:${port}/wildcard-no-heuristic`);
+ cy.ionPageVisible('heuristic-home');
+
+ cy.get('#go-to-page').click();
+ cy.ionPageVisible('heuristic-page');
+ cy.get('[data-testid="page-content"]').should('exist');
+ });
+
+ it('should navigate to "ab" specific route', () => {
+ cy.visit(`http://localhost:${port}/wildcard-no-heuristic`);
+ cy.ionPageVisible('heuristic-home');
+
+ cy.get('#go-to-ab').click();
+ cy.ionPageVisible('heuristic-ab');
+ cy.get('[data-testid="ab-content"]').should('exist');
+ });
+
+ it('should navigate to "page2" via wildcard (not confused with "page" route)', () => {
+ cy.visit(`http://localhost:${port}/wildcard-no-heuristic`);
+ cy.ionPageVisible('heuristic-home');
+
+ cy.get('#go-to-page2').click();
+ cy.ionPageVisible('heuristic-catchall');
+ cy.get('[data-testid="heuristic-catchall-content"]').should('exist');
+ });
+
+ it('should navigate to "abc" via wildcard (not confused with "ab" route)', () => {
+ cy.visit(`http://localhost:${port}/wildcard-no-heuristic`);
+ cy.ionPageVisible('heuristic-home');
+
+ cy.get('#go-to-abc').click();
+ cy.ionPageVisible('heuristic-catchall');
+ cy.get('[data-testid="heuristic-catchall-content"]').should('exist');
+ });
+
+ it('should navigate to "pager" via wildcard (not confused with "page" route)', () => {
+ cy.visit(`http://localhost:${port}/wildcard-no-heuristic`);
+ cy.ionPageVisible('heuristic-home');
+
+ cy.get('#go-to-pager').click();
+ cy.ionPageVisible('heuristic-catchall');
+ cy.get('[data-testid="heuristic-catchall-content"]').should('exist');
+ });
+
+ it('should navigate to "unknown" via wildcard', () => {
+ cy.visit(`http://localhost:${port}/wildcard-no-heuristic`);
+ cy.ionPageVisible('heuristic-home');
+
+ cy.get('#go-to-unknown').click();
+ cy.ionPageVisible('heuristic-catchall');
+ cy.get('[data-testid="heuristic-catchall-content"]').should('exist');
+ });
+
+ it('should load "page2" directly via wildcard on URL visit', () => {
+ cy.visit(`http://localhost:${port}/wildcard-no-heuristic/page2`);
+ cy.ionPageVisible('heuristic-catchall');
+ cy.get('[data-testid="heuristic-catchall-content"]').should('exist');
+ });
+
+ it('should load "abc" directly via wildcard on URL visit', () => {
+ cy.visit(`http://localhost:${port}/wildcard-no-heuristic/abc`);
+ cy.ionPageVisible('heuristic-catchall');
+ cy.get('[data-testid="heuristic-catchall-content"]').should('exist');
+ });
+
+ it('should load "page" directly as specific route on URL visit', () => {
+ cy.visit(`http://localhost:${port}/wildcard-no-heuristic/page`);
+ cy.ionPageVisible('heuristic-page');
+ cy.get('[data-testid="page-content"]').should('exist');
+ });
+
+ it('should navigate to wildcard, go back, then navigate to specific route', () => {
+ cy.visit(`http://localhost:${port}/wildcard-no-heuristic`);
+ cy.ionPageVisible('heuristic-home');
+
+ // Navigate to a wildcard route
+ cy.get('#go-to-page2').click();
+ cy.ionPageVisible('heuristic-catchall');
+
+ // Go back to home
+ cy.go('back');
+ cy.ionPageVisible('heuristic-home');
+
+ // Navigate to specific route β should work after returning from wildcard
+ cy.get('#go-to-page').click();
+ cy.ionPageVisible('heuristic-page');
+ cy.get('[data-testid="page-content"]').should('exist');
+ });
+});
diff --git a/packages/react-router/test/base/tests/e2e/support/commands.js b/packages/react-router/test/base/tests/e2e/support/commands.js
index 947a796157a..65a3f9f3a06 100644
--- a/packages/react-router/test/base/tests/e2e/support/commands.js
+++ b/packages/react-router/test/base/tests/e2e/support/commands.js
@@ -27,24 +27,10 @@
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
Cypress.Commands.add('ionPageVisible', (pageId) => {
- // cy.get(`div.ion-page[data-pageid=${pageId}]`)
- // .should('exist')
- // .should('not.have.class', 'ion-page-hidden')
- // .should('not.have.class', 'ion-page-visible')
-
cy.get(`div.ion-page[data-pageid=${pageId}]`)
.should('not.have.class', 'ion-page-hidden')
.should('not.have.class', 'ion-page-invisible')
.should('have.length', 1);
-
- // cy.get(`div.ion-page[data-pageid=${pageId}]`)
- // .should('not.have.class', 'ion-page')
- // .should('have.length', 1)
- // .not('')
- // .should('have.length', 1)
-
- // cy.get(`div.ion-page[data-pageid=${pageId}]`).should('not.have.class', 'ion-page-visible')
- // cy.get(`div.ion-page[data-pageid=${pageId}]`).should('have.attr', 'style', 'z-index: 101;')
});
Cypress.Commands.add('ionPageHidden', (pageId) => {
@@ -79,49 +65,42 @@ Cypress.Commands.add('ionSwipeToGoBack', (complete = false, selector = 'ion-rout
})
Cypress.Commands.add('ionMenuNav', (contains) => {
- // cy.get('ion-menu.show-menu').should('exist');
- // cy.wait(1000)
cy.contains('ion-item', contains).click({ force: true });
- // cy.get('div.ion-page').click();
- // cy.get('ion-menu').then(menu => {
- // cy.wait(1000)
- // menu[0].isOpen(open => {
- // if(open) {
- // menu[0].toggle()
- // }
- // cy.get('ion-menu.show-menu').should('not.exist');
- // })
- // })
- // cy.get('ion-menu.show-menu').should('not.exist');
-
- // cy.wait(1000)
- // cy.wait(1000)
- // cy.contains('ion-item', contains).click()
- // cy.contains('ion-item', contains).parent('ion-menu-toggle').click({ force: true });
+ cy.wait(250);
});
Cypress.Commands.add('ionTabClick', (tabText) => {
// TODO FW-2800: figure out how to get rid of this wait. Switching tabs after a forward nav to a details page needs it
cy.wait(500);
cy.contains('ion-tab-button', tabText).click({ force: true });
- // cy.get('ion-tab-button.tab-selected').contains(tabText)
+});
+
+Cypress.Commands.add('ionGoBack', (expectedUrlPart) => {
+ cy.go('back');
+ if (expectedUrlPart) {
+ cy.url().should('include', expectedUrlPart);
+ }
+ cy.wait(500);
+});
+
+Cypress.Commands.add('ionGoForward', (expectedUrlPart) => {
+ cy.go('forward');
+ if (expectedUrlPart) {
+ cy.url().should('include', expectedUrlPart);
+ }
+ cy.wait(500);
});
Cypress.Commands.add('ionBackClick', (pageId) => {
cy.get(`div.ion-page[data-pageid=${pageId}]`)
.should('be.visible', true)
- // .should('have.length', 1)
.find('ion-back-button')
.click();
});
Cypress.Commands.add('ionMenuClick', () => {
- // Todo: figure out how to get menu to close
- // cy.get(`div.ion-page[aria-hidden!=true]`)
- // .should('have.length', 1)
- // .find('ion-menu-button')
- // .click()
- // cy.get('ion-menu.show-menu').should('exist');
+ cy.get('ion-menu-button').first().click({ force: true });
+ cy.wait(500); // Wait for menu animation
});
Cypress.Commands.add('ionHardwareBackEvent', () => {
diff --git a/packages/react-router/test/base/tests/e2e/support/index.js b/packages/react-router/test/base/tests/e2e/support/index.js
index 37a498fb5bf..208452ffa62 100644
--- a/packages/react-router/test/base/tests/e2e/support/index.js
+++ b/packages/react-router/test/base/tests/e2e/support/index.js
@@ -18,3 +18,5 @@ import './commands';
// Alternatively you can use CommonJS syntax:
// require('./commands')
+
+require('cypress-terminal-report/src/installLogsCollector')();
diff --git a/packages/react-router/test/base/tsconfig.json b/packages/react-router/test/base/tsconfig.json
index e18c413ebfc..5bf6675b327 100644
--- a/packages/react-router/test/base/tsconfig.json
+++ b/packages/react-router/test/base/tsconfig.json
@@ -1,6 +1,6 @@
{
"compilerOptions": {
- "target": "es5",
+ "target": "ES2017",
"lib": [
"dom",
"dom.iterable",
diff --git a/packages/react/src/components/IonRedirect.tsx b/packages/react/src/components/IonRedirect.tsx
deleted file mode 100644
index 6b20376f104..00000000000
--- a/packages/react/src/components/IonRedirect.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from 'react';
-
-import { NavContext } from '../contexts/NavContext';
-
-export interface IonRedirectProps {
- path?: string;
- exact?: boolean;
- to: string;
- routerOptions?: unknown;
-}
-
-// eslint-disable-next-line @typescript-eslint/no-empty-interface
-interface IonRedirectState {}
-
-export class IonRedirect extends React.PureComponent {
- context!: React.ContextType;
-
- render() {
- const IonRedirectInner = this.context.getIonRedirect();
-
- if (!this.context.hasIonicRouter() || !IonRedirect) {
- console.error(
- 'You either do not have an Ionic Router package, or your router does not support using '
- );
- return null;
- }
-
- return ;
- }
-
- static get contextType() {
- return NavContext;
- }
-}
diff --git a/packages/react/src/components/IonRoute.tsx b/packages/react/src/components/IonRoute.tsx
index c7dc0b1af40..29e1708fd63 100644
--- a/packages/react/src/components/IonRoute.tsx
+++ b/packages/react/src/components/IonRoute.tsx
@@ -4,9 +4,10 @@ import { NavContext } from '../contexts/NavContext';
export interface IonRouteProps {
path?: string;
- exact?: boolean;
+ index?: boolean;
+ caseSensitive?: boolean;
show?: boolean;
- render: (props?: any) => JSX.Element; // TODO(FW-2959): type
+ element: React.ReactElement;
disableIonPageManagement?: boolean;
}
diff --git a/packages/react/src/components/IonRouterOutlet.tsx b/packages/react/src/components/IonRouterOutlet.tsx
index d7c75b13189..2fe25df3893 100644
--- a/packages/react/src/components/IonRouterOutlet.tsx
+++ b/packages/react/src/components/IonRouterOutlet.tsx
@@ -3,6 +3,7 @@ import React from 'react';
import { NavContext } from '../contexts/NavContext';
import OutletPageManager from '../routing/OutletPageManager';
+import { generateId } from '../utils/generateId';
import type { IonicReactProps } from './IonicReactProps';
import { IonRouterOutletInner } from './inner-proxies';
@@ -12,6 +13,7 @@ type Props = LocalJSX.IonRouterOutlet & {
basePath?: string;
ref?: React.Ref;
ionPage?: boolean;
+ id?: string;
};
interface InternalProps extends Props {
@@ -23,14 +25,17 @@ interface InternalState {}
class IonRouterOutletContainer extends React.Component {
context!: React.ContextType;
+ private readonly outletId: string;
constructor(props: InternalProps) {
super(props);
+ this.outletId = props.id ?? `routerOutlet-${generateId('routerOutlet')}`;
}
render() {
const StackManager = this.context.getStackManager();
const { children, forwardedRef, ...props } = this.props;
+ const outletId = props.id ?? this.outletId;
return this.context.hasIonicRouter() ? (
props.ionPage ? (
@@ -38,8 +43,8 @@ class IonRouterOutletContainer extends React.Component
) : (
-
-
+
+
{children}
diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts
index 12ce0320d76..7606b2b543a 100644
--- a/packages/react/src/components/index.ts
+++ b/packages/react/src/components/index.ts
@@ -125,7 +125,7 @@ export { IonBackButton } from './navigation/IonBackButton';
export { IonRouterOutlet } from './IonRouterOutlet';
export { IonIcon } from './IonIcon';
export * from './IonRoute';
-export * from './IonRedirect';
+
export * from './IonRouterContext';
// Utils
diff --git a/packages/react/src/components/navigation/IonTabBar.tsx b/packages/react/src/components/navigation/IonTabBar.tsx
index 124b00f6990..6f088c1cab4 100644
--- a/packages/react/src/components/navigation/IonTabBar.tsx
+++ b/packages/react/src/components/navigation/IonTabBar.tsx
@@ -50,7 +50,9 @@ const matchesTab = (pathname: string, href: string | undefined): boolean => {
return false;
}
- const normalizedHref = href.endsWith('/') && href !== '/' ? href.slice(0, -1) : href;
+ // Strip query string before comparing β href may contain search params (e.g., "/tabs/home?foo=bar")
+ const hrefPathname = href.split('?')[0];
+ const normalizedHref = hrefPathname.endsWith('/') && hrefPathname !== '/' ? hrefPathname.slice(0, -1) : hrefPathname;
return pathname === normalizedHref || pathname.startsWith(normalizedHref + '/');
};
@@ -164,7 +166,7 @@ class IonTabBarUnwrapped extends React.PureComponent any;
- getIonRedirect: () => any;
- getPageManager: () => any;
getStackManager: () => any;
goBack: (route?: string | RouteInfo, animationBuilder?: AnimationBuilder) => void;
navigate: (
@@ -27,9 +25,7 @@ export interface NavContextState {
}
export const NavContext = /*@__PURE__*/ React.createContext({
- getIonRedirect: () => undefined,
getIonRoute: () => undefined,
- getPageManager: () => undefined,
getStackManager: () => undefined,
goBack: (route?: string | RouteInfo) => {
if (typeof window !== 'undefined') {
diff --git a/packages/react/src/routing/LocationHistory.ts b/packages/react/src/routing/LocationHistory.ts
index e99e5a88cc9..58fd2055fe6 100644
--- a/packages/react/src/routing/LocationHistory.ts
+++ b/packages/react/src/routing/LocationHistory.ts
@@ -91,7 +91,18 @@ export class LocationHistory {
private _replace(routeInfo: RouteInfo) {
const routeInfos = this._getRouteInfosByKey(routeInfo.tab);
routeInfos && routeInfos.pop();
- this.locationHistory.pop();
+
+ // Get the current route that's being replaced
+ const currentRoute = this.locationHistory[this.locationHistory.length - 1];
+
+ // Only pop from global history if we're replacing in the same outlet context.
+ // Don't pop if we're entering a nested outlet (current route has no tab, new route has a tab)
+ const isEnteringNestedOutlet = currentRoute && !currentRoute.tab && !!routeInfo.tab;
+
+ if (!isEnteringNestedOutlet) {
+ this.locationHistory.pop();
+ }
+
this._add(routeInfo);
}
@@ -128,6 +139,21 @@ export class LocationHistory {
return undefined;
}
+ /**
+ * Returns the most recent RouteInfo in global history (excluding the current
+ * entry) whose pathname matches the given value. Unlike findLastLocation,
+ * this search is tab-agnostic. Used by the multi-step back detection.
+ */
+ findLastLocationByPathname(pathname: string) {
+ for (let i = this.locationHistory.length - 2; i >= 0; i--) {
+ const ri = this.locationHistory[i];
+ if (ri && ri.pathname === pathname) {
+ return ri;
+ }
+ }
+ return undefined;
+ }
+
findLastLocation(routeInfo: RouteInfo) {
const routeInfos = this._getRouteInfosByKey(routeInfo.tab);
if (routeInfos) {
@@ -164,4 +190,16 @@ export class LocationHistory {
canGoBack() {
return this.locationHistory.length > 1;
}
+
+ findTabForPathname(pathname: string): string | undefined {
+ for (const tab of Object.keys(this.tabHistory)) {
+ const routeInfos = this.tabHistory[tab];
+ for (let i = routeInfos.length - 1; i >= 0; i--) {
+ if (routeInfos[i].pathname === pathname) {
+ return tab;
+ }
+ }
+ }
+ return undefined;
+ }
}
diff --git a/packages/react/src/routing/NavManager.tsx b/packages/react/src/routing/NavManager.tsx
index 27396947800..f0b686257ec 100644
--- a/packages/react/src/routing/NavManager.tsx
+++ b/packages/react/src/routing/NavManager.tsx
@@ -11,7 +11,6 @@ import type { RouterDirection } from '../models/RouterDirection';
import type { RouterOptions } from '../models/RouterOptions';
import type { LocationHistory } from './LocationHistory';
-import PageManager from './PageManager';
// TODO(FW-2959): types
@@ -30,7 +29,6 @@ interface NavManagerProps {
onSetCurrentTab: (tab: string, routeInfo: RouteInfo) => void;
onChangeTab: (tab: string, path: string, routeOptions?: any) => void;
onResetTab: (tab: string, path: string, routeOptions?: any) => void;
- ionRedirect: any;
ionRoute: any;
stackManager: any;
locationHistory: LocationHistory;
@@ -61,10 +59,8 @@ export class NavManager extends React.PureComponent true,
navigate: this.navigate.bind(this),
- getIonRedirect: this.getIonRedirect.bind(this),
getIonRoute: this.getIonRoute.bind(this),
getStackManager: this.getStackManager.bind(this),
- getPageManager: this.getPageManager.bind(this),
routeInfo: this.props.routeInfo,
setCurrentTab: this.props.onSetCurrentTab,
changeTab: this.props.onChangeTab,
@@ -111,14 +107,6 @@ export class NavManager extends React.PureComponent;
routeInfo?: RouteInfo;
StackManager: any; // TODO(FW-2959): type
+ id?: string;
}
export class OutletPageManager extends React.Component {
@@ -83,14 +84,15 @@ export class OutletPageManager extends React.Component {
}
render() {
- const { StackManager, children, routeInfo, ...props } = this.props;
+ const { StackManager, children, routeInfo, id, ...props } = this.props;
return (
{(context) => {
this.ionLifeCycleContext = context;
return (
-
+
(this.ionRouterOutlet = val)}
{...props}
>
diff --git a/packages/react/src/routing/PageManager.tsx b/packages/react/src/routing/PageManager.tsx
index 19a184a6364..33dfc8210d8 100644
--- a/packages/react/src/routing/PageManager.tsx
+++ b/packages/react/src/routing/PageManager.tsx
@@ -22,7 +22,18 @@ export class PageManager extends React.PureComponent {
super(props);
this.ionPageElementRef = React.createRef();
// React refs must be stable (not created inline).
- this.stableMergedRefs = mergeRefs(this.ionPageElementRef, this.props.forwardedRef);
+ // Wrap merged refs to add ion-page-invisible synchronously when element is created
+ const baseMergedRefs = mergeRefs(this.ionPageElementRef, this.props.forwardedRef);
+ this.stableMergedRefs = (node: HTMLDivElement | null) => {
+ if (node && !node.classList.contains('ion-page-invisible') && !node.classList.contains('ion-page-hidden')) {
+ // Add ion-page-invisible synchronously before first paint (if in an outlet)
+ // This prevents the flash that occurs when componentDidMount runs after paint
+ if (this.context?.isInOutlet?.()) {
+ node.classList.add('ion-page-invisible');
+ }
+ }
+ baseMergedRefs(node);
+ };
/**
* This binds the scope of the following methods to the class scope.
@@ -36,11 +47,42 @@ export class PageManager extends React.PureComponent {
this.ionViewDidLeaveHandler = this.ionViewDidLeaveHandler.bind(this);
}
+ private parseClasses(className: string | undefined): Set {
+ if (!className) return new Set();
+ return new Set(className.split(/\s+/).filter(Boolean));
+ }
+
+ /**
+ * Updates classList by diffing old/new className props.
+ * Preserves framework-added classes (can-go-back, ion-page-invisible, etc.).
+ */
+ private updateUserClasses(oldClassName: string | undefined, newClassName: string | undefined) {
+ if (!this.ionPageElementRef.current) return;
+
+ const oldClasses = this.parseClasses(oldClassName);
+ const newClasses = this.parseClasses(newClassName);
+
+ oldClasses.forEach((cls) => {
+ if (!newClasses.has(cls)) {
+ this.ionPageElementRef.current!.classList.remove(cls);
+ }
+ });
+
+ newClasses.forEach((cls) => {
+ if (!oldClasses.has(cls)) {
+ this.ionPageElementRef.current!.classList.add(cls);
+ }
+ });
+ }
+
componentDidMount() {
if (this.ionPageElementRef.current) {
- if (this.context.isInOutlet()) {
- this.ionPageElementRef.current.classList.add('ion-page-invisible');
- }
+ // Add user classes via DOM manipulation to preserve framework-added classes.
+ // We only set "ion-page" in JSX; user classes are added here.
+ // Note: ion-page-invisible is added in the ref callback (stableMergedRefs) to prevent flash.
+ // The ref callback runs synchronously when the element is created, before the browser paints.
+ this.updateUserClasses(undefined, this.props.className);
+
this.context.registerIonPage(this.ionPageElementRef.current, this.props.routeInfo!);
this.ionPageElementRef.current.addEventListener('ionViewWillEnter', this.ionViewWillEnterHandler);
this.ionPageElementRef.current.addEventListener('ionViewDidEnter', this.ionViewDidEnterHandler);
@@ -49,6 +91,12 @@ export class PageManager extends React.PureComponent {
}
}
+ componentDidUpdate(prevProps: PageManagerProps) {
+ if (prevProps.className !== this.props.className) {
+ this.updateUserClasses(prevProps.className, this.props.className);
+ }
+ }
+
componentWillUnmount() {
if (this.ionPageElementRef.current) {
this.ionPageElementRef.current.removeEventListener('ionViewWillEnter', this.ionViewWillEnterHandler);
@@ -84,12 +132,14 @@ export class PageManager extends React.PureComponent {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { className, children, routeInfo, forwardedRef, ...props } = this.props;
+ // Only set "ion-page" in JSX. User classes are managed via DOM in componentDidMount/componentDidUpdate
+ // to preserve framework-added classes (can-go-back, ion-page-invisible, etc.) when className prop changes.
return (
{(context) => {
this.ionLifeCycleContext = context;
return (
-
+
{children}
);
diff --git a/packages/react/src/routing/RouteManagerContext.ts b/packages/react/src/routing/RouteManagerContext.ts
index 32cfa6c50f8..f044c04b590 100644
--- a/packages/react/src/routing/RouteManagerContext.ts
+++ b/packages/react/src/routing/RouteManagerContext.ts
@@ -21,8 +21,14 @@ export interface RouteManagerContextState {
outletId: string,
ionRouterOutlet: React.ReactElement,
routeInfo: RouteInfo,
- reRender: () => void
+ reRender: () => void,
+ parentPathnameBase?: string
) => React.ReactNode[];
+ /**
+ * Returns all view items currently registered for a given outlet id.
+ * Used by StackManager for out-of-scope cleanup.
+ */
+ getViewItemsForOutlet: (outletId: string) => ViewItem[];
goBack: () => void;
unMountViewItem: (viewItem: ViewItem) => void;
}
@@ -37,6 +43,7 @@ export const RouteManagerContext = /*@__PURE__*/ React.createContext
undefined,
findViewItemByRouteInfo: () => undefined,
getChildrenToRender: () => undefined as any,
+ getViewItemsForOutlet: () => [] as any,
goBack: () => undefined,
unMountViewItem: () => undefined,
});
diff --git a/packages/react/src/routing/ViewStacks.ts b/packages/react/src/routing/ViewStacks.ts
index 3a7bba3a77d..22e6486f4eb 100644
--- a/packages/react/src/routing/ViewStacks.ts
+++ b/packages/react/src/routing/ViewStacks.ts
@@ -22,10 +22,7 @@ export abstract class ViewStacks {
}
clear(outletId: string) {
- // Give some time for the leaving views to transition before removing
- return setTimeout(() => {
- delete this.viewStacks[outletId];
- }, 500);
+ delete this.viewStacks[outletId];
}
getViewItemsForOutlet(outletId: string) {
@@ -75,6 +72,6 @@ export abstract class ViewStacks {
ionRouterOutlet: React.ReactElement,
routeInfo: RouteInfo,
reRender: () => void,
- setInTransition: () => void
+ parentPathnameBase?: string
): React.ReactNode[];
}
diff --git a/packages/react/test/apps/react17/package.json b/packages/react/test/apps/react17/package.json
index 68079579bba..f05a10f55de 100644
--- a/packages/react/test/apps/react17/package.json
+++ b/packages/react/test/apps/react17/package.json
@@ -2,18 +2,22 @@
"name": "test-app",
"version": "0.0.1",
"private": true,
+ "overrides": {
+ "@ionic/react-router": {
+ "react-router": "$react-router",
+ "react-router-dom": "$react-router-dom"
+ }
+ },
"dependencies": {
"@ionic/react": "^6.6.1",
"@ionic/react-router": "^6.6.1",
"@types/react": "^17.0.53",
"@types/react-dom": "^17.0.19",
- "@types/react-router": "^5.1.20",
- "@types/react-router-dom": "^5.3.3",
"ionicons": "^8.0.13",
"react": "^17.0.2",
"react-dom": "^17.0.2",
- "react-router": "^5.3.4",
- "react-router-dom": "^5.3.4",
+ "react-router": "^6.0.0",
+ "react-router-dom": "^6.0.0",
"react-scripts": "^5.0.0",
"typescript": "^4.1.3"
},
diff --git a/packages/react/test/apps/react18/package.json b/packages/react/test/apps/react18/package.json
index 65ab9e00239..1c401656216 100644
--- a/packages/react/test/apps/react18/package.json
+++ b/packages/react/test/apps/react18/package.json
@@ -2,14 +2,20 @@
"name": "test-app",
"version": "0.0.1",
"private": true,
+ "overrides": {
+ "@ionic/react-router": {
+ "react-router": "$react-router",
+ "react-router-dom": "$react-router-dom"
+ }
+ },
"dependencies": {
"@ionic/react": "^7.0.0",
"@ionic/react-router": "^7.0.0",
"ionicons": "^8.0.13",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "react-router": "^5.3.4",
- "react-router-dom": "^5.3.4"
+ "react-router": "^6.0.0",
+ "react-router-dom": "^6.0.0"
},
"scripts": {
"dev": "vite",
@@ -27,9 +33,6 @@
"@testing-library/user-event": "^14.4.3",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
- "@types/react-router": "^5.1.20",
- "@types/react-router-dom": "^5.3.3",
- "@vitejs/plugin-legacy": "^4.0.2",
"@vitejs/plugin-react": "^4.0.1",
"concurrently": "^6.3.0",
"cypress": "^13.2.0",
diff --git a/packages/react/test/apps/react18/vite.config.ts b/packages/react/test/apps/react18/vite.config.ts
index 20e6c2a8071..a5f03b990f4 100644
--- a/packages/react/test/apps/react18/vite.config.ts
+++ b/packages/react/test/apps/react18/vite.config.ts
@@ -1,4 +1,3 @@
-import legacy from '@vitejs/plugin-legacy'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
@@ -6,7 +5,6 @@ import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
react(),
- legacy()
],
test: {
globals: true,
diff --git a/packages/react/test/apps/react19/package.json b/packages/react/test/apps/react19/package.json
index 67e3bf1db83..028e792fe9d 100644
--- a/packages/react/test/apps/react19/package.json
+++ b/packages/react/test/apps/react19/package.json
@@ -2,14 +2,20 @@
"name": "test-app",
"version": "0.0.1",
"private": true,
+ "overrides": {
+ "@ionic/react-router": {
+ "react-router": "$react-router",
+ "react-router-dom": "$react-router-dom"
+ }
+ },
"dependencies": {
"@ionic/react": "^8.4.0",
"@ionic/react-router": "^8.4.0",
"ionicons": "^8.0.13",
"react": "19.0.0",
"react-dom": "19.0.0",
- "react-router": "^5.3.4",
- "react-router-dom": "^5.3.4"
+ "react-router": "^6.0.0",
+ "react-router-dom": "^6.0.0"
},
"scripts": {
"dev": "vite",
@@ -22,15 +28,15 @@
"e2e": "concurrently \"serve -s dist -l 3000\" \"wait-on http-get://localhost:3000 && npm run cypress\" --kill-others --success first"
},
"devDependencies": {
+ "@testing-library/dom": "^10.0.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.4.3",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
- "@types/react-router": "^5.1.20",
- "@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-legacy": "^4.0.2",
"@vitejs/plugin-react": "^4.0.1",
+ "terser": "^5.16.0",
"concurrently": "^6.3.0",
"cypress": "^13.2.0",
"eslint": "^8.35.0",
diff --git a/packages/react/test/base/scripts/sync.sh b/packages/react/test/base/scripts/sync.sh
index a6b441d8b5c..7c0955602b0 100755
--- a/packages/react/test/base/scripts/sync.sh
+++ b/packages/react/test/base/scripts/sync.sh
@@ -15,4 +15,7 @@ npm pack ../../../
npm pack ../../../../react-router
# Install Dependencies
-npm install *.tgz --no-save
+# TODO: Remove --legacy-peer-deps once @ionic/react peer deps align with test app versions.
+# Currently needed because packed tarballs may have peer dep ranges that conflict with
+# the specific React/React-Router versions in test apps.
+npm install *.tgz --no-save --legacy-peer-deps
diff --git a/packages/react/test/base/src/App.tsx b/packages/react/test/base/src/App.tsx
index c8ea117f60e..5ab6a0e6aff 100644
--- a/packages/react/test/base/src/App.tsx
+++ b/packages/react/test/base/src/App.tsx
@@ -1,7 +1,7 @@
import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router';
import React from 'react';
-import { Route } from 'react-router-dom';
+import { Route } from 'react-router';
/* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css';
@@ -46,33 +46,33 @@ const App: React.FC = () => (
-
-
-
-
+ } />
+ } />
+ } />
+ } />
}
/>
-
+ } />
}
/>
}
/>
-
-
-
-
-
-
-
-
-
-
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
diff --git a/packages/react/test/base/src/pages/Tabs.tsx b/packages/react/test/base/src/pages/Tabs.tsx
index 2098bfcb266..89c82fd2c53 100644
--- a/packages/react/test/base/src/pages/Tabs.tsx
+++ b/packages/react/test/base/src/pages/Tabs.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs, IonPage } from '@ionic/react';
-import { Route, Redirect } from 'react-router';
+import { Route, Navigate } from 'react-router';
interface TabsProps {}
@@ -9,8 +9,8 @@ const Tabs: React.FC = () => {
-
- Tab 1 } />
+ } />
+ Tab 1} />
window.alert('Tab was clicked')}>
diff --git a/packages/react/test/base/src/pages/TabsDirectNavigation.tsx b/packages/react/test/base/src/pages/TabsDirectNavigation.tsx
index 2e412e174ab..c02589ad6c6 100644
--- a/packages/react/test/base/src/pages/TabsDirectNavigation.tsx
+++ b/packages/react/test/base/src/pages/TabsDirectNavigation.tsx
@@ -1,7 +1,7 @@
import { IonContent, IonHeader, IonIcon, IonLabel, IonPage, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs, IonTitle, IonToolbar } from '@ionic/react';
import { homeOutline, radioOutline, libraryOutline, searchOutline } from 'ionicons/icons';
import React from 'react';
-import { Route, Redirect } from 'react-router-dom';
+import { Route, Navigate } from 'react-router';
const HomePage: React.FC = () => (
@@ -59,11 +59,11 @@ const TabsDirectNavigation: React.FC = () => {
return (
-
- } exact={true} />
- } exact={true} />
- } exact={true} />
- } exact={true} />
+ } />
+ } />
+ } />
+ } />
+ } />
diff --git a/packages/react/test/base/src/pages/TabsSimilarPrefixes.tsx b/packages/react/test/base/src/pages/TabsSimilarPrefixes.tsx
index 78672dc075d..ee6e07540d4 100644
--- a/packages/react/test/base/src/pages/TabsSimilarPrefixes.tsx
+++ b/packages/react/test/base/src/pages/TabsSimilarPrefixes.tsx
@@ -13,7 +13,7 @@ import {
} from '@ionic/react';
import { homeOutline, radioOutline, libraryOutline } from 'ionicons/icons';
import React from 'react';
-import { Route, Redirect } from 'react-router-dom';
+import { Route, Navigate } from 'react-router';
const HomePage: React.FC = () => (
@@ -58,10 +58,10 @@ const TabsSimilarPrefixes: React.FC = () => {
return (
-
- } exact={true} />
- } exact={true} />
- } exact={true} />
+ } />
+ } />
+ } />
+ } />
diff --git a/packages/react/test/base/src/pages/overlay-components/ModalSheetChildRoute.tsx b/packages/react/test/base/src/pages/overlay-components/ModalSheetChildRoute.tsx
index e686bbd00eb..976352cb847 100644
--- a/packages/react/test/base/src/pages/overlay-components/ModalSheetChildRoute.tsx
+++ b/packages/react/test/base/src/pages/overlay-components/ModalSheetChildRoute.tsx
@@ -38,7 +38,7 @@ const ModalSheetChildRouteParent: React.FC = () => {
-
+ } />
);
diff --git a/packages/react/test/base/src/pages/overlay-components/OverlayComponents.tsx b/packages/react/test/base/src/pages/overlay-components/OverlayComponents.tsx
index 75bf98bb734..1381ed14763 100644
--- a/packages/react/test/base/src/pages/overlay-components/OverlayComponents.tsx
+++ b/packages/react/test/base/src/pages/overlay-components/OverlayComponents.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react';
-import { Route, Redirect } from 'react-router';
+import { Route, Navigate } from 'react-router';
import {
addCircleOutline,
alarm,
@@ -27,17 +27,17 @@ const OverlayHooks: React.FC = () => {
return (
-
-
-
-
-
-
-
-
-
-
-
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
diff --git a/packages/react/test/base/src/pages/overlay-hooks/OverlayHooks.tsx b/packages/react/test/base/src/pages/overlay-hooks/OverlayHooks.tsx
index 4cc9bf88ddd..8036c0efbee 100644
--- a/packages/react/test/base/src/pages/overlay-hooks/OverlayHooks.tsx
+++ b/packages/react/test/base/src/pages/overlay-hooks/OverlayHooks.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react';
-import { Route, Redirect } from 'react-router';
+import { Route, Navigate } from 'react-router';
import ActionSheetHook from './ActionSheetHook';
import {
addCircleOutline,
@@ -24,14 +24,14 @@ const OverlayHooks: React.FC = () => {
return (
-
-
-
-
-
-
-
-
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />