Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/angular/build/src/builders/application/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
loadPostcssConfiguration,
} from '../../utils/postcss-configuration';
import { getProjectRootPaths, normalizeDirectoryPath } from '../../utils/project-metadata';
import { urlJoin } from '../../utils/url';
import { addTrailingSlash, joinUrlParts } from '../../utils/url';
import {
Schema as ApplicationBuilderOptions,
ExperimentalPlatform,
Expand Down Expand Up @@ -681,7 +681,9 @@ export function getLocaleBaseHref(

const baseHrefSuffix = localeData.baseHref ?? localeData.subPath + '/';

return baseHrefSuffix !== '' ? urlJoin(baseHref, baseHrefSuffix) : undefined;
return baseHrefSuffix !== ''
? addTrailingSlash(joinUrlParts(baseHref, baseHrefSuffix))
: undefined;
}

/**
Expand Down
20 changes: 4 additions & 16 deletions packages/angular/build/src/utils/server-rendering/prerender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundle
import { BuildOutputAsset } from '../../tools/esbuild/bundler-execution-result';
import { assertIsError } from '../error';
import { toPosixPath } from '../path';
import { urlJoin } from '../url';
import { addLeadingSlash, addTrailingSlash, joinUrlParts, stripLeadingSlash } from '../url';
import { WorkerPool } from '../worker-pool';
import { IMPORT_EXEC_ARGV } from './esm-in-memory-loader/utils';
import { SERVER_APP_MANIFEST_FILENAME } from './manifest';
Expand Down Expand Up @@ -240,7 +240,7 @@ async function renderPages(
? addLeadingSlash(route.slice(baseHrefPathnameWithLeadingSlash.length))
: route;

const outPath = posix.join(removeLeadingSlash(routeWithoutBaseHref), 'index.html');
const outPath = stripLeadingSlash(posix.join(routeWithoutBaseHref, 'index.html'));

if (typeof redirectTo === 'string') {
output[outPath] = { content: generateRedirectStaticPage(redirectTo), appShellRoute: false };
Expand Down Expand Up @@ -298,7 +298,7 @@ async function getAllRoutes(
let appShellRoute: string | undefined;

if (appShellOptions) {
appShellRoute = urlJoin(baseHref, appShellOptions.route);
appShellRoute = joinUrlParts(baseHref, appShellOptions.route);

routes.push({
renderMode: RouteRenderMode.Prerender,
Expand All @@ -311,7 +311,7 @@ async function getAllRoutes(
for (const route of routesFromFile) {
routes.push({
renderMode: RouteRenderMode.Prerender,
route: urlJoin(baseHref, route.trim()),
route: joinUrlParts(baseHref, route.trim()),
});
}
}
Expand Down Expand Up @@ -369,15 +369,3 @@ async function getAllRoutes(
void renderWorker.destroy();
}
}

function addLeadingSlash(value: string): string {
return value[0] === '/' ? value : '/' + value;
}

function addTrailingSlash(url: string): string {
return url[url.length - 1] === '/' ? url : `${url}/`;
}

function removeLeadingSlash(value: string): string {
return value[0] === '/' ? value.slice(1) : value;
}
118 changes: 112 additions & 6 deletions packages/angular/build/src/utils/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,117 @@
* found in the LICENSE file at https://angular.dev/license
*/

export function urlJoin(...parts: string[]): string {
const [p, ...rest] = parts;
/**
* Removes the trailing slash from a URL if it exists.
*
* @param url - The URL string from which to remove the trailing slash.
* @returns The URL string without a trailing slash.
*
* @example
* ```js
* stripTrailingSlash('path/'); // 'path'
* stripTrailingSlash('/path'); // '/path'
* stripTrailingSlash('/'); // '/'
* stripTrailingSlash(''); // ''
* ```
*/
export function stripTrailingSlash(url: string): string {
// Check if the last character of the URL is a slash
return url.length > 1 && url[url.length - 1] === '/' ? url.slice(0, -1) : url;
}

/**
* Removes the leading slash from a URL if it exists.
*
* @param url - The URL string from which to remove the leading slash.
* @returns The URL string without a leading slash.
*
* @example
* ```js
* stripLeadingSlash('/path'); // 'path'
* stripLeadingSlash('/path/'); // 'path/'
* stripLeadingSlash('/'); // '/'
* stripLeadingSlash(''); // ''
* ```
*/
export function stripLeadingSlash(url: string): string {
// Check if the first character of the URL is a slash
return url.length > 1 && url[0] === '/' ? url.slice(1) : url;
}

/**
* Adds a leading slash to a URL if it does not already have one.
*
* @param url - The URL string to which the leading slash will be added.
* @returns The URL string with a leading slash.
*
* @example
* ```js
* addLeadingSlash('path'); // '/path'
* addLeadingSlash('/path'); // '/path'
* ```
*/
export function addLeadingSlash(url: string): string {
// Check if the URL already starts with a slash
return url[0] === '/' ? url : `/${url}`;
}

/**
* Adds a trailing slash to a URL if it does not already have one.
*
* @param url - The URL string to which the trailing slash will be added.
* @returns The URL string with a trailing slash.
*
* @example
* ```js
* addTrailingSlash('path'); // 'path/'
* addTrailingSlash('path/'); // 'path/'
* ```
*/
export function addTrailingSlash(url: string): string {
// Check if the URL already end with a slash
return url[url.length - 1] === '/' ? url : `${url}/`;
}

/**
* Joins URL parts into a single URL string.
*
* This function takes multiple URL segments, normalizes them by removing leading
* and trailing slashes where appropriate, and then joins them into a single URL.
*
* @param parts - The parts of the URL to join. Each part can be a string with or without slashes.
* @returns The joined URL string, with normalized slashes.
*
* @example
* ```js
* joinUrlParts('path/', '/to/resource'); // '/path/to/resource'
* joinUrlParts('/path/', 'to/resource'); // '/path/to/resource'
* joinUrlParts('http://localhost/path/', 'to/resource'); // 'http://localhost/path/to/resource'
* joinUrlParts('', ''); // '/'
* ```
*/
export function joinUrlParts(...parts: string[]): string {
const normalizeParts: string[] = [];
for (const part of parts) {
if (part === '') {
// Skip any empty parts
continue;
}

let normalizedPart = part;
if (part[0] === '/') {
normalizedPart = normalizedPart.slice(1);
}
if (part[part.length - 1] === '/') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we get to use https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/at here since it's been widely available since March 2022?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is copied from the SSR package. I will do a followup to change the .at(-1).

normalizedPart = normalizedPart.slice(0, -1);
}
if (normalizedPart !== '') {
normalizeParts.push(normalizedPart);
}
}

const protocolMatch = normalizeParts.length && /^https?:\/\//.test(normalizeParts[0]);
const joinedParts = normalizeParts.join('/');

// Remove trailing slash from first part
// Join all parts with `/`
// Dedupe double slashes from path names
return p.replace(/\/$/, '') + ('/' + rest.join('/')).replace(/\/\/+/g, '/');
return protocolMatch ? joinedParts : addLeadingSlash(joinedParts);
}