diff --git a/aio/content/guide/deprecations.md b/aio/content/guide/deprecations.md index 5fe705c694d..6cd7d2f264b 100644 --- a/aio/content/guide/deprecations.md +++ b/aio/content/guide/deprecations.md @@ -321,6 +321,8 @@ The `relativeLinkResolution` option is deprecated and being removed. In version 11, the default behavior was changed to the correct one. After `relativeLinkResolution` is removed, the correct behavior is always used without an option to use the broken behavior. +A dev mode warning was added in v14 to warn if a created `UrlTree` relies on the `relativeLinkResolution: 'legacy'` option. + ### loadChildren string syntax diff --git a/packages/router/src/create_url_tree.ts b/packages/router/src/create_url_tree.ts index a074bce1e11..f9c7a7df621 100644 --- a/packages/router/src/create_url_tree.ts +++ b/packages/router/src/create_url_tree.ts @@ -24,13 +24,30 @@ export function createUrlTree( return tree(urlTree.root, urlTree.root, new UrlSegmentGroup([], {}), queryParams, fragment); } - const startingPosition = findStartingPosition(nav, urlTree, route); + function createTreeUsingPathIndex(lastPathIndex: number) { + const startingPosition = + findStartingPosition(nav, urlTree, route.snapshot._urlSegment, lastPathIndex); - const segmentGroup = startingPosition.processChildren ? - updateSegmentGroupChildren( - startingPosition.segmentGroup, startingPosition.index, nav.commands) : - updateSegmentGroup(startingPosition.segmentGroup, startingPosition.index, nav.commands); - return tree(urlTree.root, startingPosition.segmentGroup, segmentGroup, queryParams, fragment); + const segmentGroup = startingPosition.processChildren ? + updateSegmentGroupChildren( + startingPosition.segmentGroup, startingPosition.index, nav.commands) : + updateSegmentGroup(startingPosition.segmentGroup, startingPosition.index, nav.commands); + return tree(urlTree.root, startingPosition.segmentGroup, segmentGroup, queryParams, fragment); + } + const result = createTreeUsingPathIndex(route.snapshot._lastPathIndex); + + // Check if application is relying on `relativeLinkResolution: 'legacy'` + if (typeof ngDevMode === 'undefined' || !!ngDevMode) { + const correctedResult = createTreeUsingPathIndex(route.snapshot._correctedLastPathIndex); + if (correctedResult.toString() !== result.toString()) { + console.warn( + `relativeLinkResolution: 'legacy' is deprecated and will be removed in a future version of Angular. The link to ${ + result.toString()} will change to ${ + correctedResult.toString()} if the code is not updated before then.`); + } + } + + return result; } function isMatrixParams(command: any): boolean { @@ -151,13 +168,14 @@ class Position { } } -function findStartingPosition(nav: Navigation, tree: UrlTree, route: ActivatedRoute): Position { +function findStartingPosition( + nav: Navigation, tree: UrlTree, segmentGroup: UrlSegmentGroup, + lastPathIndex: number): Position { if (nav.isAbsolute) { return new Position(tree.root, true, 0); } - if (route.snapshot._lastPathIndex === -1) { - const segmentGroup = route.snapshot._urlSegment; + if (lastPathIndex === -1) { // Pathless ActivatedRoute has _lastPathIndex === -1 but should not process children // see issue #26224, #13011, #35687 // However, if the ActivatedRoute is the root we should process children like above. @@ -166,9 +184,8 @@ function findStartingPosition(nav: Navigation, tree: UrlTree, route: ActivatedRo } const modifier = isMatrixParams(nav.commands[0]) ? 0 : 1; - const index = route.snapshot._lastPathIndex + modifier; - return createPositionApplyingDoubleDots( - route.snapshot._urlSegment, index, nav.numberOfDoubleDots); + const index = lastPathIndex + modifier; + return createPositionApplyingDoubleDots(segmentGroup, index, nav.numberOfDoubleDots); } function createPositionApplyingDoubleDots( diff --git a/packages/router/src/recognize.ts b/packages/router/src/recognize.ts index 0bf5194c8ae..5975ffa4b14 100644 --- a/packages/router/src/recognize.ts +++ b/packages/router/src/recognize.ts @@ -18,6 +18,8 @@ import {getOutlet, sortByMatchingOutlets} from './utils/config'; import {isImmediateMatch, match, noLeftoversInUrl, split} from './utils/config_matching'; import {TreeNode} from './utils/tree'; +const NG_DEV_MODE = typeof ngDevMode === 'undefined' || !!ngDevMode; + class NoMatch {} function newObservableError(e: unknown): Observable { @@ -159,11 +161,16 @@ export class Recognizer { if (route.path === '**') { const params = segments.length > 0 ? last(segments)!.parameters : {}; + const pathIndexShift = getPathIndexShift(rawSegment) + segments.length; snapshot = new ActivatedRouteSnapshot( segments, params, Object.freeze({...this.urlTree.queryParams}), this.urlTree.fragment, getData(route), getOutlet(route), route.component!, route, - getSourceSegmentGroup(rawSegment), getPathIndexShift(rawSegment) + segments.length, - getResolve(route)); + getSourceSegmentGroup(rawSegment), pathIndexShift, getResolve(route), + // NG_DEV_MODE is used to prevent the getCorrectedPathIndexShift function from affecting + // production bundle size. This value is intended only to surface a warning to users + // depending on `relativeLinkResolution: 'legacy'` in dev mode. + (NG_DEV_MODE ? getCorrectedPathIndexShift(rawSegment) + segments.length : + pathIndexShift)); } else { const result = match(rawSegment, route, segments); if (!result.matched) { @@ -171,12 +178,14 @@ export class Recognizer { } consumedSegments = result.consumedSegments; remainingSegments = result.remainingSegments; + const pathIndexShift = getPathIndexShift(rawSegment) + consumedSegments.length; snapshot = new ActivatedRouteSnapshot( consumedSegments, result.parameters, Object.freeze({...this.urlTree.queryParams}), this.urlTree.fragment, getData(route), getOutlet(route), route.component!, route, - getSourceSegmentGroup(rawSegment), - getPathIndexShift(rawSegment) + consumedSegments.length, getResolve(route)); + getSourceSegmentGroup(rawSegment), pathIndexShift, getResolve(route), + (NG_DEV_MODE ? getCorrectedPathIndexShift(rawSegment) + consumedSegments.length : + pathIndexShift)); } const childConfig: Route[] = getChildConfig(route); @@ -303,10 +312,20 @@ function getSourceSegmentGroup(segmentGroup: UrlSegmentGroup): UrlSegmentGroup { function getPathIndexShift(segmentGroup: UrlSegmentGroup): number { let s = segmentGroup; - let res = (s._segmentIndexShift ? s._segmentIndexShift : 0); + let res = s._segmentIndexShift ?? 0; while (s._sourceSegment) { s = s._sourceSegment; - res += (s._segmentIndexShift ? s._segmentIndexShift : 0); + res += s._segmentIndexShift ?? 0; + } + return res - 1; +} + +function getCorrectedPathIndexShift(segmentGroup: UrlSegmentGroup): number { + let s = segmentGroup; + let res = s._segmentIndexShiftCorrected ?? s._segmentIndexShift ?? 0; + while (s._sourceSegment) { + s = s._sourceSegment; + res += s._segmentIndexShiftCorrected ?? s._segmentIndexShift ?? 0; } return res - 1; } diff --git a/packages/router/src/router_state.ts b/packages/router/src/router_state.ts index 67151111556..a5848868785 100644 --- a/packages/router/src/router_state.ts +++ b/packages/router/src/router_state.ts @@ -284,6 +284,13 @@ export class ActivatedRouteSnapshot { _urlSegment: UrlSegmentGroup; /** @internal */ _lastPathIndex: number; + /** + * @internal + * + * Used only in dev mode to detect if application relies on `relativeLinkResolution: 'legacy'` + * Should be removed in v16. + */ + _correctedLastPathIndex: number; /** @internal */ _resolve: ResolveData; /** @internal */ @@ -333,10 +340,11 @@ export class ActivatedRouteSnapshot { public outlet: string, /** The component of the route */ public component: Type|string|null, routeConfig: Route|null, urlSegment: UrlSegmentGroup, - lastPathIndex: number, resolve: ResolveData) { + lastPathIndex: number, resolve: ResolveData, correctedLastPathIndex?: number) { this.routeConfig = routeConfig; this._urlSegment = urlSegment; this._lastPathIndex = lastPathIndex; + this._correctedLastPathIndex = correctedLastPathIndex ?? lastPathIndex; this._resolve = resolve; } diff --git a/packages/router/src/url_tree.ts b/packages/router/src/url_tree.ts index 09774ab443a..14f55d1b795 100644 --- a/packages/router/src/url_tree.ts +++ b/packages/router/src/url_tree.ts @@ -227,6 +227,13 @@ export class UrlSegmentGroup { _sourceSegment?: UrlSegmentGroup; /** @internal */ _segmentIndexShift?: number; + /** + * @internal + * + * Used only in dev mode to detect if application relies on `relativeLinkResolution: 'legacy'` + * Should be removed in when `relativeLinkResolution` is removed. + */ + _segmentIndexShiftCorrected?: number; /** The parent node in the url tree */ parent: UrlSegmentGroup|null = null; diff --git a/packages/router/src/utils/config_matching.ts b/packages/router/src/utils/config_matching.ts index 9967e192a25..482c296135b 100644 --- a/packages/router/src/utils/config_matching.ts +++ b/packages/router/src/utils/config_matching.ts @@ -111,6 +111,9 @@ function addEmptyPathsToChildrenIfNeeded( s._sourceSegment = segmentGroup; if (relativeLinkResolution === 'legacy') { s._segmentIndexShift = segmentGroup.segments.length; + if (typeof ngDevMode === 'undefined' || !!ngDevMode) { + s._segmentIndexShiftCorrected = consumedSegments.length; + } } else { s._segmentIndexShift = consumedSegments.length; } diff --git a/packages/router/test/integration.spec.ts b/packages/router/test/integration.spec.ts index f22c24866b4..5730911f836 100644 --- a/packages/router/test/integration.spec.ts +++ b/packages/router/test/integration.spec.ts @@ -87,6 +87,7 @@ describe('Integration', () => { it('should not ignore empty paths in legacy mode', fakeAsync(inject([Router], (router: Router) => { + const warnSpy = spyOn(console, 'warn'); router.relativeLinkResolution = 'legacy'; const fixture = createRoot(router, RootCmp); @@ -96,6 +97,10 @@ describe('Integration', () => { const link = fixture.nativeElement.querySelector('a'); expect(link.getAttribute('href')).toEqual('/foo/bar/simple'); + expect(warnSpy.calls.first().args[0]) + .toContain('/foo/bar/simple will change to /foo/simple'); + expect(warnSpy.calls.first().args[0]) + .toContain('relativeLinkResolution: \'legacy\' is deprecated'); }))); it('should ignore empty paths in corrected mode', @@ -5888,6 +5893,7 @@ describe('Integration', () => { it('should not ignore empty path when in legacy mode', fakeAsync(inject([Router], (router: Router) => { + const warnSpy = spyOn(console, 'warn'); router.relativeLinkResolution = 'legacy'; const fixture = createRoot(router, RootCmp); @@ -5899,6 +5905,10 @@ describe('Integration', () => { const link = fixture.nativeElement.querySelector('a'); expect(link.getAttribute('href')).toEqual('/lazy/foo/bar/simple'); + expect(warnSpy.calls.first().args[0]) + .toContain('/lazy/foo/bar/simple will change to /lazy/foo/simple'); + expect(warnSpy.calls.first().args[0]) + .toContain('relativeLinkResolution: \'legacy\' is deprecated'); }))); it('should ignore empty path when in corrected mode',