feat(context-navigation): add plugin package, dev portal integration, and plugin docs - #4751
Conversation
🦋 Changeset detectedLatest commit: 477481f The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Pull request overview
This pull request introduces @equinor/fusion-framework-plugin-context-navigation as a first-class plugin package, wires it into the dev portal, and restructures VuePress docs to present context navigation under a new /plugins/ surface (instead of module-oriented paths). It also extends the context and navigation modules to support routing strategy semantics and basename/path normalization required by the new plugin model.
Changes:
- Add new context-navigation plugin package (runtime, configurator, adapters, sources, URL utilities, events) plus initial changeset.
- Integrate the plugin into
@equinor/fusion-framework-dev-portaland remove the prior ad-hocuseAppContextNavigationhook. - Add/adjust docs and migrations (VuePress plugin section + context routing strategy migration), plus context/navigation module behavior updates and changesets.
Reviewed changes
Copilot reviewed 59 out of 60 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| vue-press/src/plugins/README.md | New VuePress landing page for plugins and plugin intent. |
| vue-press/src/plugins/context-navigation/README.md | VuePress entry for context-navigation plugin, includes package README. |
| vue-press/src/modules/context/migration-routing-strategy.md | VuePress wrapper for context routing strategy migration doc. |
| vue-press/src/.vuepress/theme.ts | Adds “Plugins” to top navbar. |
| vue-press/src/.vuepress/sidebar.ts | Adds /plugins/ sidebar section and context-navigation entry. |
| packages/plugins/context-navigation/vitest.config.ts | Adds Vitest project config for the new plugin package. |
| packages/plugins/context-navigation/tsconfig.json | Adds TS project config + references for plugin package. |
| packages/plugins/context-navigation/src/utils/url/url-utils.ts | URL strategy dispatch + provider-level URL helpers and re-exports. |
| packages/plugins/context-navigation/src/utils/url/strip-context-query-param.ts | Utility to remove $contextId query param when using path/custom encodings. |
| packages/plugins/context-navigation/src/utils/url/query-utils.ts | Query param read/write helpers for $contextId. |
| packages/plugins/context-navigation/src/utils/url/path-utils.ts | Path parsing/writing utilities for /apps/:appKey/:contextId/.... |
| packages/plugins/context-navigation/src/utils/url/index.ts | Public URL utilities barrel export. |
| packages/plugins/context-navigation/src/utils/url/app-route.ts | URLPattern-based parsing/building of /apps/:appKey/:contextId?/…. |
| packages/plugins/context-navigation/src/utils/resolve-context-from-url.ts | Default initial context resolver using adapter decode. |
| packages/plugins/context-navigation/src/utils/legacy-app-navigation-fix.ts | Optional fix for legacy navigation module (< v7) apps. |
| packages/plugins/context-navigation/src/utils/has-custom-context-generators.ts | Detects apps with custom context URL hooks (custom adapter selection). |
| packages/plugins/context-navigation/src/types.ts | Plugin public types (adapters, config, events, sources). |
| packages/plugins/context-navigation/src/sources/index.ts | Barrel export for built-in source factories. |
| packages/plugins/context-navigation/src/sources/context-first-source.ts | Context-first reconciler source strategy. |
| packages/plugins/context-navigation/src/sources/app-first-source.ts | App-first reconciler source strategy (default). |
| packages/plugins/context-navigation/src/plugin.ts | Core runtime plugin: reconciler + URL guard + event dispatch. |
| packages/plugins/context-navigation/src/index.ts | Package public API surface exports + side-effect event augmentation. |
| packages/plugins/context-navigation/src/events.ts | Event map augmentation for context-navigation events. |
| packages/plugins/context-navigation/src/enable.ts | enableContextNavigation helper registering the plugin via configurator. |
| packages/plugins/context-navigation/src/enable.test.ts | Unit tests covering enableContextNavigation registration/teardown wiring. |
| packages/plugins/context-navigation/src/configurator.ts | Fluent-like builder for plugin config defaults + adapter registration. |
| packages/plugins/context-navigation/src/adapters/query-adapter.ts | Query adapter implementation (?$contextId=). |
| packages/plugins/context-navigation/src/adapters/path-adapter.ts | Path adapter implementation (/apps/:appKey/:contextId/...). |
| packages/plugins/context-navigation/src/adapters/index.ts | Barrel export for built-in adapters. |
| packages/plugins/context-navigation/src/adapters/custom-adapter.ts | Custom adapter factory bridging app-owned URL hooks. |
| packages/plugins/context-navigation/README.md | Consumer-facing documentation for the new plugin package. |
| packages/plugins/context-navigation/package.json | New published package manifest + exports map. |
| packages/modules/navigation/src/NavigationProvider.ts | Basename boundary check + trailing-slash behavior adjustments. |
| packages/modules/context/src/utils/resolve-initial-context.ts | Removes URL-based initial context resolution; parent-only resolution. |
| packages/modules/context/src/utils/resolve-context-from-path.ts | Improves extractor docs; strips query/hash; deprecates path resolution helpers. |
| packages/modules/context/src/types.ts | Adds ContextRoutingStrategy type. |
| packages/modules/context/src/ContextProvider.ts | Exposes routingStrategy + adds version typing; sets provider routingStrategy. |
| packages/modules/context/src/ContextConfigBuilder.ts | Adds setRoutingStrategy builder method. |
| packages/modules/context/src/configurator.ts | Adds routingStrategy and extends generator signature; defaults strategy + warns. |
| packages/modules/context/README.md | Documents routing strategy builder usage. |
| packages/modules/context/package.json | Adds semver/@types/semver deps/peers. |
| packages/modules/context/docs/migration-routing-strategy.md | New migration guide for routing strategy adoption. |
| packages/dev-portal/src/useAppContextNavigation.ts | Removes old ad-hoc context→URL sync hook. |
| packages/dev-portal/src/Router.tsx | Removes usage of deleted hook; updates router documentation. |
| packages/dev-portal/src/config.ts | Enables context module + new context-navigation plugin; exposes modules on window. |
| packages/dev-portal/src/config-context.ts | Adds dev-portal context URL hook wiring via plugin URL utils. |
| packages/dev-portal/README.md | Updates dev-portal docs to reference plugin-based context navigation. |
| packages/dev-portal/package.json | Adds plugin dependency + semver deps to satisfy updated module typings. |
| cookbooks/portal-analytics/src/framworkConfig.ts | Sets explicit routing strategy for cookbook portal context module. |
| cookbooks/app-react-context/src/config.ts | Sets explicit routing strategy for cookbook app. |
| cookbooks/app-react-context-custom-error/src/config.ts | Sets explicit routing strategy for cookbook app. |
| cookbooks/app-react-bookmark/src/config.ts | Sets explicit routing strategy for cookbook app. |
| cookbooks/app-react-bookmark-advanced/src/config.ts | Sets explicit routing strategy for cookbook app. |
| .changeset/fusion-framework-plugin-context-navigation_initial.md | Changeset for initial plugin release. |
| .changeset/fusion-framework-module-navigation_trailing-slash-fix.md | Changeset describing navigation basename/trailing-slash fixes. |
| .changeset/fusion-framework-module-context_routing-strategy.md | Changeset describing context routing strategy changes + deprecations. |
| .changeset/fusion-framework-dev-portal_context-navigation-handler.md | Changeset describing dev-portal integration update. |
8e82081 to
3c6b04d
Compare
8b94fef to
b34a593
Compare
|
Two-part ask: expose them from the navigation module, then delete the duplicates and use the real ones here. Step 1 — add a In "./utils": {
"import": "./dist/esm/lib/utils/index.js",
"types": "./dist/types/lib/utils/index.d.ts"
}In Step 2 — replace - import { parseAppRoute, buildAppRoute } from './app-route';
+ import { resolvePath } from '@equinor/fusion-framework-module-navigation/utils';
+ import { parseAppRoute, buildAppRoute } from './app-route';
- export const splitRelativePath = (path: string) => { ... };
export const readContextIdFromAppPath = (path: string) => {
- const { pathname } = splitRelativePath(path);
+ const { pathname } = resolvePath(path);
return parseAppRoute(pathname)?.contextId;
};Step 3 — replace the manual reassembly in - import { splitRelativePath } from './path-utils';
+ import { resolvePath, pathToString } from '@equinor/fusion-framework-module-navigation/utils';
export const writeContextIdToQueryParam = (path: string, contextId?: string): string => {
- const { pathname, search, hash } = splitRelativePath(path);
+ const { pathname, search, hash } = resolvePath(path);
...
- return `${pathname}${nextSearch ? `?${nextSearch}` : ''}${hash ? `#${hash}` : ''}`;
+ return pathToString({ pathname, search: nextSearch, hash });
};Step 4 — drop |
|
1. The guard app.current$.pipe(
switchMap((currentApp) => {
if (!currentApp) return EMPTY;
return currentApp.instance$.pipe(
switchMap((appModules) => {
if (!appModules) return EMPTY;
return navigation.state$.pipe(
switchMap(() => of({ appModules, appKey: currentApp.appKey })),
);
}),
);
}),
);Each level is a separate concern (app switch, module resolution, nav event). Extract this into a named operator or factory function so the guard's // operators/active-app-navigation-events.ts
export function activeAppNavigationEvents$(
app: AppModuleProvider,
navigation: INavigationProvider,
): Observable<{ appKey: string; appModules: AppModulesInstance<[ContextModule]> }> {
return app.current$.pipe(
switchMap((currentApp) => (!currentApp ? EMPTY : currentApp.instance$.pipe(
switchMap((appModules) => (!appModules ? EMPTY : navigation.state$.pipe(
map(() => ({ appKey: currentApp.appKey, appModules: appModules as AppModulesInstance<[ContextModule]> })),
)),
))),
);
}Then the wire-up becomes readable: subscriptions.add(
activeAppNavigationEvents$(app, navigation).subscribe(handleGuardTick),
);2. The guard's URL scope check → own-nav token → adapter resolution → URL decode → push-mode branch → replace-mode fallback. Every branch is an implicit function isInAppScope(currentURL: URL, appKey: string): boolean { ... }
function consumeOwnNavToken(currentURL: URL): boolean { ... } // returns true if this was our own nav
function handlePushModeGuard(...): void { ... }
function handleReplaceModeGuard(...): void { ... }Then the callback reads like a decision tree, not archaeology. 3.
Better: make it a 4. The 5. Nothing in this file is independently testable. Every function closes over |
45b8216 to
1ff2b44
Compare
1ff2b44 to
c28b79a
Compare
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
5b5725b to
9624158
Compare
9624158 to
516a76c
Compare
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.
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
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)
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.
…anges 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
…ft correction to prevent back-navigation traps
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
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
…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
… performance and avoid ReDoS vulnerabilities
…nts and using array for path construction
Why is this change needed?
This branch introduces the context-navigation plugin as a first-class plugin package, wires it into the dev portal, and provides documentation and migration guidance. The plugin decouples URL-to-context reconciliation logic from the context module, making it an opt-in portal-level concern rather than baking it into every app's context configuration.
What is the current behavior?
On the
mainbranch:useAppContextNavigationhook for URL synchronizationWhat is the new behavior?
This PR adds:
@equinor/fusion-framework-plugin-context-navigation— an event-driven plugin that reconciles context with URLs using pluggable adapters (path, query, custom)useAppContextNavigationwith the pluginWhat is the intended behavior or invariant?
Context-to-URL synchronization should be:
build.options.contextRoutingDoes this PR introduce a breaking change?
No breaking changes compared to
main:Impact assessment:
mainbranch)@equinor/fusion-framework-plugin-context-navigation:0.0.0→0.1.0(initial minor release)@equinor/fusion-framework-module-context: patch (URL resolution moved to plugin)@equinor/fusion-framework-module-navigation: patch (trailing-slash and basename fixes)@equinor/fusion-framework-dev-portal: patch (internal plugin integration)@equinor/fusion-framework-plugin-context-navigationand enable itbuild.options.contextRoutingin manifests to control URL encodingsetContextPathExtractor/setContextPathGeneratorhooks (unchanged from current behavior)Review guidance:
useAppContextNavigation/modules/contextto/plugins/context-navigationAdditional context
Validation completed during this branch work:
pnpm --filter @equinor/fusion-framework-dev-portal exec tsc --noEmit -p tsconfig.json✓Validation not completed:
pnpm test && pnpm build && pnpm -w check(CI will validate)Related issues
None.
Checklist
Closes: https://github.com/equinor/fusion-core-tasks/issues/567