diff --git a/packages/platform-server/src/location.ts b/packages/platform-server/src/location.ts index bba5d6ae78d..5e7c00908c1 100644 --- a/packages/platform-server/src/location.ts +++ b/packages/platform-server/src/location.ts @@ -9,19 +9,45 @@ import {DOCUMENT, LocationChangeEvent, LocationChangeListener, PlatformLocation, ɵgetDOM as getDOM} from '@angular/common'; import {Inject, Injectable, Optional, ɵWritable as Writable} from '@angular/core'; import {Subject} from 'rxjs'; -import * as url from 'url'; import {INITIAL_CONFIG, PlatformConfig} from './tokens'; -function parseUrl(urlStr: string) { - const parsedUrl = url.parse(urlStr); +const RESOLVE_PROTOCOL = 'resolve:'; + +function parseUrl(urlStr: string): { + hostname: string, + protocol: string, + port: string, + pathname: string, + search: string, + hash: string, +} { + let {hostname, protocol, port, pathname, search, hash} = new URL(urlStr, RESOLVE_PROTOCOL + '//'); + + /** + * TODO(alanagius): Remove the below in version 18. + * The following are done to maintain the same behaviour as `url.parse`. + * The main differences are; + * - `pathname` is always suffixed with a `/`. + * - `port` is empty when `http:` protocol and port in url is `80` + * - `port` is empty when `https:` protocol and port in url is `443` + */ + if (protocol !== RESOLVE_PROTOCOL && port === '' && /\:(80|443)/.test(urlStr)) { + port = protocol === 'http:' ? '80' : '443'; + } + + if (protocol === RESOLVE_PROTOCOL && urlStr.charAt(0) !== '/') { + pathname = pathname.slice(1); // Remove leading slash. + } + // END TODO + return { - hostname: parsedUrl.hostname || '', - protocol: parsedUrl.protocol || '', - port: parsedUrl.port || '', - pathname: parsedUrl.pathname || '', - search: parsedUrl.search || '', - hash: parsedUrl.hash || '', + hostname, + protocol: protocol === RESOLVE_PROTOCOL ? '' : protocol, + port, + pathname, + search, + hash, }; } diff --git a/packages/platform-server/test/integration_spec.ts b/packages/platform-server/test/integration_spec.ts index 06087cdc4bd..2e0e75a4c4f 100644 --- a/packages/platform-server/test/integration_spec.ts +++ b/packages/platform-server/test/integration_spec.ts @@ -707,6 +707,58 @@ describe('platform-server integration', () => { location.pushState(null, 'Test', '/foo#bar'); }); }); + + describe('Legacy (Remove once url normalization is removed)', () => { + it('parses and returns pathname without a slash', async () => { + const platform = platformServer([{ + provide: INITIAL_CONFIG, + useValue: {document: '', url: 'deep/path?query#hash'} + }]); + + const appRef = await platform.bootstrapModule(ExampleModule); + + const location = appRef.injector.get(PlatformLocation); + expect(location.hostname).toBe(''); + expect(location.protocol).toBe(''); + expect(location.port).toBe(''); + expect(location.pathname).toBe('deep/path'); + expect(location.search).toBe('?query'); + expect(location.hash).toBe('#hash'); + }); + it('port is set to 443 when using https protocol', async () => { + const platform = platformServer([{ + provide: INITIAL_CONFIG, + useValue: {document: '', url: 'https://test.com:443/deep/path?query#hash'} + }]); + + const appRef = await platform.bootstrapModule(ExampleModule); + + const location = appRef.injector.get(PlatformLocation); + expect(location.hostname).toBe('test.com'); + expect(location.protocol).toBe('https:'); + expect(location.port).toBe('443'); + expect(location.pathname).toBe('/deep/path'); + expect(location.search).toBe('?query'); + expect(location.hash).toBe('#hash'); + }); + + it('port is set to 80 when using http protocol', async () => { + const platform = platformServer([{ + provide: INITIAL_CONFIG, + useValue: {document: '', url: 'http://test.com:80/deep/path?query#hash'} + }]); + + const appRef = await platform.bootstrapModule(ExampleModule); + + const location = appRef.injector.get(PlatformLocation); + expect(location.hostname).toBe('test.com'); + expect(location.protocol).toBe('http:'); + expect(location.port).toBe('80'); + expect(location.pathname).toBe('/deep/path'); + expect(location.search).toBe('?query'); + expect(location.hash).toBe('#hash'); + }); + }); }); describe('render', () => {