Skip to content

Commit 1b9d026

Browse files
authored
feat(context-navigation): add plugin package, dev portal integration, and plugin docs (#4751)
* feat(context-navigation): add context navigation plugin package Introduce a new plugin that manages bidirectional synchronization between the browser URL and the selected context (e.g. facility, project). Supports three routing strategies: - 'path': context ID embedded in the URL path segment - 'query': context ID as a query parameter (?contextId=...) - 'custom': consumer-provided adapter for non-standard URL layouts Key capabilities: - URL guard that intercepts navigation to resolve context from URLs - Reconciler that updates the URL when context changes programmatically - App-first and context-first source factories for initial resolution - Active-app navigation event stream for cross-app context handoff - Legacy app path normalization for backward compatibility Includes full test coverage for plugin lifecycle, guard handlers, reconciliation logic, and navigation event operators. * feat(context): add routing strategy and navigation fixes Extend the context module with a routingStrategy configuration option that controls how context identity is represented in URLs ('path' or 'query'). This is the foundation the context-navigation plugin reads to decide which adapter to use. Context module changes: - Add routingStrategy to ContextModuleConfig and configurator - Add setRoutingStrategy() to ContextConfigBuilder - Update resolveContextFromPath with custom matcher support and TSDoc - Simplify resolveInitialContext to only resolve from parent context (URL-based resolution is now handled by the plugin) - Expose routingStrategy on ContextProvider and IContextProvider - Add routingStrategy as third argument to generatePathFromContext - Add migration guide for routing strategy adoption Navigation module changes: - Stop stripping trailing slashes in normalizePathname - Fix basename boundary check in _isWithinBasenameScope to prevent false positives from overlapping app name prefixes - Fall back to '/' in _localizePath when basename-stripped path is empty * feat(dev-portal): integrate context navigation plugin Wire up the context-navigation plugin in the dev portal so that apps running in the development shell get automatic URL-to-context synchronization matching production behavior. Dev portal changes: - Enable context-navigation plugin with setPortalName, setDebug, and setUrlGuard in dev portal config - Enable context module with path generator and extractor using plugin utility functions (buildContextUrlForStrategy, resolveContextIdFromUrl) - Set explicit 'path' routing strategy - Delete useAppContextNavigation hook (replaced by plugin) - Remove useAppContextNavigation() call from Router component - Update README to document context navigation plugin integration Cookbook changes: - Add explicit setRoutingStrategy('path') to context-using cookbook configs (app-react-context, app-react-bookmark, app-react-bookmark-advanced, app-react-context-custom-error, portal-analytics) * docs: add plugin documentation, migration guides, and changesets VuePress documentation: - Add top-level plugins section with overview of plugin vs module intent and the context-navigation plugin README - Add context routing strategy migration guide under modules/context - Update sidebar and theme config for new plugin section Changesets: - plugin-context-navigation: initial release (minor) - module-context: routing strategy addition (minor) - module-navigation: trailing-slash and basename fix (patch) - dev-portal: context navigation integration (patch) Also updates pnpm-lock.yaml for new plugin package dependency. * docs: improve context routing documentation and fix TSDoc accuracy * fix(changeset): correct context module changeset to reflect actual changes vs main The previous changeset described removing routingStrategy from the context module, but that feature never existed on main. The actual change is moving URL-based initial context resolution from the context module to the context-navigation plugin. Changes: - Downgrade from major to patch (no breaking changes vs main) - Accurately describe the URL resolution responsibility shift - Remove references to routingStrategy removal (never existed on main) - Keep migration guidance focused on plugin enablement * fix(context-provider): suppress TypeScript errors for future signature updates * fix(navigation): normalize basename to treat '/' as no-basename and fix scope checks * fix(context-navigation): force replace:true on app switch and URL drift correction to prevent back-navigation traps * fix(navigation): prevent ReDoS vulnerability in pathname normalization Replace regex-based slash collapsing with iterative approach to eliminate potential Regular Expression Denial of Service attack when processing user-controlled basename values with pathological input. The normalizePathname function now guarantees O(n) linear time complexity instead of using /\/+/g regex pattern that could be exploited with thousands of consecutive slashes. Adds ReDoS protection test to verify performance with pathological input. Resolves CodeQL security alert: Polynomial regular expression used on uncontrolled data (High severity) Related: #4751 * fix(context-navigation): clear hash fragment on context changes Hash fragments are intentionally cleared when context changes to prevent inconsistent app state. When a context change resets the app to its root view, preserving the hash would leave anchors pointing to sections that may not exist in the new context. This matches the existing behavior for sub-routes (intentionally dropped) and maintains consistency across all three adapters (path, query, custom). Adds test to verify hash is cleared on context change. Addresses PR review feedback in #4751 * fix(navigation): eliminate remaining ReDoS vulnerability in trailing slash removal Replace regex-based trailing slash removal (.replace(/\/+$/, '')) with iterative stripTrailingSlashes() function to prevent ReDoS attacks. The /\/+$/ pattern can cause exponential backtracking when processing basenames with thousands of trailing slashes. The new stripTrailingSlashes function guarantees O(n) linear time by scanning backwards from the end. Added test verifying performance with 10k trailing slashes (< 100ms). This completes the ReDoS vulnerability remediation - both consecutive slash patterns are now safe: - normalizePathname: O(n) forward scan (commit ee14d80) - stripTrailingSlashes: O(n) backward scan (this commit) Addresses CodeQL alert: Polynomial regular expression used on uncontrolled data * fix(navigation): refactor pathname normalization functions to improve performance and avoid ReDoS vulnerabilities * refactor: improve readability by adding braces to conditional statements and using array for path construction * chore: remove unnecessary blank line in handleReplaceModeGuard function
1 parent c4b4ab1 commit 1b9d026

87 files changed

Lines changed: 6718 additions & 1163 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@equinor/fusion-framework-dev-portal": patch
3+
---
4+
5+
Integrate `@equinor/fusion-framework-plugin-context-navigation` into the dev portal.
6+
7+
Portal context-to-URL reconciliation is now handled by `@equinor/fusion-framework-plugin-context-navigation`, replacing the ad-hoc hook-based approach.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@equinor/fusion-framework-module-context": patch
3+
---
4+
5+
Move URL-based initial context resolution to the context-navigation plugin.
6+
7+
The context module's `resolveInitialContext` no longer resolves context from the URL path — that responsibility has been moved to `@equinor/fusion-framework-plugin-context-navigation` at the portal level. This decouples URL concerns from the context module.
8+
9+
**Changes:**
10+
11+
- `resolveInitialContext` no longer accepts the `options` parameter with path resolution config.
12+
- URL-based initial context resolution is now handled by the context-navigation plugin.
13+
- Added `version` property to `IContextProvider` interface (non-breaking).
14+
- Added explanatory `@ts-ignore` directives on compatibility assignments to avoid a breaking API signature change in this release.
15+
16+
**Migration:** Apps do not need to change their code. Portal hosts should enable the `@equinor/fusion-framework-plugin-context-navigation` plugin to restore URL-based context resolution behavior.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@equinor/fusion-framework-module-navigation": patch
3+
---
4+
5+
Fix basename boundary matching and trailing-slash handling.
6+
7+
- `normalizePathname` no longer strips trailing slashes — only collapses consecutive slashes. Trailing slash is now preserved as part of the path identity.
8+
- `_isWithinBasenameScope` uses a path-boundary check (`pathname === basename || pathname.startsWith(basename + '/')`) to prevent false positives from apps with overlapping name prefixes (e.g. `/apps/my-app` no longer matches `/apps/my-app-other/foo`).
9+
- `_localizePath` falls back to `'/'` when the basename-stripped pathname is empty.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@equinor/fusion-framework-plugin-context-navigation": minor
3+
---
4+
5+
Initial release of the context navigation plugin.
6+
7+
Adapter-based, event-driven plugin that reconciles context selection with the browser URL for portal hosts. Ships with built-in adapters for query-param, path-segment, and custom URL shapes, plus two pre-wired source strategies:
8+
9+
- **app-first** — app sets context, the plugin encodes it to the URL.
10+
- **context-first** — the plugin decodes context from the URL on startup, redirects to a configurable null-context URL when no context is resolvable.
11+
12+
```ts
13+
import { enableContextNavigation } from '@equinor/fusion-framework-plugin-context-navigation';
14+
import { createAppFirstSource } from '@equinor/fusion-framework-plugin-context-navigation/sources';
15+
16+
enableContextNavigation(configurator, (builder) => {
17+
builder.setSourceFactory(createAppFirstSource());
18+
});
19+
```
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@equinor/fusion-framework-module-navigation": patch
3+
---
4+
5+
**Security Fix**: Replaced regex-based pathname normalization with iterative approach to prevent potential ReDoS (Regular Expression Denial of Service) vulnerability when processing user-controlled basename values.
6+
7+
The `normalizePathname` function now uses a simple character-by-character scan instead of `/\/+/g` regex, ensuring O(n) linear time complexity even with pathological input containing thousands of consecutive slashes.
8+
9+
This addresses CodeQL security alert: "Polynomial regular expression used on uncontrolled data"
10+
11+
Related: #4751

CODEMAP.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ search to rediscover it.
1313

1414
| Path | Contains | Published? |
1515
| --- | --- | --- |
16-
| `packages/*` | Framework libraries (58 packages) | Yes, via Changesets |
16+
| `packages/*` | Framework libraries (59 packages) | Yes, via Changesets |
1717
| `cookbooks/*` | Runnable example apps and portals | Yes (versioned, but examples) |
1818
| `eds-content/`, `eds/` | EDS design-system content and token tooling | No |
1919
| `vue-press/` | Documentation site | Partly |
@@ -82,7 +82,13 @@ Format: `package name` → path → role.
8282
| `@equinor/fusion-framework-react-components-bookmark` | `packages/react/components/bookmark` | Bookmark UI components |
8383
| `@equinor/fusion-framework-react-components-people-provider` | `packages/react/components/people-resolver` | People resolver components |
8484

85-
### Utils (`packages/utils/*`)
85+
### Plugins (`packages/plugins/*`)i
86+
87+
| Package | Path | Role |
88+
| --- | --- | --- |
89+
| `@equinor/fusion-framework-plugin-context-navigation` | `packages/plugins/context-navigation` | Plugin for context-based navigation handling |
90+
91+
## Utils (`packages/utils/*`)
8692

8793
| Package | Path | Role |
8894
| --- | --- | --- |

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"lint:fusion": "pnpm build:lint && pnpm lint:fusion:packages && pnpm lint:fusion:cookbooks",
4141
"lint:fusion:packages": "fusion-lint lint packages",
4242
"lint:fusion:cookbooks": "fusion-lint lint cookbooks",
43+
"lint:fusion:plugins": "fusion-lint lint packages/plugins",
4344
"lint:staged": "biome lint --staged",
4445
"verify:agent-context": "node .github/scripts/verify-agent-context.mjs",
4546
"format": "biome format",

packages/dev-portal/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Use this package when you need a portal host for local app development. For the
1313
- **Portal shell**: A React application that renders the top bar, context selector, and an app mounting area.
1414
- **Application loader**: Dynamically initializes and mounts a Fusion app by its `appKey`, handling manifest resolution, script loading, and teardown.
1515
- **Framework modules**: The portal pre-configures telemetry, navigation, bookmarks, feature flags, analytics, AG Grid, and service integrations so loaded apps inherit a realistic environment.
16-
- **Context navigation**: When an app uses the context module, the portal synchronizes URL navigation with context changes automatically.
16+
- **Context navigation plugin**: The portal enables `enableContextNavigation` from `@equinor/fusion-framework-plugin-context-navigation` so context changes and app switches keep the URL in sync automatically.
1717

1818
## Installation
1919

@@ -94,12 +94,13 @@ const devServer = await createDevServer({
9494
The portal is composed of these internal parts:
9595

9696
- **`render`** — Entry point; creates a React root with theme, framework, and people-resolver providers.
97-
- **`configure`** — Configures all framework modules (telemetry, navigation, bookmarks, feature flags, analytics, AG Grid, services).
97+
- **`configure`** — Configures all framework modules (telemetry, navigation, context navigation, bookmarks, feature flags, analytics, AG Grid, services).
98+
- **Context configuration**`enableContext` wires the context module's path generator and path extractor to the shared context-navigation URL utilities.
9899
- **`Router`** — Sets up routes with `react-router` via the navigation module; routes `/apps/:appKey/*` to the app loader.
99100
- **`AppLoader`** — Resolves, initializes, and mounts a Fusion app by key; handles loading states and errors.
100101
- **`Header`** — Top bar with the Fusion logo, context selector, bookmark toggle, and person settings.
101102
- **`ContextSelector`** — Wired to the current app's context module for searching and selecting context items.
102-
- **`useAppContextNavigation`**Synchronizes URL pathname with context changes for apps that use the context module.
103+
- **`enableContextNavigation`**Registers the portal-level context navigation plugin that reconciles the active context with the browser URL.
103104

104105
## Constraints
105106

packages/dev-portal/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"@equinor/fusion-framework-module-app": "workspace:*",
3535
"@equinor/fusion-framework-module-bookmark": "workspace:*",
3636
"@equinor/fusion-framework-module-context": "workspace:*",
37+
"@equinor/fusion-framework-plugin-context-navigation": "workspace:*",
3738
"@equinor/fusion-framework-module-feature-flag": "workspace:*",
3839
"@equinor/fusion-framework-module-navigation": "workspace:*",
3940
"@equinor/fusion-framework-module-services": "workspace:*",
@@ -53,12 +54,14 @@
5354
"@equinor/fusion-wc-person": "^3.5.5",
5455
"@types/react": "^19.2.7",
5556
"@types/react-dom": "^19.2.3",
57+
"@types/semver": "^7.7.1",
5658
"@vitejs/plugin-react": "^6.0.1",
5759
"dotenv": "^17.3.1",
5860
"react": "^19.2.1",
5961
"react-dom": "^19.2.1",
6062
"rxjs": "^7.8.1",
6163
"styled-components": "^6.3.11",
64+
"semver": "^7.7.2",
6265
"tsx": "^4.19.3",
6366
"typescript": "^7.0.2",
6467
"vite": "^8.0.0"
@@ -72,6 +75,7 @@
7275
"@equinor/fusion-framework-module-app": "workspace:*",
7376
"@equinor/fusion-framework-module-bookmark": "workspace:*",
7477
"@equinor/fusion-framework-module-context": "workspace:*",
78+
"@equinor/fusion-framework-plugin-context-navigation": "workspace:*",
7579
"@equinor/fusion-framework-module-feature-flag": "workspace:*",
7680
"@equinor/fusion-framework-module-navigation": "workspace:*",
7781
"@equinor/fusion-framework-module-services": "workspace:*",

packages/dev-portal/src/Router.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { useBookmarkNavigate } from '@equinor/fusion-framework-react-module-bookmark/portal';
22

3-
import { Outlet, Router as FusionRouter, useParams } from '@equinor/fusion-framework-react-router';
3+
import { Router as FusionRouter, Outlet, useParams } from '@equinor/fusion-framework-react-router';
44
import AppLoader from './AppLoader';
55
import { Header } from './Header';
66

77
import { styled } from 'styled-components';
8-
import { useAppContextNavigation } from './useAppContextNavigation';
98

109
const Styled = {
1110
ContentContainer: styled.div`
@@ -76,11 +75,9 @@ const routes = [
7675
/**
7776
* Top-level router for the Fusion Dev Portal.
7877
*
79-
* Renders the application via `FusionRouter`. Observes context changes through
80-
* {@link useAppContextNavigation} to keep the URL in sync.
78+
* Uses `@equinor/fusion-framework-react-router` which automatically connects
79+
* to the framework's navigation module for history and basename.
8180
*/
8281
export const Router = () => {
83-
// observe the context changes and navigate when the context changes
84-
useAppContextNavigation();
8582
return <FusionRouter routes={routes} />;
8683
};

0 commit comments

Comments
 (0)