@angular/ssr 18.2.1 → 19.0.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,157 @@
1
+ /**
2
+ * @license
3
+ * Copyright Google LLC All Rights Reserved.
4
+ *
5
+ * Use of this source code is governed by an MIT-style license that can be
6
+ * found in the LICENSE file at https://angular.dev/license
7
+ */
8
+ import { APP_BASE_HREF, PlatformLocation } from '@angular/common';
9
+ import { ApplicationRef, Compiler, createPlatformFactory, platformCore, ɵwhenStable as whenStable, ɵConsole, ɵresetCompiledComponents, } from '@angular/core';
10
+ import { INITIAL_CONFIG, ɵINTERNAL_SERVER_PLATFORM_PROVIDERS as INTERNAL_SERVER_PLATFORM_PROVIDERS, } from '@angular/platform-server';
11
+ import { Router, ɵloadChildren as loadChildrenHelper } from '@angular/router';
12
+ import { Console } from '../console';
13
+ import { isNgModule } from '../utils/ng';
14
+ import { joinUrlParts } from '../utils/url';
15
+ /**
16
+ * Recursively traverses the Angular router configuration to retrieve routes.
17
+ *
18
+ * Iterates through the router configuration, yielding each route along with its potential
19
+ * redirection or error status. Handles nested routes and lazy-loaded child routes.
20
+ *
21
+ * @param options - An object containing the parameters for traversing routes.
22
+ * @returns An async iterator yielding `RouteResult` objects.
23
+ */
24
+ async function* traverseRoutesConfig(options) {
25
+ const { routes, compiler, parentInjector, parentRoute } = options;
26
+ for (const route of routes) {
27
+ const { path = '', redirectTo, loadChildren, children } = route;
28
+ const currentRoutePath = joinUrlParts(parentRoute, path);
29
+ yield {
30
+ route: currentRoutePath,
31
+ redirectTo: typeof redirectTo === 'string'
32
+ ? resolveRedirectTo(currentRoutePath, redirectTo)
33
+ : undefined,
34
+ };
35
+ if (children?.length) {
36
+ // Recursively process child routes.
37
+ yield* traverseRoutesConfig({
38
+ routes: children,
39
+ compiler,
40
+ parentInjector,
41
+ parentRoute: currentRoutePath,
42
+ });
43
+ }
44
+ if (loadChildren) {
45
+ // Load and process lazy-loaded child routes.
46
+ const loadedChildRoutes = await loadChildrenHelper(route, compiler, parentInjector).toPromise();
47
+ if (loadedChildRoutes) {
48
+ const { routes: childRoutes, injector = parentInjector } = loadedChildRoutes;
49
+ yield* traverseRoutesConfig({
50
+ routes: childRoutes,
51
+ compiler,
52
+ parentInjector: injector,
53
+ parentRoute: currentRoutePath,
54
+ });
55
+ }
56
+ }
57
+ }
58
+ }
59
+ /**
60
+ * Resolves the `redirectTo` property for a given route.
61
+ *
62
+ * This function processes the `redirectTo` property to ensure that it correctly
63
+ * resolves relative to the current route path. If `redirectTo` is an absolute path,
64
+ * it is returned as is. If it is a relative path, it is resolved based on the current route path.
65
+ *
66
+ * @param routePath - The current route path.
67
+ * @param redirectTo - The target path for redirection.
68
+ * @returns The resolved redirect path as a string.
69
+ */
70
+ function resolveRedirectTo(routePath, redirectTo) {
71
+ if (redirectTo[0] === '/') {
72
+ // If the redirectTo path is absolute, return it as is.
73
+ return redirectTo;
74
+ }
75
+ // Resolve relative redirectTo based on the current route path.
76
+ const segments = routePath.split('/');
77
+ segments.pop(); // Remove the last segment to make it relative.
78
+ return joinUrlParts(...segments, redirectTo);
79
+ }
80
+ /**
81
+ * Retrieves routes from the given Angular application.
82
+ *
83
+ * This function initializes an Angular platform, bootstraps the application or module,
84
+ * and retrieves routes from the Angular router configuration. It handles both module-based
85
+ * and function-based bootstrapping. It yields the resulting routes as `RouteResult` objects.
86
+ *
87
+ * @param bootstrap - A function that returns a promise resolving to an `ApplicationRef` or an Angular module to bootstrap.
88
+ * @param document - The initial HTML document used for server-side rendering.
89
+ * This document is necessary to render the application on the server.
90
+ * @param url - The URL for server-side rendering. The URL is used to configure `ServerPlatformLocation`. This configuration is crucial
91
+ * for ensuring that API requests for relative paths succeed, which is essential for accurate route extraction.
92
+ * See:
93
+ * - https://github.com/angular/angular/blob/d608b857c689d17a7ffa33bbb510301014d24a17/packages/platform-server/src/location.ts#L51
94
+ * - https://github.com/angular/angular/blob/6882cc7d9eed26d3caeedca027452367ba25f2b9/packages/platform-server/src/http.ts#L44
95
+ * @returns A promise that resolves to an object of type `AngularRouterConfigResult`.
96
+ */
97
+ export async function getRoutesFromAngularRouterConfig(bootstrap, document, url) {
98
+ if (typeof ngDevMode === 'undefined' || ngDevMode) {
99
+ // Need to clean up GENERATED_COMP_IDS map in `@angular/core`.
100
+ // Otherwise an incorrect component ID generation collision detected warning will be displayed in development.
101
+ // See: https://github.com/angular/angular-cli/issues/25924
102
+ ɵresetCompiledComponents();
103
+ }
104
+ const { protocol, host } = url;
105
+ // Create and initialize the Angular platform for server-side rendering.
106
+ const platformRef = createPlatformFactory(platformCore, 'server', [
107
+ {
108
+ provide: INITIAL_CONFIG,
109
+ useValue: { document, url: `${protocol}//${host}/` },
110
+ },
111
+ {
112
+ provide: ɵConsole,
113
+ useFactory: () => new Console(),
114
+ },
115
+ ...INTERNAL_SERVER_PLATFORM_PROVIDERS,
116
+ ])();
117
+ try {
118
+ let applicationRef;
119
+ if (isNgModule(bootstrap)) {
120
+ const moduleRef = await platformRef.bootstrapModule(bootstrap);
121
+ applicationRef = moduleRef.injector.get(ApplicationRef);
122
+ }
123
+ else {
124
+ applicationRef = await bootstrap();
125
+ }
126
+ // Wait until the application is stable.
127
+ await whenStable(applicationRef);
128
+ const injector = applicationRef.injector;
129
+ const router = injector.get(Router);
130
+ const routesResults = [];
131
+ if (router.config.length) {
132
+ const compiler = injector.get(Compiler);
133
+ // Retrieve all routes from the Angular router configuration.
134
+ const traverseRoutes = traverseRoutesConfig({
135
+ routes: router.config,
136
+ compiler,
137
+ parentInjector: injector,
138
+ parentRoute: '',
139
+ });
140
+ for await (const result of traverseRoutes) {
141
+ routesResults.push(result);
142
+ }
143
+ }
144
+ else {
145
+ routesResults.push({ route: '' });
146
+ }
147
+ const baseHref = injector.get(APP_BASE_HREF, null, { optional: true }) ??
148
+ injector.get(PlatformLocation).getBaseHrefFromDOM();
149
+ return {
150
+ baseHref,
151
+ routes: routesResults,
152
+ };
153
+ }
154
+ finally {
155
+ platformRef.destroy();
156
+ }
157
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * @license
3
+ * Copyright Google LLC All Rights Reserved.
4
+ *
5
+ * Use of this source code is governed by an MIT-style license that can be
6
+ * found in the LICENSE file at https://angular.dev/license
7
+ */
8
+ import { stripTrailingSlash } from '../utils/url';
9
+ /**
10
+ * A route tree implementation that supports efficient route matching, including support for wildcard routes.
11
+ * This structure is useful for organizing and retrieving routes in a hierarchical manner,
12
+ * enabling complex routing scenarios with nested paths.
13
+ */
14
+ export class RouteTree {
15
+ /**
16
+ * The root node of the route tree.
17
+ * All routes are stored and accessed relative to this root node.
18
+ */
19
+ root = this.createEmptyRouteTreeNode('');
20
+ /**
21
+ * A counter that tracks the order of route insertion.
22
+ * This ensures that routes are matched in the order they were defined,
23
+ * with earlier routes taking precedence.
24
+ */
25
+ insertionIndexCounter = 0;
26
+ /**
27
+ * Inserts a new route into the route tree.
28
+ * The route is broken down into segments, and each segment is added to the tree.
29
+ * Parameterized segments (e.g., :id) are normalized to wildcards (*) for matching purposes.
30
+ *
31
+ * @param route - The route path to insert into the tree.
32
+ * @param metadata - Metadata associated with the route, excluding the route path itself.
33
+ */
34
+ insert(route, metadata) {
35
+ let node = this.root;
36
+ const normalizedRoute = stripTrailingSlash(route);
37
+ const segments = normalizedRoute.split('/');
38
+ for (const segment of segments) {
39
+ // Replace parameterized segments (e.g., :id) with a wildcard (*) for matching
40
+ const normalizedSegment = segment[0] === ':' ? '*' : segment;
41
+ let childNode = node.children.get(normalizedSegment);
42
+ if (!childNode) {
43
+ childNode = this.createEmptyRouteTreeNode(normalizedSegment);
44
+ node.children.set(normalizedSegment, childNode);
45
+ }
46
+ node = childNode;
47
+ }
48
+ // At the leaf node, store the full route and its associated metadata
49
+ node.metadata = {
50
+ ...metadata,
51
+ route: normalizedRoute,
52
+ };
53
+ node.insertionIndex = this.insertionIndexCounter++;
54
+ }
55
+ /**
56
+ * Matches a given route against the route tree and returns the best matching route's metadata.
57
+ * The best match is determined by the lowest insertion index, meaning the earliest defined route
58
+ * takes precedence.
59
+ *
60
+ * @param route - The route path to match against the route tree.
61
+ * @returns The metadata of the best matching route or `undefined` if no match is found.
62
+ */
63
+ match(route) {
64
+ const segments = stripTrailingSlash(route).split('/');
65
+ return this.traverseBySegments(segments)?.metadata;
66
+ }
67
+ /**
68
+ * Converts the route tree into a serialized format representation.
69
+ * This method converts the route tree into an array of metadata objects that describe the structure of the tree.
70
+ * The array represents the routes in a nested manner where each entry includes the route and its associated metadata.
71
+ *
72
+ * @returns An array of `RouteTreeNodeMetadata` objects representing the route tree structure.
73
+ * Each object includes the `route` and associated metadata of a route.
74
+ */
75
+ toObject() {
76
+ return Array.from(this.traverse());
77
+ }
78
+ /**
79
+ * Constructs a `RouteTree` from an object representation.
80
+ * This method is used to recreate a `RouteTree` instance from an array of metadata objects.
81
+ * The array should be in the format produced by `toObject`, allowing for the reconstruction of the route tree
82
+ * with the same routes and metadata.
83
+ *
84
+ * @param value - An array of `RouteTreeNodeMetadata` objects that represent the serialized format of the route tree.
85
+ * Each object should include a `route` and its associated metadata.
86
+ * @returns A new `RouteTree` instance constructed from the provided metadata objects.
87
+ */
88
+ static fromObject(value) {
89
+ const tree = new RouteTree();
90
+ for (const { route, ...metadata } of value) {
91
+ tree.insert(route, metadata);
92
+ }
93
+ return tree;
94
+ }
95
+ /**
96
+ * A generator function that recursively traverses the route tree and yields the metadata of each node.
97
+ * This allows for easy and efficient iteration over all nodes in the tree.
98
+ *
99
+ * @param node - The current node to start the traversal from. Defaults to the root node of the tree.
100
+ */
101
+ *traverse(node = this.root) {
102
+ if (node.metadata) {
103
+ yield node.metadata;
104
+ }
105
+ for (const childNode of node.children.values()) {
106
+ yield* this.traverse(childNode);
107
+ }
108
+ }
109
+ /**
110
+ * Recursively traverses the route tree from a given node, attempting to match the remaining route segments.
111
+ * If the node is a leaf node (no more segments to match) and contains metadata, the node is yielded.
112
+ *
113
+ * This function prioritizes exact segment matches first, followed by wildcard matches (`*`),
114
+ * and finally deep wildcard matches (`**`) that consume all segments.
115
+ *
116
+ * @param remainingSegments - The remaining segments of the route path to match.
117
+ * @param node - The current node in the route tree to start traversal from.
118
+ *
119
+ * @returns The node that best matches the remaining segments or `undefined` if no match is found.
120
+ */
121
+ traverseBySegments(remainingSegments, node = this.root) {
122
+ const { metadata, children } = node;
123
+ // If there are no remaining segments and the node has metadata, return this node
124
+ if (!remainingSegments?.length) {
125
+ if (metadata) {
126
+ return node;
127
+ }
128
+ return;
129
+ }
130
+ // If the node has no children, end the traversal
131
+ if (!children.size) {
132
+ return;
133
+ }
134
+ const [segment, ...restSegments] = remainingSegments;
135
+ let currentBestMatchNode;
136
+ // 1. Exact segment match
137
+ const exactMatchNode = node.children.get(segment);
138
+ currentBestMatchNode = this.getHigherPriorityNode(currentBestMatchNode, this.traverseBySegments(restSegments, exactMatchNode));
139
+ // 2. Wildcard segment match (`*`)
140
+ const wildcardNode = node.children.get('*');
141
+ currentBestMatchNode = this.getHigherPriorityNode(currentBestMatchNode, this.traverseBySegments(restSegments, wildcardNode));
142
+ // 3. Deep wildcard segment match (`**`)
143
+ const deepWildcardNode = node.children.get('**');
144
+ currentBestMatchNode = this.getHigherPriorityNode(currentBestMatchNode, deepWildcardNode);
145
+ return currentBestMatchNode;
146
+ }
147
+ /**
148
+ * Compares two nodes and returns the node with higher priority based on insertion index.
149
+ * A node with a lower insertion index is prioritized as it was defined earlier.
150
+ *
151
+ * @param currentBestMatchNode - The current best match node.
152
+ * @param candidateNode - The node being evaluated for higher priority based on insertion index.
153
+ * @returns The node with higher priority (i.e., lower insertion index). If one of the nodes is `undefined`, the other node is returned.
154
+ */
155
+ getHigherPriorityNode(currentBestMatchNode, candidateNode) {
156
+ if (!candidateNode) {
157
+ return currentBestMatchNode;
158
+ }
159
+ if (!currentBestMatchNode) {
160
+ return candidateNode;
161
+ }
162
+ return candidateNode.insertionIndex < currentBestMatchNode.insertionIndex
163
+ ? candidateNode
164
+ : currentBestMatchNode;
165
+ }
166
+ /**
167
+ * Creates an empty route tree node with the specified segment.
168
+ * This helper function is used during the tree construction.
169
+ *
170
+ * @param segment - The route segment that this node represents.
171
+ * @returns A new, empty route tree node.
172
+ */
173
+ createEmptyRouteTreeNode(segment) {
174
+ return {
175
+ segment,
176
+ insertionIndex: -1,
177
+ children: new Map(),
178
+ };
179
+ }
180
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @license
3
+ * Copyright Google LLC All Rights Reserved.
4
+ *
5
+ * Use of this source code is governed by an MIT-style license that can be
6
+ * found in the LICENSE file at https://angular.dev/license
7
+ */
8
+ import { ServerAssets } from '../assets';
9
+ import { joinUrlParts, stripIndexHtmlFromURL } from '../utils/url';
10
+ import { getRoutesFromAngularRouterConfig } from './ng-routes';
11
+ import { RouteTree } from './route-tree';
12
+ /**
13
+ * Manages the application's server routing logic by building and maintaining a route tree.
14
+ *
15
+ * This class is responsible for constructing the route tree from the Angular application
16
+ * configuration and using it to match incoming requests to the appropriate routes.
17
+ */
18
+ export class ServerRouter {
19
+ routeTree;
20
+ /**
21
+ * Creates an instance of the `ServerRouter`.
22
+ *
23
+ * @param routeTree - An instance of `RouteTree` that holds the routing information.
24
+ * The `RouteTree` is used to match request URLs to the appropriate route metadata.
25
+ */
26
+ constructor(routeTree) {
27
+ this.routeTree = routeTree;
28
+ }
29
+ /**
30
+ * Static property to track the ongoing build promise.
31
+ */
32
+ static #extractionPromise;
33
+ /**
34
+ * Creates or retrieves a `ServerRouter` instance based on the provided manifest and URL.
35
+ *
36
+ * If the manifest contains pre-built routes, a new `ServerRouter` is immediately created.
37
+ * Otherwise, it builds the router by extracting routes from the Angular configuration
38
+ * asynchronously. This method ensures that concurrent builds are prevented by re-using
39
+ * the same promise.
40
+ *
41
+ * @param manifest - An instance of `AngularAppManifest` that contains the route information.
42
+ * @param url - The URL for server-side rendering. The URL is needed to configure `ServerPlatformLocation`.
43
+ * This is necessary to ensure that API requests for relative paths succeed, which is crucial for correct route extraction.
44
+ * [Reference](https://github.com/angular/angular/blob/d608b857c689d17a7ffa33bbb510301014d24a17/packages/platform-server/src/location.ts#L51)
45
+ * @returns A promise resolving to a `ServerRouter` instance.
46
+ */
47
+ static from(manifest, url) {
48
+ if (manifest.routes) {
49
+ const routeTree = RouteTree.fromObject(manifest.routes);
50
+ return Promise.resolve(new ServerRouter(routeTree));
51
+ }
52
+ // Create and store a new promise for the build process.
53
+ // This prevents concurrent builds by re-using the same promise.
54
+ ServerRouter.#extractionPromise ??= (async () => {
55
+ try {
56
+ const routeTree = new RouteTree();
57
+ const document = await new ServerAssets(manifest).getIndexServerHtml();
58
+ const { baseHref, routes } = await getRoutesFromAngularRouterConfig(manifest.bootstrap(), document, url);
59
+ for (let { route, redirectTo } of routes) {
60
+ route = joinUrlParts(baseHref, route);
61
+ redirectTo = redirectTo === undefined ? undefined : joinUrlParts(baseHref, redirectTo);
62
+ routeTree.insert(route, { redirectTo });
63
+ }
64
+ return new ServerRouter(routeTree);
65
+ }
66
+ finally {
67
+ ServerRouter.#extractionPromise = undefined;
68
+ }
69
+ })();
70
+ return ServerRouter.#extractionPromise;
71
+ }
72
+ /**
73
+ * Matches a request URL against the route tree to retrieve route metadata.
74
+ *
75
+ * This method strips 'index.html' from the URL if it is present and then attempts
76
+ * to find a match in the route tree. If a match is found, it returns the associated
77
+ * route metadata; otherwise, it returns `undefined`.
78
+ *
79
+ * @param url - The URL to be matched against the route tree.
80
+ * @returns The metadata for the matched route or `undefined` if no match is found.
81
+ */
82
+ match(url) {
83
+ // Strip 'index.html' from URL if present.
84
+ // A request to `http://www.example.com/page/index.html` will render the Angular route corresponding to `http://www.example.com/page`.
85
+ const { pathname } = stripIndexHtmlFromURL(url);
86
+ return this.routeTree.match(decodeURIComponent(pathname));
87
+ }
88
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @license
3
+ * Copyright Google LLC All Rights Reserved.
4
+ *
5
+ * Use of this source code is governed by an MIT-style license that can be
6
+ * found in the LICENSE file at https://angular.dev/license
7
+ */
8
+ import { InjectionToken } from '@angular/core';
9
+ /**
10
+ * Injection token for the current request.
11
+ */
12
+ export const REQUEST = new InjectionToken('REQUEST');
13
+ /**
14
+ * Injection token for the response initialization options.
15
+ */
16
+ export const RESPONSE_INIT = new InjectionToken('RESPONSE_INIT');
17
+ /**
18
+ * Injection token for additional request context.
19
+ */
20
+ export const REQUEST_CONTEXT = new InjectionToken('REQUEST_CONTEXT');
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @license
3
+ * Copyright Google LLC All Rights Reserved.
4
+ *
5
+ * Use of this source code is governed by an MIT-style license that can be
6
+ * found in the LICENSE file at https://angular.dev/license
7
+ */
8
+ import { renderApplication, renderModule } from '@angular/platform-server';
9
+ import { stripIndexHtmlFromURL } from './url';
10
+ /**
11
+ * Renders an Angular application or module to an HTML string.
12
+ *
13
+ * This function determines whether the provided `bootstrap` value is an Angular module
14
+ * or a bootstrap function and calls the appropriate rendering method (`renderModule` or
15
+ * `renderApplication`) based on that determination.
16
+ *
17
+ * @param html - The HTML string to be used as the initial document content.
18
+ * @param bootstrap - Either an Angular module type or a function that returns a promise
19
+ * resolving to an `ApplicationRef`.
20
+ * @param url - The URL of the application. This is used for server-side rendering to
21
+ * correctly handle route-based rendering.
22
+ * @param platformProviders - An array of platform providers to be used during the
23
+ * rendering process.
24
+ * @returns A promise that resolves to a string containing the rendered HTML.
25
+ */
26
+ export function renderAngular(html, bootstrap, url, platformProviders) {
27
+ // A request to `http://www.example.com/page/index.html` will render the Angular route corresponding to `http://www.example.com/page`.
28
+ const urlToRender = stripIndexHtmlFromURL(url).toString();
29
+ return isNgModule(bootstrap)
30
+ ? renderModule(bootstrap, {
31
+ url: urlToRender,
32
+ document: html,
33
+ extraProviders: platformProviders,
34
+ })
35
+ : renderApplication(bootstrap, {
36
+ url: urlToRender,
37
+ document: html,
38
+ platformProviders,
39
+ });
40
+ }
41
+ /**
42
+ * Type guard to determine if a given value is an Angular module.
43
+ * Angular modules are identified by the presence of the `ɵmod` static property.
44
+ * This function helps distinguish between Angular modules and bootstrap functions.
45
+ *
46
+ * @param value - The value to be checked.
47
+ * @returns True if the value is an Angular module (i.e., it has the `ɵmod` property), false otherwise.
48
+ */
49
+ export function isNgModule(value) {
50
+ return 'ɵmod' in value;
51
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @license
3
+ * Copyright Google LLC All Rights Reserved.
4
+ *
5
+ * Use of this source code is governed by an MIT-style license that can be
6
+ * found in the LICENSE file at https://angular.dev/license
7
+ */
8
+ /**
9
+ * Removes the trailing slash from a URL if it exists.
10
+ *
11
+ * @param url - The URL string from which to remove the trailing slash.
12
+ * @returns The URL string without a trailing slash.
13
+ *
14
+ * @example
15
+ * ```js
16
+ * stripTrailingSlash('path/'); // 'path'
17
+ * stripTrailingSlash('/path'); // '/path'
18
+ * ```
19
+ */
20
+ export function stripTrailingSlash(url) {
21
+ // Check if the last character of the URL is a slash
22
+ return url[url.length - 1] === '/' ? url.slice(0, -1) : url;
23
+ }
24
+ /**
25
+ * Joins URL parts into a single URL string.
26
+ *
27
+ * This function takes multiple URL segments, normalizes them by removing leading
28
+ * and trailing slashes where appropriate, and then joins them into a single URL.
29
+ *
30
+ * @param parts - The parts of the URL to join. Each part can be a string with or without slashes.
31
+ * @returns The joined URL string, with normalized slashes.
32
+ *
33
+ * @example
34
+ * ```js
35
+ * joinUrlParts('path/', '/to/resource'); // '/path/to/resource'
36
+ * joinUrlParts('/path/', 'to/resource'); // '/path/to/resource'
37
+ * ```
38
+ */
39
+ export function joinUrlParts(...parts) {
40
+ // Initialize an array with an empty string to always add a leading slash
41
+ const normalizeParts = [''];
42
+ for (const part of parts) {
43
+ if (part === '') {
44
+ // Skip any empty parts
45
+ continue;
46
+ }
47
+ let normalizedPart = part;
48
+ if (part[0] === '/') {
49
+ normalizedPart = normalizedPart.slice(1);
50
+ }
51
+ if (part[part.length - 1] === '/') {
52
+ normalizedPart = normalizedPart.slice(0, -1);
53
+ }
54
+ if (normalizedPart !== '') {
55
+ normalizeParts.push(normalizedPart);
56
+ }
57
+ }
58
+ return normalizeParts.join('/');
59
+ }
60
+ /**
61
+ * Strips `/index.html` from the end of a URL's path, if present.
62
+ *
63
+ * This function is used to convert URLs pointing to an `index.html` file into their directory
64
+ * equivalents. For example, it transforms a URL like `http://www.example.com/page/index.html`
65
+ * into `http://www.example.com/page`.
66
+ *
67
+ * @param url - The URL object to process.
68
+ * @returns A new URL object with `/index.html` removed from the path, if it was present.
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * const originalUrl = new URL('http://www.example.com/page/index.html');
73
+ * const cleanedUrl = stripIndexHtmlFromURL(originalUrl);
74
+ * console.log(cleanedUrl.href); // Output: 'http://www.example.com/page'
75
+ * ```
76
+ */
77
+ export function stripIndexHtmlFromURL(url) {
78
+ if (url.pathname.endsWith('/index.html')) {
79
+ const modifiedURL = new URL(url);
80
+ // Remove '/index.html' from the pathname
81
+ modifiedURL.pathname = modifiedURL.pathname.slice(0, /** '/index.html'.length */ -11);
82
+ return modifiedURL;
83
+ }
84
+ return url;
85
+ }